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 !
2021-06-28 10:43:07 +00:00
2021-10-16 22:48:58 +00:00
@ Published var validInstance = true
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
2021-12-19 16:56:47 +00:00
validInstance = account . anonymous
2021-09-25 08:18:22 +00:00
configure ( )
2021-12-19 16:56:47 +00:00
if ! account . anonymous {
validate ( )
}
2021-09-25 08:18:22 +00:00
}
func validate ( ) {
validateInstance ( )
validateSID ( )
}
func validateInstance ( ) {
guard ! validInstance else {
return
}
2021-10-20 22:21:50 +00:00
home ?
2021-09-25 08:18:22 +00:00
. load ( )
. onSuccess { _ in
self . validInstance = true
}
. onFailure { _ in
self . validInstance = false
}
}
func validateSID ( ) {
2022-08-25 23:36:46 +00:00
guard signedIn , ! ( account . token ? . isEmpty ? ? true ) else {
2021-09-25 08:18:22 +00:00
return
}
2021-10-20 22:21:50 +00:00
feed ?
2021-09-25 08:18:22 +00:00
. load ( )
2022-08-25 23:36:46 +00:00
. onFailure { _ in
self . updateToken ( force : true )
2021-09-25 08:18:22 +00:00
}
}
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 ) )
2021-10-21 23:29:10 +00:00
} else if type = = " playlist " {
2022-01-04 23:18:01 +00:00
return ContentItem ( playlist : self . extractChannelPlaylist ( from : json ) )
2022-03-28 19:26:38 +00:00
} else if type = = " video " {
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 " ] {
return suggestions . arrayValue . map ( String . init )
}
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
2021-07-08 15:14:54 +00:00
// h a c k y , t o v e r i f y i f p o s s i b l e t o g e t i t i n e a s i e r w a y
Playlist ( JSON ( parseJSON : String ( data : content . content , encoding : . utf8 ) ! ) )
}
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
}
2021-09-25 08:18:22 +00:00
configureTransformer ( pathPattern ( " channels/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> Channel in
2021-12-17 16:39:26 +00:00
self . extractChannel ( 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
2021-12-17 16:39:26 +00:00
content . json . arrayValue . map ( self . extractVideo )
2021-09-18 20:36:42 +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
updateToken ( )
}
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 " )
2021-11-04 22:01:27 +00:00
. withParam ( " type " , category ? . name )
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
}
2021-10-20 22:21:50 +00:00
var feed : Resource ? {
2022-05-20 19:53:17 +00:00
resource ( baseURL : account . url , path : " \( Self . basePath ) /auth/feed " )
2021-09-25 08:18:22 +00:00
}
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
}
2021-08-31 21:17:50 +00:00
func channel ( _ id : String ) -> Resource {
2021-09-25 08:18:22 +00:00
resource ( baseURL : account . url , path : basePathAppending ( " channels/ \( id ) " ) )
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 ? {
2021-10-27 21:11:38 +00:00
guard let instanceURLComponents = URLComponents ( string : instance . apiURL ) ,
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
var publishedAt : Date ?
if let publishedInterval = json [ " published " ] . double {
publishedAt = Date ( timeIntervalSince1970 : publishedInterval )
}
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
2021-10-21 23:29:10 +00:00
return Video (
id : id ,
videoID : videoID ,
title : json [ " title " ] . stringValue ,
author : json [ " author " ] . stringValue ,
length : json [ " lengthSeconds " ] . doubleValue ,
published : json [ " publishedText " ] . stringValue ,
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 ,
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 ) ,
2022-07-05 17:20:25 +00:00
chapters : extractChapters ( from : description ) ,
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 ) ] ) = = " // " ,
let accountUrlComponents = URLComponents ( string : account . url )
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
return Channel (
id : json [ " authorId " ] . stringValue ,
name : json [ " author " ] . stringValue ,
thumbnailURL : URL ( string : thumbnailURL ) ,
subscriptionsCount : json [ " subCount " ] . int ,
subscriptionsText : json [ " subCountText " ] . string ,
2021-12-17 16:39:26 +00:00
videos : json . dictionaryValue [ " latestVideos " ] ? . arrayValue . map ( extractVideo ) ? ? [ ]
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 ) ,
2021-12-17 16:39:26 +00:00
videos : details [ " videos " ] ? . arrayValue . compactMap ( extractVideo ) ? ? [ ]
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 ,
let accountUrlComponents = URLComponents ( string : account . url )
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
}
}
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 ,
videoFormat : videoStream [ " type " ] . 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 ? ? " "
return Comment (
id : UUID ( ) . uuidString ,
author : author ,
authorAvatarURL : authorAvatarURL ,
time : details [ " publishedText " ] ? . string ? ? " " ,
pinned : false ,
hearted : false ,
likeCount : details [ " likeCount " ] ? . int ? ? 0 ,
text : details [ " content " ] ? . string ? ? " " ,
repliesPage : details [ " replies " ] ? . dictionaryValue [ " continuation " ] ? . string ,
channel : Channel ( id : channelId , name : author )
)
}
2022-07-05 17:20:25 +00:00
private func extractCaptions ( from content : JSON ) -> [ Captions ] {
content [ " captions " ] . arrayValue . compactMap { details in
2022-07-21 22:44:21 +00:00
let baseURL = account . url
guard let url = URL ( string : baseURL + details [ " url " ] . stringValue ) else { return nil }
2022-07-05 17:20:25 +00:00
return Captions (
label : details [ " label " ] . stringValue ,
code : details [ " language_code " ] . stringValue ,
url : url
)
}
}
2021-06-28 10:43:07 +00:00
}