2022-08-25 23:36:46 +00:00
import Alamofire
2021-10-21 23:29:10 +00:00
import AVKit
2021-07-07 22:39:18 +00:00
import Defaults
2021-06-28 10:43:07 +00:00
import Foundation
import Siesta
import SwiftyJSON
2021-10-20 22:21:50 +00:00
final class InvidiousAPI : Service , ObservableObject , VideosAPI {
2021-09-25 08:18:22 +00:00
static let basePath = " /api/v1 "
2021-06-28 10:43:07 +00:00
2021-10-20 22:21:50 +00:00
@ Published var account : Account !
2022-08-25 23:36:46 +00:00
2022-12-09 00:15:19 +00:00
static func withAnonymousAccountForInstanceURL ( _ url : URL ) -> InvidiousAPI {
. init ( account : Instance ( app : . invidious , apiURLString : url . absoluteString ) . anonymousAccount )
}
2022-08-25 23:36:46 +00:00
var signedIn : Bool {
2022-09-28 14:27:01 +00:00
guard let account else { return false }
2022-08-25 23:36:46 +00:00
return ! account . anonymous && ! ( account . token ? . isEmpty ? ? true )
}
2021-06-28 10:43:07 +00:00
2021-10-20 22:21:50 +00:00
init ( account : Account ? = nil ) {
2021-10-16 22:48:58 +00:00
super . init ( )
guard ! account . isNil else {
2021-10-17 21:49:56 +00:00
self . account = . init ( name : " Empty " )
2021-10-16 22:48:58 +00:00
return
}
setAccount ( account ! )
}
2021-10-20 22:21:50 +00:00
func setAccount ( _ account : Account ) {
2021-09-25 08:18:22 +00:00
self . account = account
configure ( )
}
func configure ( ) {
2022-08-25 23:36:46 +00:00
invalidateConfiguration ( )
2021-06-28 10:43:07 +00:00
configure {
2022-08-25 23:36:46 +00:00
if let cookie = self . cookieHeader {
$0 . headers [ " Cookie " ] = cookie
2021-10-16 22:48:58 +00:00
}
2021-06-28 10:43:07 +00:00
$0 . pipeline [ . parsing ] . add ( SwiftyJSONTransformer , contentTypes : [ " */json " ] )
}
configure ( " ** " , requestMethods : [ . post ] ) {
$0 . pipeline [ . parsing ] . removeTransformers ( )
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " popular " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-12-17 16:39:26 +00:00
content . json . arrayValue . map ( self . extractVideo )
2021-06-28 10:43:07 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " trending " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-12-17 16:39:26 +00:00
content . json . arrayValue . map ( self . extractVideo )
2021-06-28 10:43:07 +00:00
}
2022-01-04 23:18:01 +00:00
configureTransformer ( pathPattern ( " search " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> SearchPage in
2022-03-28 19:26:38 +00:00
let results = content . json . arrayValue . compactMap { json -> ContentItem ? in
2022-06-18 11:24:23 +00:00
let type = json . dictionaryValue [ " type " ] ? . string
2021-10-21 23:29:10 +00:00
if type = = " channel " {
2022-01-04 23:18:01 +00:00
return ContentItem ( channel : self . extractChannel ( from : json ) )
2023-06-17 12:09:51 +00:00
}
if type = = " playlist " {
2022-01-04 23:18:01 +00:00
return ContentItem ( playlist : self . extractChannelPlaylist ( from : json ) )
2023-06-17 12:09:51 +00:00
}
if type = = " video " {
2022-03-28 19:26:38 +00:00
return ContentItem ( video : self . extractVideo ( from : json ) )
2021-10-21 23:29:10 +00:00
}
2022-03-28 19:26:38 +00:00
return nil
2021-10-21 23:29:10 +00:00
}
2022-01-04 23:18:01 +00:00
return SearchPage ( results : results , last : results . isEmpty )
2021-06-28 10:43:07 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " search/suggestions " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ String ] in
2021-09-13 20:41:16 +00:00
if let suggestions = content . json . dictionaryValue [ " suggestions " ] {
2024-08-18 12:46:51 +00:00
return suggestions . arrayValue . map ( \ . stringValue ) . map ( \ . replacingHTMLEntities )
2021-09-13 20:41:16 +00:00
}
return [ ]
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " auth/playlists " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Playlist ] in
2021-12-17 16:39:26 +00:00
content . json . arrayValue . map ( self . extractPlaylist )
2021-06-28 10:43:07 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " auth/playlists/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> Playlist in
2021-12-17 16:39:26 +00:00
self . extractPlaylist ( from : content . json )
2021-08-29 21:36:18 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " auth/playlists " ) , requestMethods : [ . post , . patch ] ) { ( content : Entity < Data > ) -> Playlist in
2022-12-11 17:04:39 +00:00
self . extractPlaylist ( from : JSON ( parseJSON : String ( data : content . content , encoding : . utf8 ) ! ) )
2021-07-08 15:14:54 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " auth/feed " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-06-28 10:43:07 +00:00
if let feedVideos = content . json . dictionaryValue [ " videos " ] {
2021-12-17 16:39:26 +00:00
return feedVideos . arrayValue . map ( self . extractVideo )
2021-06-28 10:43:07 +00:00
}
return [ ]
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " auth/subscriptions " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Channel ] in
2021-12-17 16:39:26 +00:00
content . json . arrayValue . map ( self . extractChannel )
2021-08-25 22:12:59 +00:00
}
2023-02-28 20:03:02 +00:00
configureTransformer ( pathPattern ( " channels/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPage in
self . extractChannelPage ( from : content . json , forceNotLast : true )
}
configureTransformer ( pathPattern ( " channels/*/videos " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPage in
self . extractChannelPage ( from : content . json )
2021-06-28 10:43:07 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " channels/*/latest " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2023-01-27 20:02:02 +00:00
content . json . dictionaryValue [ " videos " ] ? . arrayValue . map ( self . extractVideo ) ? ? [ ]
2021-09-18 20:36:42 +00:00
}
2024-04-01 13:08:08 +00:00
for type in [ " latest " , " playlists " , " streams " , " shorts " , " channels " , " videos " , " releases " , " podcasts " ] {
2023-02-28 20:03:02 +00:00
configureTransformer ( pathPattern ( " channels/*/ \( type ) " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPage in
self . extractChannelPage ( from : content . json )
}
2022-11-27 10:42:16 +00:00
}
2021-10-22 23:04:03 +00:00
configureTransformer ( pathPattern ( " playlists/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPlaylist in
2021-12-17 16:39:26 +00:00
self . extractChannelPlaylist ( from : content . json )
2021-10-22 23:04:03 +00:00
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " videos/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> Video in
2021-12-17 16:39:26 +00:00
self . extractVideo ( from : content . json )
2021-06-28 10:43:07 +00:00
}
2022-07-01 22:14:04 +00:00
configureTransformer ( pathPattern ( " comments/* " ) ) { ( content : Entity < JSON > ) -> CommentsPage in
let details = content . json . dictionaryValue
let comments = details [ " comments " ] ? . arrayValue . compactMap { self . extractComment ( from : $0 ) } ? ? [ ]
let nextPage = details [ " continuation " ] ? . string
let disabled = ! details [ " error " ] . isNil
return CommentsPage ( comments : comments , nextPage : nextPage , disabled : disabled )
}
2022-08-25 23:36:46 +00:00
2022-12-14 16:23:04 +00:00
if account . token . isNil || account . token ! . isEmpty {
updateToken ( )
} else {
FeedModel . shared . onAccountChange ( )
2022-12-20 22:51:04 +00:00
SubscribedChannelsModel . shared . onAccountChange ( )
PlaylistsModel . shared . onAccountChange ( )
2022-12-14 16:23:04 +00:00
}
2022-08-25 23:36:46 +00:00
}
func updateToken ( force : Bool = false ) {
let ( username , password ) = AccountsModel . getCredentials ( account )
guard ! account . anonymous ,
( account . token ? . isEmpty ? ? true ) || force
else {
return
}
2022-09-28 14:27:01 +00:00
guard let username ,
let password ,
2022-08-25 23:36:46 +00:00
! username . isEmpty ,
! password . isEmpty
else {
NavigationModel . shared . presentAlert (
title : " Account Error " ,
message : " Remove and add your account again in Settings. "
)
return
}
let presentTokenUpdateFailedAlert : ( AFDataResponse < Data ? >? , String ? ) -> Void = { response , message in
NavigationModel . shared . presentAlert (
title : " Account Error " ,
message : message ? ? " \( response ? . response ? . statusCode ? ? - 1 ) - \( response ? . error ? . errorDescription ? ? " unknown " ) \n If this issue persists, try removing and adding your account again in Settings. "
)
}
AF
. request ( login . url , method : . post , parameters : [ " email " : username , " password " : password ] , encoding : URLEncoding . default )
. redirect ( using : . doNotFollow )
. response { response in
guard let headers = response . response ? . headers ,
let cookies = headers [ " Set-Cookie " ]
else {
presentTokenUpdateFailedAlert ( response , nil )
return
}
let sidRegex = # " SID=(?<sid>[^;]*); " #
guard let sidRegex = try ? NSRegularExpression ( pattern : sidRegex ) ,
let match = sidRegex . matches ( in : cookies , range : NSRange ( cookies . startIndex . . . , in : cookies ) ) . first
else {
2022-10-12 16:49:47 +00:00
presentTokenUpdateFailedAlert ( nil , String ( format : " Could not extract SID from received cookies: %@ " . localized ( ) , cookies ) )
2022-08-25 23:36:46 +00:00
return
}
let matchRange = match . range ( withName : " sid " )
if let substringRange = Range ( matchRange , in : cookies ) {
let sid = String ( cookies [ substringRange ] )
AccountsModel . setToken ( self . account , sid )
2022-08-31 20:00:24 +00:00
self . objectWillChange . send ( )
2022-08-25 23:36:46 +00:00
} else {
2022-10-12 16:49:47 +00:00
presentTokenUpdateFailedAlert ( nil , String ( format : " Could not extract SID from received cookies: %@ " . localized ( ) , cookies ) )
2022-08-25 23:36:46 +00:00
}
2022-08-31 20:00:24 +00:00
self . configure ( )
2022-08-25 23:36:46 +00:00
}
}
var login : Resource {
resource ( baseURL : account . url , path : " login " )
2021-06-28 10:43:07 +00:00
}
2021-10-21 23:29:10 +00:00
private func pathPattern ( _ path : String ) -> String {
2022-05-20 19:53:17 +00:00
" ** \( Self . basePath ) / \( path ) "
2021-09-25 08:18:22 +00:00
}
2021-10-21 23:29:10 +00:00
private func basePathAppending ( _ path : String ) -> String {
2022-05-20 19:53:17 +00:00
" \( Self . basePath ) / \( path ) "
2021-09-25 08:18:22 +00:00
}
2022-08-25 23:36:46 +00:00
private var cookieHeader : String ? {
guard let token = account ? . token , ! token . isEmpty else { return nil }
return " SID= \( token ) "
2021-09-25 08:18:22 +00:00
}
2021-06-28 10:43:07 +00:00
2021-10-20 22:21:50 +00:00
var popular : Resource ? {
2022-05-20 19:53:17 +00:00
resource ( baseURL : account . url , path : " \( Self . basePath ) /popular " )
2021-06-28 10:43:07 +00:00
}
2021-10-20 22:21:50 +00:00
func trending ( country : Country , category : TrendingCategory ? ) -> Resource {
2022-05-20 19:53:17 +00:00
resource ( baseURL : account . url , path : " \( Self . basePath ) /trending " )
2023-05-26 22:24:53 +00:00
. withParam ( " type " , category ? . type )
2021-06-28 10:43:07 +00:00
. withParam ( " region " , country . rawValue )
}
2021-10-20 22:21:50 +00:00
var home : Resource ? {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : " /feed/subscriptions " )
2021-09-19 17:31:21 +00:00
}
2022-12-10 02:01:59 +00:00
func feed ( _ page : Int ? ) -> Resource ? {
2022-05-20 19:53:17 +00:00
resource ( baseURL : account . url , path : " \( Self . basePath ) /auth/feed " )
2022-12-10 02:01:59 +00:00
. withParam ( " page " , String ( page ? ? 1 ) )
}
var feed : Resource ? {
resource ( baseURL : account . url , path : basePathAppending ( " auth/feed " ) )
}
2021-10-20 22:21:50 +00:00
var subscriptions : Resource ? {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " auth/subscriptions " ) )
2021-08-25 22:12:59 +00:00
}
2021-11-14 23:06:01 +00:00
func subscribe ( _ channelID : String , onCompletion : @ escaping ( ) -> Void = { } ) {
resource ( baseURL : account . url , path : basePathAppending ( " auth/subscriptions " ) )
. child ( channelID )
. request ( . post )
. onCompletion { _ in onCompletion ( ) }
}
func unsubscribe ( _ channelID : String , onCompletion : @ escaping ( ) -> Void ) {
resource ( baseURL : account . url , path : basePathAppending ( " auth/subscriptions " ) )
. child ( channelID )
. request ( . delete )
. onCompletion { _ in onCompletion ( ) }
2021-08-25 22:12:59 +00:00
}
2023-02-28 20:03:02 +00:00
func channel ( _ id : String , contentType : Channel . ContentType , data _ : String ? , page : String ? ) -> Resource {
if page . isNil , contentType = = . videos {
return resource ( baseURL : account . url , path : basePathAppending ( " channels/ \( id ) " ) )
2022-11-27 10:42:16 +00:00
}
2023-02-28 20:03:02 +00:00
var resource = resource ( baseURL : account . url , path : basePathAppending ( " channels/ \( id ) / \( contentType . invidiousID ) " ) )
if let page , ! page . isEmpty {
resource = resource . withParam ( " continuation " , page )
}
return resource
2021-06-28 10:43:07 +00:00
}
2022-06-24 22:48:57 +00:00
func channelByName ( _ : String ) -> Resource ? {
nil
}
2022-06-29 23:31:51 +00:00
func channelByUsername ( _ : String ) -> Resource ? {
nil
}
2021-09-18 20:36:42 +00:00
func channelVideos ( _ id : String ) -> Resource {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " channels/ \( id ) /latest " ) )
2021-09-18 20:36:42 +00:00
}
2021-06-28 10:43:07 +00:00
func video ( _ id : String ) -> Resource {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " videos/ \( id ) " ) )
2021-06-28 10:43:07 +00:00
}
2021-10-20 22:21:50 +00:00
var playlists : Resource ? {
2021-11-14 23:06:01 +00:00
if account . isNil || account . anonymous {
return nil
}
return resource ( baseURL : account . url , path : basePathAppending ( " auth/playlists " ) )
2021-06-28 10:43:07 +00:00
}
2021-10-20 22:21:50 +00:00
func playlist ( _ id : String ) -> Resource ? {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " auth/playlists/ \( id ) " ) )
2021-07-08 17:18:36 +00:00
}
2021-10-20 22:21:50 +00:00
func playlistVideos ( _ id : String ) -> Resource ? {
playlist ( id ) ? . child ( " videos " )
2021-07-09 14:53:53 +00:00
}
2021-10-20 22:21:50 +00:00
func playlistVideo ( _ playlistID : String , _ videoID : String ) -> Resource ? {
playlist ( playlistID ) ? . child ( " videos " ) . child ( videoID )
2021-07-09 14:53:53 +00:00
}
2022-05-21 22:29:51 +00:00
func addVideoToPlaylist (
_ videoID : String ,
_ playlistID : String ,
onFailure : @ escaping ( RequestError ) -> Void = { _ in } ,
onSuccess : @ escaping ( ) -> Void = { }
) {
let resource = playlistVideos ( playlistID )
let body = [ " videoId " : videoID ]
resource ?
. request ( . post , json : body )
. onSuccess { _ in onSuccess ( ) }
. onFailure ( onFailure )
}
func removeVideoFromPlaylist (
_ index : String ,
_ playlistID : String ,
onFailure : @ escaping ( RequestError ) -> Void ,
onSuccess : @ escaping ( ) -> Void
) {
let resource = playlistVideo ( playlistID , index )
resource ?
. request ( . delete )
. onSuccess { _ in onSuccess ( ) }
. onFailure ( onFailure )
}
func playlistForm (
_ name : String ,
_ visibility : String ,
playlist : Playlist ? ,
onFailure : @ escaping ( RequestError ) -> Void ,
onSuccess : @ escaping ( Playlist ? ) -> Void
) {
let body = [ " title " : name , " privacy " : visibility ]
let resource = ! playlist . isNil ? self . playlist ( playlist ! . id ) : playlists
resource ?
. request ( ! playlist . isNil ? . patch : . post , json : body )
. onSuccess { response in
if let modifiedPlaylist : Playlist = response . typedContent ( ) {
onSuccess ( modifiedPlaylist )
}
}
. onFailure ( onFailure )
}
func deletePlaylist (
_ playlist : Playlist ,
onFailure : @ escaping ( RequestError ) -> Void ,
onSuccess : @ escaping ( ) -> Void
) {
self . playlist ( playlist . id ) ?
. request ( . delete )
. onSuccess { _ in onSuccess ( ) }
. onFailure ( onFailure )
}
2021-10-22 23:04:03 +00:00
func channelPlaylist ( _ id : String ) -> Resource ? {
resource ( baseURL : account . url , path : basePathAppending ( " playlists/ \( id ) " ) )
}
2022-01-04 23:18:01 +00:00
func search ( _ query : SearchQuery , page : String ? ) -> Resource {
2021-09-25 08:18:22 +00:00
var resource = resource ( baseURL : account . url , path : basePathAppending ( " search " ) )
2021-07-07 22:39:18 +00:00
. withParam ( " q " , searchQuery ( query . query ) )
. withParam ( " sort_by " , query . sortBy . parameter )
2021-10-21 23:29:10 +00:00
. withParam ( " type " , " all " )
2021-07-07 22:39:18 +00:00
2021-09-26 17:40:25 +00:00
if let date = query . date , date != . any {
resource = resource . withParam ( " date " , date . rawValue )
2021-07-07 22:39:18 +00:00
}
2021-09-26 17:40:25 +00:00
if let duration = query . duration , duration != . any {
resource = resource . withParam ( " duration " , duration . rawValue )
2021-07-07 22:39:18 +00:00
}
2022-09-28 14:27:01 +00:00
if let page {
2022-01-04 23:18:01 +00:00
resource = resource . withParam ( " page " , page )
}
2021-07-07 22:39:18 +00:00
return resource
2021-06-28 10:43:07 +00:00
}
2021-09-13 20:41:16 +00:00
func searchSuggestions ( query : String ) -> Resource {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " search/suggestions " ) )
2021-09-13 20:41:16 +00:00
. withParam ( " q " , query . lowercased ( ) )
}
2022-07-01 22:14:04 +00:00
func comments ( _ id : Video . ID , page : String ? ) -> Resource ? {
let resource = resource ( baseURL : account . url , path : basePathAppending ( " comments/ \( id ) " ) )
2022-09-28 14:27:01 +00:00
guard let page else { return resource }
2022-07-01 22:14:04 +00:00
return resource . withParam ( " continuation " , page )
}
2021-12-04 19:35:41 +00:00
2021-06-28 10:43:07 +00:00
private func searchQuery ( _ query : String ) -> String {
var searchQuery = query
let url = URLComponents ( string : query )
if url != nil ,
url ! . host = = " youtu.be "
{
searchQuery = url ! . path . replacingOccurrences ( of : " / " , with : " " )
}
let queryItem = url ? . queryItems ? . first { item in item . name = = " v " }
if let id = queryItem ? . value {
searchQuery = id
}
2021-07-08 15:14:54 +00:00
return searchQuery
2021-06-28 10:43:07 +00:00
}
2021-10-21 23:29:10 +00:00
2021-10-22 15:00:09 +00:00
static func proxiedAsset ( instance : Instance , asset : AVURLAsset ) -> AVURLAsset ? {
2022-12-09 00:15:19 +00:00
guard let instanceURLComponents = URLComponents ( url : instance . apiURL , resolvingAgainstBaseURL : false ) ,
2021-10-22 15:00:09 +00:00
var urlComponents = URLComponents ( url : asset . url , resolvingAgainstBaseURL : false ) else { return nil }
2021-10-21 23:29:10 +00:00
urlComponents . scheme = instanceURLComponents . scheme
urlComponents . host = instanceURLComponents . host
2021-10-22 15:00:09 +00:00
guard let url = urlComponents . url else {
return nil
}
return AVURLAsset ( url : url )
2021-10-21 23:29:10 +00:00
}
2021-12-17 16:39:26 +00:00
func extractVideo ( from json : JSON ) -> Video {
2021-10-21 23:29:10 +00:00
let indexID : String ?
var id : Video . ID
2022-12-13 20:55:03 +00:00
var published = json [ " publishedText " ] . stringValue
2021-10-21 23:29:10 +00:00
var publishedAt : Date ?
if let publishedInterval = json [ " published " ] . double {
publishedAt = Date ( timeIntervalSince1970 : publishedInterval )
2022-12-13 20:55:03 +00:00
published = " "
2021-10-21 23:29:10 +00:00
}
let videoID = json [ " videoId " ] . stringValue
if let index = json [ " indexId " ] . string {
indexID = index
id = videoID + index
} else {
indexID = nil
id = videoID
}
2022-06-18 12:39:49 +00:00
let description = json [ " description " ] . stringValue
2023-02-25 15:42:18 +00:00
let length = json [ " lengthSeconds " ] . doubleValue
2022-06-18 12:39:49 +00:00
2021-10-21 23:29:10 +00:00
return Video (
2022-12-09 00:15:19 +00:00
instanceID : account . instanceID ,
app : . invidious ,
instanceURL : account . instance . apiURL ,
2022-12-18 12:39:39 +00:00
id : id ,
2021-10-21 23:29:10 +00:00
videoID : videoID ,
title : json [ " title " ] . stringValue ,
author : json [ " author " ] . stringValue ,
2023-02-25 15:42:18 +00:00
length : length ,
2022-12-13 20:55:03 +00:00
published : published ,
2021-10-21 23:29:10 +00:00
views : json [ " viewCount " ] . intValue ,
2022-06-18 12:39:49 +00:00
description : description ,
2021-10-21 23:29:10 +00:00
genre : json [ " genre " ] . stringValue ,
channel : extractChannel ( from : json ) ,
thumbnails : extractThumbnails ( from : json ) ,
indexID : indexID ,
live : json [ " liveNow " ] . boolValue ,
upcoming : json [ " isUpcoming " ] . boolValue ,
2023-02-25 15:42:18 +00:00
short : length <= Video . shortLength ,
2021-10-21 23:29:10 +00:00
publishedAt : publishedAt ,
likes : json [ " likeCount " ] . int ,
dislikes : json [ " dislikeCount " ] . int ,
2022-06-18 11:24:23 +00:00
keywords : json [ " keywords " ] . arrayValue . compactMap { $0 . string } ,
2021-11-02 23:02:02 +00:00
streams : extractStreams ( from : json ) ,
2022-06-18 12:39:49 +00:00
related : extractRelated ( from : json ) ,
2024-05-20 18:11:41 +00:00
chapters : createChapters ( from : description , thumbnails : json ) ,
2022-07-05 17:20:25 +00:00
captions : extractCaptions ( from : json )
2021-10-21 23:29:10 +00:00
)
}
2021-12-17 16:39:26 +00:00
func extractChannel ( from json : JSON ) -> Channel {
2022-06-18 11:24:23 +00:00
var thumbnailURL = json [ " authorThumbnails " ] . arrayValue . last ? . dictionaryValue [ " url " ] ? . string ? ? " "
2021-12-17 16:39:26 +00:00
2022-06-15 21:48:38 +00:00
// a p p e n d p r o t o c o l t o u n p r o x i e d t h u m b n a i l U R L i f i t ' s m i s s i n g
2021-12-17 16:39:26 +00:00
if thumbnailURL . count > 2 ,
2022-06-15 21:48:38 +00:00
String ( thumbnailURL [ . . < thumbnailURL . index ( thumbnailURL . startIndex , offsetBy : 2 ) ] ) = = " // " ,
2022-12-09 00:15:19 +00:00
let accountUrlComponents = URLComponents ( string : account . urlString )
2021-12-17 16:39:26 +00:00
{
2022-06-15 21:48:38 +00:00
thumbnailURL = " \( accountUrlComponents . scheme ? ? " https " ) : \( thumbnailURL ) "
2021-12-17 16:39:26 +00:00
}
2021-10-21 23:29:10 +00:00
2023-02-28 20:03:02 +00:00
let tabs = json [ " tabs " ] . arrayValue . compactMap { name in
if let name = name . string , let type = Channel . ContentType . from ( name ) {
return Channel . Tab ( contentType : type , data : " " )
}
return nil
}
2021-10-21 23:29:10 +00:00
return Channel (
2022-12-13 23:07:32 +00:00
app : . invidious ,
2021-10-21 23:29:10 +00:00
id : json [ " authorId " ] . stringValue ,
name : json [ " author " ] . stringValue ,
2022-11-27 10:42:16 +00:00
bannerURL : json [ " authorBanners " ] . arrayValue . first ? . dictionaryValue [ " url " ] ? . url ,
2021-10-21 23:29:10 +00:00
thumbnailURL : URL ( string : thumbnailURL ) ,
2022-11-27 10:42:16 +00:00
description : json [ " description " ] . stringValue ,
2021-10-21 23:29:10 +00:00
subscriptionsCount : json [ " subCount " ] . int ,
subscriptionsText : json [ " subCountText " ] . string ,
2022-11-27 10:42:16 +00:00
totalViews : json [ " totalViews " ] . int ,
2023-02-28 20:03:02 +00:00
videos : json . dictionaryValue [ " latestVideos " ] ? . arrayValue . map ( extractVideo ) ? ? [ ] ,
tabs : tabs
2021-10-21 23:29:10 +00:00
)
}
2021-12-17 16:39:26 +00:00
func extractChannelPlaylist ( from json : JSON ) -> ChannelPlaylist {
2021-10-22 23:04:03 +00:00
let details = json . dictionaryValue
return ChannelPlaylist (
2022-06-30 08:11:11 +00:00
id : details [ " playlistId " ] ? . string ? ? details [ " mixId " ] ? . string ? ? UUID ( ) . uuidString ,
title : details [ " title " ] ? . stringValue ? ? " " ,
2021-10-22 23:04:03 +00:00
thumbnailURL : details [ " playlistThumbnail " ] ? . url ,
channel : extractChannel ( from : json ) ,
2022-11-27 10:42:16 +00:00
videos : details [ " videos " ] ? . arrayValue . compactMap ( extractVideo ) ? ? [ ] ,
videosCount : details [ " videoCount " ] ? . int
2021-10-22 23:04:03 +00:00
)
}
2021-12-17 16:39:26 +00:00
private func extractThumbnails ( from details : JSON ) -> [ Thumbnail ] {
2022-06-15 08:05:52 +00:00
details [ " videoThumbnails " ] . arrayValue . compactMap { json in
guard let url = json [ " url " ] . url ,
var components = URLComponents ( url : url , resolvingAgainstBaseURL : false ) ,
2022-06-15 21:48:38 +00:00
let quality = json [ " quality " ] . string ,
2022-12-09 00:15:19 +00:00
let accountUrlComponents = URLComponents ( string : account . urlString )
2022-06-15 08:05:52 +00:00
else {
return nil
}
2022-06-15 21:48:38 +00:00
// s o m e o f i n s t a n c e s a r e n o t c o n f i g u r e d p r o p e r l y a n d r e t u r n t h u m b n a i l s l i n k s
// w i t h i n c o r r e c t s c h e m e
components . scheme = accountUrlComponents . scheme
2022-06-15 08:05:52 +00:00
guard let thumbnailUrl = components . url else {
return nil
}
return Thumbnail ( url : thumbnailUrl , quality : . init ( rawValue : quality ) ! )
2021-10-21 23:29:10 +00:00
}
}
2024-05-20 18:11:41 +00:00
private func createChapters ( from description : String , thumbnails : JSON ) -> [ Chapter ] {
var chapters = extractChapters ( from : description )
if ! chapters . isEmpty {
let thumbnailsData = extractThumbnails ( from : thumbnails )
let thumbnailURL = thumbnailsData . first { $0 . quality = = . medium } ? . url
for chapter in chapters . indices {
if let url = thumbnailURL {
chapters [ chapter ] . image = url
}
}
}
return chapters
}
2023-02-28 20:03:02 +00:00
private static var contentItemsKeys = [ " items " , " videos " , " latestVideos " , " playlists " , " relatedChannels " ]
private func extractChannelPage ( from json : JSON , forceNotLast : Bool = false ) -> ChannelPage {
let nextPage = json . dictionaryValue [ " continuation " ] ? . string
var contentItems = [ ContentItem ] ( )
if let key = Self . contentItemsKeys . first ( where : { json . dictionaryValue . keys . contains ( $0 ) } ) ,
let items = json . dictionaryValue [ key ]
{
contentItems = extractContentItems ( from : items )
}
var last = false
if ! forceNotLast {
last = nextPage ? . isEmpty ? ? true
}
return ChannelPage (
results : contentItems ,
channel : extractChannel ( from : json ) ,
nextPage : nextPage ,
last : last
)
}
2021-12-17 16:39:26 +00:00
private func extractStreams ( from json : JSON ) -> [ Stream ] {
2022-07-21 22:44:21 +00:00
let hls = extractHLSStreams ( from : json )
if json [ " liveNow " ] . boolValue {
return hls
}
return extractFormatStreams ( from : json [ " formatStreams " ] . arrayValue ) +
extractAdaptiveFormats ( from : json [ " adaptiveFormats " ] . arrayValue ) +
hls
2021-11-02 23:02:02 +00:00
}
2021-12-17 16:39:26 +00:00
private func extractFormatStreams ( from streams : [ JSON ] ) -> [ Stream ] {
2022-06-18 11:24:23 +00:00
streams . compactMap { stream in
guard let streamURL = stream [ " url " ] . url else {
return nil
}
return SingleAssetStream (
2022-08-16 21:16:35 +00:00
instance : account . instance ,
2022-06-18 11:24:23 +00:00
avAsset : AVURLAsset ( url : streamURL ) ,
resolution : Stream . Resolution . from ( resolution : stream [ " resolution " ] . string ? ? " " ) ,
2021-10-21 23:29:10 +00:00
kind : . stream ,
2022-06-18 11:24:23 +00:00
encoding : stream [ " encoding " ] . string ? ? " "
2021-10-21 23:29:10 +00:00
)
}
}
2021-12-17 16:39:26 +00:00
private func extractAdaptiveFormats ( from streams : [ JSON ] ) -> [ Stream ] {
2022-07-10 22:42:47 +00:00
let audioStreams = streams
. filter { $0 [ " type " ] . stringValue . starts ( with : " audio/mp4 " ) }
. sorted {
$0 . dictionaryValue [ " bitrate " ] ? . int ? ? 0 >
$1 . dictionaryValue [ " bitrate " ] ? . int ? ? 0
}
guard let audioStream = audioStreams . first else {
2022-06-18 11:24:23 +00:00
return . init ( )
2021-10-21 23:29:10 +00:00
}
2022-06-18 11:24:23 +00:00
let videoStreams = streams . filter { $0 [ " type " ] . stringValue . starts ( with : " video/ " ) }
return videoStreams . compactMap { videoStream in
guard let audioAssetURL = audioStream [ " url " ] . url ,
let videoAssetURL = videoStream [ " url " ] . url
else {
return nil
}
2021-10-21 23:29:10 +00:00
2022-06-18 11:24:23 +00:00
return Stream (
2022-08-16 21:16:35 +00:00
instance : account . instance ,
2022-06-18 11:24:23 +00:00
audioAsset : AVURLAsset ( url : audioAssetURL ) ,
videoAsset : AVURLAsset ( url : videoAssetURL ) ,
resolution : Stream . Resolution . from ( resolution : videoStream [ " resolution " ] . stringValue ) ,
2021-10-21 23:29:10 +00:00
kind : . adaptive ,
2022-06-18 11:24:23 +00:00
encoding : videoStream [ " encoding " ] . string ,
2024-05-13 05:54:24 +00:00
videoFormat : videoStream [ " type " ] . string ,
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
bitrate : videoStream [ " bitrate " ] . int ,
requestRange : videoStream [ " init " ] . string ? ? videoStream [ " index " ] . string
2021-10-21 23:29:10 +00:00
)
}
}
2021-11-02 23:02:02 +00:00
2022-07-21 22:44:21 +00:00
private func extractHLSStreams ( from content : JSON ) -> [ Stream ] {
if let hlsURL = content . dictionaryValue [ " hlsUrl " ] ? . url {
2022-08-16 21:16:35 +00:00
return [ Stream ( instance : account . instance , hlsURL : hlsURL ) ]
2022-07-21 22:44:21 +00:00
}
return [ ]
}
2021-12-17 16:39:26 +00:00
private func extractRelated ( from content : JSON ) -> [ Video ] {
2021-11-02 23:02:02 +00:00
content
. dictionaryValue [ " recommendedVideos " ] ?
. arrayValue
. compactMap ( extractVideo ( from : ) ) ? ? [ ]
}
2021-12-17 16:39:26 +00:00
private func extractPlaylist ( from content : JSON ) -> Playlist {
2022-09-12 15:23:20 +00:00
let id = content [ " playlistId " ] . stringValue
return Playlist (
id : id ,
2021-12-17 16:39:26 +00:00
title : content [ " title " ] . stringValue ,
visibility : content [ " isListed " ] . boolValue ? . public : . private ,
2022-09-12 15:23:20 +00:00
editable : id . starts ( with : " IV " ) ,
2021-12-17 16:39:26 +00:00
updated : content [ " updated " ] . doubleValue ,
videos : content [ " videos " ] . arrayValue . map { extractVideo ( from : $0 ) }
)
}
2022-07-01 22:14:04 +00:00
private func extractComment ( from content : JSON ) -> Comment ? {
let details = content . dictionaryValue
let author = details [ " author " ] ? . string ? ? " "
let channelId = details [ " authorId " ] ? . string ? ? UUID ( ) . uuidString
let authorAvatarURL = details [ " authorThumbnails " ] ? . arrayValue . last ? . dictionaryValue [ " url " ] ? . string ? ? " "
2024-04-01 13:08:08 +00:00
let htmlContent = details [ " contentHtml " ] ? . string ? ? " "
let decodedContent = decodeHtml ( htmlContent )
2022-07-01 22:14:04 +00:00
return Comment (
id : UUID ( ) . uuidString ,
author : author ,
authorAvatarURL : authorAvatarURL ,
time : details [ " publishedText " ] ? . string ? ? " " ,
pinned : false ,
hearted : false ,
likeCount : details [ " likeCount " ] ? . int ? ? 0 ,
2024-04-01 13:08:08 +00:00
text : decodedContent ,
2022-07-01 22:14:04 +00:00
repliesPage : details [ " replies " ] ? . dictionaryValue [ " continuation " ] ? . string ,
2022-12-13 23:07:32 +00:00
channel : Channel ( app : . invidious , id : channelId , name : author )
2022-07-01 22:14:04 +00:00
)
}
2022-07-05 17:20:25 +00:00
2024-04-01 13:08:08 +00:00
private func decodeHtml ( _ htmlEncodedString : String ) -> String {
if let data = htmlEncodedString . data ( using : . utf8 ) {
let options : [ NSAttributedString . DocumentReadingOptionKey : Any ] = [
. documentType : NSAttributedString . DocumentType . html ,
. characterEncoding : String . Encoding . utf8 . rawValue
]
if let attributedString = try ? NSAttributedString ( data : data , options : options , documentAttributes : nil ) {
return attributedString . string
}
}
return htmlEncodedString
}
2022-07-05 17:20:25 +00:00
private func extractCaptions ( from content : JSON ) -> [ Captions ] {
content [ " captions " ] . arrayValue . compactMap { details in
2022-12-09 00:15:19 +00:00
guard let url = URL ( string : details [ " url " ] . stringValue , relativeTo : account . url ) else { return nil }
2022-07-05 17:20:25 +00:00
return Captions (
label : details [ " label " ] . stringValue ,
code : details [ " language_code " ] . stringValue ,
url : url
)
}
}
2023-02-28 20:03:02 +00:00
private func extractContentItems ( from json : JSON ) -> [ ContentItem ] {
json . arrayValue . compactMap { extractContentItem ( from : $0 ) }
}
private func extractContentItem ( from json : JSON ) -> ContentItem ? {
let type = json . dictionaryValue [ " type " ] ? . string
if type = = " channel " {
return ContentItem ( channel : extractChannel ( from : json ) )
2023-06-17 12:09:51 +00:00
}
if type = = " playlist " {
2023-02-28 20:03:02 +00:00
return ContentItem ( playlist : extractChannelPlaylist ( from : json ) )
2023-06-17 12:09:51 +00:00
}
if type = = " video " {
2023-02-28 20:03:02 +00:00
return ContentItem ( video : extractVideo ( from : json ) )
}
return nil
}
}
extension Channel . ContentType {
var invidiousID : String {
switch self {
case . livestreams :
return " streams "
default :
return rawValue
}
}
2021-06-28 10:43:07 +00:00
}