2022-08-26 05:06:46 +05:30
import Alamofire
2021-10-22 04:59:10 +05:30
import AVKit
2021-07-08 04:09:18 +05:30
import Defaults
2021-06-28 16:13:07 +05:30
import Foundation
import Siesta
import SwiftyJSON
2021-10-21 03:51:50 +05:30
final class InvidiousAPI : Service , ObservableObject , VideosAPI {
2021-09-25 13:48:22 +05:30
static let basePath = " /api/v1 "
2021-06-28 16:13:07 +05:30
2021-10-21 03:51:50 +05:30
@ Published var account : Account !
2022-08-26 05:06:46 +05:30
2022-12-09 05:45:19 +05:30
static func withAnonymousAccountForInstanceURL ( _ url : URL ) -> InvidiousAPI {
. init ( account : Instance ( app : . invidious , apiURLString : url . absoluteString ) . anonymousAccount )
}
2022-08-26 05:06:46 +05:30
var signedIn : Bool {
2022-09-28 19:57:01 +05:30
guard let account else { return false }
2022-08-26 05:06:46 +05:30
return ! account . anonymous && ! ( account . token ? . isEmpty ? ? true )
}
2021-06-28 16:13:07 +05:30
2021-10-21 03:51:50 +05:30
init ( account : Account ? = nil ) {
2021-10-17 04:18:58 +05:30
super . init ( )
guard ! account . isNil else {
2021-10-18 03:19:56 +05:30
self . account = . init ( name : " Empty " )
2021-10-17 04:18:58 +05:30
return
}
setAccount ( account ! )
}
2021-10-21 03:51:50 +05:30
func setAccount ( _ account : Account ) {
2021-09-25 13:48:22 +05:30
self . account = account
configure ( )
}
func configure ( ) {
2022-08-26 05:06:46 +05:30
invalidateConfiguration ( )
2021-06-28 16:13:07 +05:30
configure {
2022-08-26 05:06:46 +05:30
if let cookie = self . cookieHeader {
$0 . headers [ " Cookie " ] = cookie
2021-10-17 04:18:58 +05:30
}
2021-06-28 16:13:07 +05:30
$0 . pipeline [ . parsing ] . add ( SwiftyJSONTransformer , contentTypes : [ " */json " ] )
}
configure ( " ** " , requestMethods : [ . post ] ) {
$0 . pipeline [ . parsing ] . removeTransformers ( )
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " popular " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-12-17 22:09:26 +05:30
content . json . arrayValue . map ( self . extractVideo )
2021-06-28 16:13:07 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " trending " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-12-17 22:09:26 +05:30
content . json . arrayValue . map ( self . extractVideo )
2021-06-28 16:13:07 +05:30
}
2022-01-05 04:48:01 +05:30
configureTransformer ( pathPattern ( " search " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> SearchPage in
2022-03-29 00:56:38 +05:30
let results = content . json . arrayValue . compactMap { json -> ContentItem ? in
2022-06-18 16:54:23 +05:30
let type = json . dictionaryValue [ " type " ] ? . string
2021-10-22 04:59:10 +05:30
if type = = " channel " {
2022-01-05 04:48:01 +05:30
return ContentItem ( channel : self . extractChannel ( from : json ) )
2023-06-17 17:39:51 +05:30
}
if type = = " playlist " {
2022-01-05 04:48:01 +05:30
return ContentItem ( playlist : self . extractChannelPlaylist ( from : json ) )
2023-06-17 17:39:51 +05:30
}
if type = = " video " {
2022-03-29 00:56:38 +05:30
return ContentItem ( video : self . extractVideo ( from : json ) )
2021-10-22 04:59:10 +05:30
}
2022-03-29 00:56:38 +05:30
return nil
2021-10-22 04:59:10 +05:30
}
2022-01-05 04:48:01 +05:30
return SearchPage ( results : results , last : results . isEmpty )
2021-06-28 16:13:07 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " search/suggestions " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ String ] in
2021-09-14 02:11:16 +05:30
if let suggestions = content . json . dictionaryValue [ " suggestions " ] {
2023-05-27 04:31:17 +05:30
return suggestions . arrayValue . map { $0 . stringValue . replacingHTMLEntities }
2021-09-14 02:11:16 +05:30
}
return [ ]
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " auth/playlists " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Playlist ] in
2021-12-17 22:09:26 +05:30
content . json . arrayValue . map ( self . extractPlaylist )
2021-06-28 16:13:07 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " auth/playlists/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> Playlist in
2021-12-17 22:09:26 +05:30
self . extractPlaylist ( from : content . json )
2021-08-30 03:06:18 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " auth/playlists " ) , requestMethods : [ . post , . patch ] ) { ( content : Entity < Data > ) -> Playlist in
2022-12-11 22:34:39 +05:30
self . extractPlaylist ( from : JSON ( parseJSON : String ( data : content . content , encoding : . utf8 ) ! ) )
2021-07-08 20:44:54 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " auth/feed " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2021-06-28 16:13:07 +05:30
if let feedVideos = content . json . dictionaryValue [ " videos " ] {
2021-12-17 22:09:26 +05:30
return feedVideos . arrayValue . map ( self . extractVideo )
2021-06-28 16:13:07 +05:30
}
return [ ]
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " auth/subscriptions " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Channel ] in
2021-12-17 22:09:26 +05:30
content . json . arrayValue . map ( self . extractChannel )
2021-08-26 03:42:59 +05:30
}
2023-03-01 01:33:02 +05:30
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 16:13:07 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " channels/*/latest " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> [ Video ] in
2023-01-28 01:32:02 +05:30
content . json . dictionaryValue [ " videos " ] ? . arrayValue . map ( self . extractVideo ) ? ? [ ]
2021-09-19 02:06:42 +05:30
}
2024-04-01 18:38:08 +05:30
for type in [ " latest " , " playlists " , " streams " , " shorts " , " channels " , " videos " , " releases " , " podcasts " ] {
2023-03-01 01:33:02 +05:30
configureTransformer ( pathPattern ( " channels/*/ \( type ) " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPage in
self . extractChannelPage ( from : content . json )
}
2022-11-27 16:12:16 +05:30
}
2021-10-23 04:34:03 +05:30
configureTransformer ( pathPattern ( " playlists/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> ChannelPlaylist in
2021-12-17 22:09:26 +05:30
self . extractChannelPlaylist ( from : content . json )
2021-10-23 04:34:03 +05:30
}
2021-09-25 13:48:22 +05:30
configureTransformer ( pathPattern ( " videos/* " ) , requestMethods : [ . get ] ) { ( content : Entity < JSON > ) -> Video in
2021-12-17 22:09:26 +05:30
self . extractVideo ( from : content . json )
2021-06-28 16:13:07 +05:30
}
2022-07-02 03:44:04 +05:30
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-26 05:06:46 +05:30
2022-12-14 21:53:04 +05:30
if account . token . isNil || account . token ! . isEmpty {
updateToken ( )
} else {
FeedModel . shared . onAccountChange ( )
2022-12-21 04:21:04 +05:30
SubscribedChannelsModel . shared . onAccountChange ( )
PlaylistsModel . shared . onAccountChange ( )
2022-12-14 21:53:04 +05:30
}
2022-08-26 05:06:46 +05:30
}
func updateToken ( force : Bool = false ) {
let ( username , password ) = AccountsModel . getCredentials ( account )
guard ! account . anonymous ,
( account . token ? . isEmpty ? ? true ) || force
else {
return
}
2022-09-28 19:57:01 +05:30
guard let username ,
let password ,
2022-08-26 05:06:46 +05:30
! 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 22:19:47 +05:30
presentTokenUpdateFailedAlert ( nil , String ( format : " Could not extract SID from received cookies: %@ " . localized ( ) , cookies ) )
2022-08-26 05:06:46 +05:30
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-09-01 01:30:24 +05:30
self . objectWillChange . send ( )
2022-08-26 05:06:46 +05:30
} else {
2022-10-12 22:19:47 +05:30
presentTokenUpdateFailedAlert ( nil , String ( format : " Could not extract SID from received cookies: %@ " . localized ( ) , cookies ) )
2022-08-26 05:06:46 +05:30
}
2022-09-01 01:30:24 +05:30
self . configure ( )
2022-08-26 05:06:46 +05:30
}
}
var login : Resource {
resource ( baseURL : account . url , path : " login " )
2021-06-28 16:13:07 +05:30
}
2021-10-22 04:59:10 +05:30
private func pathPattern ( _ path : String ) -> String {
2022-05-21 01:23:17 +05:30
" ** \( Self . basePath ) / \( path ) "
2021-09-25 13:48:22 +05:30
}
2021-10-22 04:59:10 +05:30
private func basePathAppending ( _ path : String ) -> String {
2022-05-21 01:23:17 +05:30
" \( Self . basePath ) / \( path ) "
2021-09-25 13:48:22 +05:30
}
2022-08-26 05:06:46 +05:30
private var cookieHeader : String ? {
guard let token = account ? . token , ! token . isEmpty else { return nil }
return " SID= \( token ) "
2021-09-25 13:48:22 +05:30
}
2021-06-28 16:13:07 +05:30
2021-10-21 03:51:50 +05:30
var popular : Resource ? {
2022-05-21 01:23:17 +05:30
resource ( baseURL : account . url , path : " \( Self . basePath ) /popular " )
2021-06-28 16:13:07 +05:30
}
2021-10-21 03:51:50 +05:30
func trending ( country : Country , category : TrendingCategory ? ) -> Resource {
2022-05-21 01:23:17 +05:30
resource ( baseURL : account . url , path : " \( Self . basePath ) /trending " )
2023-05-27 03:54:53 +05:30
. withParam ( " type " , category ? . type )
2021-06-28 16:13:07 +05:30
. withParam ( " region " , country . rawValue )
}
2021-10-21 03:51:50 +05:30
var home : Resource ? {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : " /feed/subscriptions " )
2021-09-19 23:01:21 +05:30
}
2022-12-10 07:31:59 +05:30
func feed ( _ page : Int ? ) -> Resource ? {
2022-05-21 01:23:17 +05:30
resource ( baseURL : account . url , path : " \( Self . basePath ) /auth/feed " )
2022-12-10 07:31:59 +05:30
. withParam ( " page " , String ( page ? ? 1 ) )
}
var feed : Resource ? {
resource ( baseURL : account . url , path : basePathAppending ( " auth/feed " ) )
}
2021-10-21 03:51:50 +05:30
var subscriptions : Resource ? {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : basePathAppending ( " auth/subscriptions " ) )
2021-08-26 03:42:59 +05:30
}
2021-11-15 04:36:01 +05:30
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-26 03:42:59 +05:30
}
2023-03-01 01:33:02 +05:30
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 16:12:16 +05:30
}
2023-03-01 01:33:02 +05:30
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 16:13:07 +05:30
}
2022-06-25 04:18:57 +05:30
func channelByName ( _ : String ) -> Resource ? {
nil
}
2022-06-30 05:01:51 +05:30
func channelByUsername ( _ : String ) -> Resource ? {
nil
}
2021-09-19 02:06:42 +05:30
func channelVideos ( _ id : String ) -> Resource {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : basePathAppending ( " channels/ \( id ) /latest " ) )
2021-09-19 02:06:42 +05:30
}
2021-06-28 16:13:07 +05:30
func video ( _ id : String ) -> Resource {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : basePathAppending ( " videos/ \( id ) " ) )
2021-06-28 16:13:07 +05:30
}
2021-10-21 03:51:50 +05:30
var playlists : Resource ? {
2021-11-15 04:36:01 +05:30
if account . isNil || account . anonymous {
return nil
}
return resource ( baseURL : account . url , path : basePathAppending ( " auth/playlists " ) )
2021-06-28 16:13:07 +05:30
}
2021-10-21 03:51:50 +05:30
func playlist ( _ id : String ) -> Resource ? {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : basePathAppending ( " auth/playlists/ \( id ) " ) )
2021-07-08 22:48:36 +05:30
}
2021-10-21 03:51:50 +05:30
func playlistVideos ( _ id : String ) -> Resource ? {
playlist ( id ) ? . child ( " videos " )
2021-07-09 20:23:53 +05:30
}
2021-10-21 03:51:50 +05:30
func playlistVideo ( _ playlistID : String , _ videoID : String ) -> Resource ? {
playlist ( playlistID ) ? . child ( " videos " ) . child ( videoID )
2021-07-09 20:23:53 +05:30
}
2022-05-22 03:59:51 +05:30
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-23 04:34:03 +05:30
func channelPlaylist ( _ id : String ) -> Resource ? {
resource ( baseURL : account . url , path : basePathAppending ( " playlists/ \( id ) " ) )
}
2022-01-05 04:48:01 +05:30
func search ( _ query : SearchQuery , page : String ? ) -> Resource {
2021-09-25 13:48:22 +05:30
var resource = resource ( baseURL : account . url , path : basePathAppending ( " search " ) )
2021-07-08 04:09:18 +05:30
. withParam ( " q " , searchQuery ( query . query ) )
. withParam ( " sort_by " , query . sortBy . parameter )
2021-10-22 04:59:10 +05:30
. withParam ( " type " , " all " )
2021-07-08 04:09:18 +05:30
2021-09-26 23:10:25 +05:30
if let date = query . date , date != . any {
resource = resource . withParam ( " date " , date . rawValue )
2021-07-08 04:09:18 +05:30
}
2021-09-26 23:10:25 +05:30
if let duration = query . duration , duration != . any {
resource = resource . withParam ( " duration " , duration . rawValue )
2021-07-08 04:09:18 +05:30
}
2022-09-28 19:57:01 +05:30
if let page {
2022-01-05 04:48:01 +05:30
resource = resource . withParam ( " page " , page )
}
2021-07-08 04:09:18 +05:30
return resource
2021-06-28 16:13:07 +05:30
}
2021-09-14 02:11:16 +05:30
func searchSuggestions ( query : String ) -> Resource {
2021-09-25 13:48:22 +05:30
resource ( baseURL : account . url , path : basePathAppending ( " search/suggestions " ) )
2021-09-14 02:11:16 +05:30
. withParam ( " q " , query . lowercased ( ) )
}
2022-07-02 03:44:04 +05:30
func comments ( _ id : Video . ID , page : String ? ) -> Resource ? {
let resource = resource ( baseURL : account . url , path : basePathAppending ( " comments/ \( id ) " ) )
2022-09-28 19:57:01 +05:30
guard let page else { return resource }
2022-07-02 03:44:04 +05:30
return resource . withParam ( " continuation " , page )
}
2021-12-05 01:05:41 +05:30
2021-06-28 16:13:07 +05:30
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 20:44:54 +05:30
return searchQuery
2021-06-28 16:13:07 +05:30
}
2021-10-22 04:59:10 +05:30
2021-10-22 20:30:09 +05:30
static func proxiedAsset ( instance : Instance , asset : AVURLAsset ) -> AVURLAsset ? {
2022-12-09 05:45:19 +05:30
guard let instanceURLComponents = URLComponents ( url : instance . apiURL , resolvingAgainstBaseURL : false ) ,
2021-10-22 20:30:09 +05:30
var urlComponents = URLComponents ( url : asset . url , resolvingAgainstBaseURL : false ) else { return nil }
2021-10-22 04:59:10 +05:30
urlComponents . scheme = instanceURLComponents . scheme
urlComponents . host = instanceURLComponents . host
2021-10-22 20:30:09 +05:30
guard let url = urlComponents . url else {
return nil
}
return AVURLAsset ( url : url )
2021-10-22 04:59:10 +05:30
}
2021-12-17 22:09:26 +05:30
func extractVideo ( from json : JSON ) -> Video {
2021-10-22 04:59:10 +05:30
let indexID : String ?
var id : Video . ID
2022-12-14 02:25:03 +05:30
var published = json [ " publishedText " ] . stringValue
2021-10-22 04:59:10 +05:30
var publishedAt : Date ?
if let publishedInterval = json [ " published " ] . double {
publishedAt = Date ( timeIntervalSince1970 : publishedInterval )
2022-12-14 02:25:03 +05:30
published = " "
2021-10-22 04:59:10 +05:30
}
let videoID = json [ " videoId " ] . stringValue
if let index = json [ " indexId " ] . string {
indexID = index
id = videoID + index
} else {
indexID = nil
id = videoID
}
2022-06-18 18:09:49 +05:30
let description = json [ " description " ] . stringValue
2023-02-25 21:12:18 +05:30
let length = json [ " lengthSeconds " ] . doubleValue
2022-06-18 18:09:49 +05:30
2021-10-22 04:59:10 +05:30
return Video (
2022-12-09 05:45:19 +05:30
instanceID : account . instanceID ,
app : . invidious ,
instanceURL : account . instance . apiURL ,
2022-12-18 18:09:39 +05:30
id : id ,
2021-10-22 04:59:10 +05:30
videoID : videoID ,
title : json [ " title " ] . stringValue ,
author : json [ " author " ] . stringValue ,
2023-02-25 21:12:18 +05:30
length : length ,
2022-12-14 02:25:03 +05:30
published : published ,
2021-10-22 04:59:10 +05:30
views : json [ " viewCount " ] . intValue ,
2022-06-18 18:09:49 +05:30
description : description ,
2021-10-22 04:59:10 +05:30
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 21:12:18 +05:30
short : length <= Video . shortLength ,
2021-10-22 04:59:10 +05:30
publishedAt : publishedAt ,
likes : json [ " likeCount " ] . int ,
dislikes : json [ " dislikeCount " ] . int ,
2022-06-18 16:54:23 +05:30
keywords : json [ " keywords " ] . arrayValue . compactMap { $0 . string } ,
2021-11-03 04:32:02 +05:30
streams : extractStreams ( from : json ) ,
2022-06-18 18:09:49 +05:30
related : extractRelated ( from : json ) ,
2022-07-05 22:50:25 +05:30
chapters : extractChapters ( from : description ) ,
captions : extractCaptions ( from : json )
2021-10-22 04:59:10 +05:30
)
}
2021-12-17 22:09:26 +05:30
func extractChannel ( from json : JSON ) -> Channel {
2022-06-18 16:54:23 +05:30
var thumbnailURL = json [ " authorThumbnails " ] . arrayValue . last ? . dictionaryValue [ " url " ] ? . string ? ? " "
2021-12-17 22:09:26 +05:30
2022-06-16 03:18:38 +05:30
// 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 22:09:26 +05:30
if thumbnailURL . count > 2 ,
2022-06-16 03:18:38 +05:30
String ( thumbnailURL [ . . < thumbnailURL . index ( thumbnailURL . startIndex , offsetBy : 2 ) ] ) = = " // " ,
2022-12-09 05:45:19 +05:30
let accountUrlComponents = URLComponents ( string : account . urlString )
2021-12-17 22:09:26 +05:30
{
2022-06-16 03:18:38 +05:30
thumbnailURL = " \( accountUrlComponents . scheme ? ? " https " ) : \( thumbnailURL ) "
2021-12-17 22:09:26 +05:30
}
2021-10-22 04:59:10 +05:30
2023-03-01 01:33:02 +05:30
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-22 04:59:10 +05:30
return Channel (
2022-12-14 04:37:32 +05:30
app : . invidious ,
2021-10-22 04:59:10 +05:30
id : json [ " authorId " ] . stringValue ,
name : json [ " author " ] . stringValue ,
2022-11-27 16:12:16 +05:30
bannerURL : json [ " authorBanners " ] . arrayValue . first ? . dictionaryValue [ " url " ] ? . url ,
2021-10-22 04:59:10 +05:30
thumbnailURL : URL ( string : thumbnailURL ) ,
2022-11-27 16:12:16 +05:30
description : json [ " description " ] . stringValue ,
2021-10-22 04:59:10 +05:30
subscriptionsCount : json [ " subCount " ] . int ,
subscriptionsText : json [ " subCountText " ] . string ,
2022-11-27 16:12:16 +05:30
totalViews : json [ " totalViews " ] . int ,
2023-03-01 01:33:02 +05:30
videos : json . dictionaryValue [ " latestVideos " ] ? . arrayValue . map ( extractVideo ) ? ? [ ] ,
tabs : tabs
2021-10-22 04:59:10 +05:30
)
}
2021-12-17 22:09:26 +05:30
func extractChannelPlaylist ( from json : JSON ) -> ChannelPlaylist {
2021-10-23 04:34:03 +05:30
let details = json . dictionaryValue
return ChannelPlaylist (
2022-06-30 13:41:11 +05:30
id : details [ " playlistId " ] ? . string ? ? details [ " mixId " ] ? . string ? ? UUID ( ) . uuidString ,
title : details [ " title " ] ? . stringValue ? ? " " ,
2021-10-23 04:34:03 +05:30
thumbnailURL : details [ " playlistThumbnail " ] ? . url ,
channel : extractChannel ( from : json ) ,
2022-11-27 16:12:16 +05:30
videos : details [ " videos " ] ? . arrayValue . compactMap ( extractVideo ) ? ? [ ] ,
videosCount : details [ " videoCount " ] ? . int
2021-10-23 04:34:03 +05:30
)
}
2021-12-17 22:09:26 +05:30
private func extractThumbnails ( from details : JSON ) -> [ Thumbnail ] {
2022-06-15 13:35:52 +05:30
details [ " videoThumbnails " ] . arrayValue . compactMap { json in
guard let url = json [ " url " ] . url ,
var components = URLComponents ( url : url , resolvingAgainstBaseURL : false ) ,
2022-06-16 03:18:38 +05:30
let quality = json [ " quality " ] . string ,
2022-12-09 05:45:19 +05:30
let accountUrlComponents = URLComponents ( string : account . urlString )
2022-06-15 13:35:52 +05:30
else {
return nil
}
2022-06-16 03:18:38 +05:30
// 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 13:35:52 +05:30
guard let thumbnailUrl = components . url else {
return nil
}
return Thumbnail ( url : thumbnailUrl , quality : . init ( rawValue : quality ) ! )
2021-10-22 04:59:10 +05:30
}
}
2023-03-01 01:33:02 +05:30
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 22:09:26 +05:30
private func extractStreams ( from json : JSON ) -> [ Stream ] {
2022-07-22 04:14:21 +05:30
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-03 04:32:02 +05:30
}
2021-12-17 22:09:26 +05:30
private func extractFormatStreams ( from streams : [ JSON ] ) -> [ Stream ] {
2022-06-18 16:54:23 +05:30
streams . compactMap { stream in
guard let streamURL = stream [ " url " ] . url else {
return nil
}
return SingleAssetStream (
2022-08-17 02:46:35 +05:30
instance : account . instance ,
2022-06-18 16:54:23 +05:30
avAsset : AVURLAsset ( url : streamURL ) ,
resolution : Stream . Resolution . from ( resolution : stream [ " resolution " ] . string ? ? " " ) ,
2021-10-22 04:59:10 +05:30
kind : . stream ,
2022-06-18 16:54:23 +05:30
encoding : stream [ " encoding " ] . string ? ? " "
2021-10-22 04:59:10 +05:30
)
}
}
2021-12-17 22:09:26 +05:30
private func extractAdaptiveFormats ( from streams : [ JSON ] ) -> [ Stream ] {
2022-07-11 04:12:47 +05:30
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 16:54:23 +05:30
return . init ( )
2021-10-22 04:59:10 +05:30
}
2022-06-18 16:54:23 +05:30
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-22 04:59:10 +05:30
2022-06-18 16:54:23 +05:30
return Stream (
2022-08-17 02:46:35 +05:30
instance : account . instance ,
2022-06-18 16:54:23 +05:30
audioAsset : AVURLAsset ( url : audioAssetURL ) ,
videoAsset : AVURLAsset ( url : videoAssetURL ) ,
resolution : Stream . Resolution . from ( resolution : videoStream [ " resolution " ] . stringValue ) ,
2021-10-22 04:59:10 +05:30
kind : . adaptive ,
2022-06-18 16:54:23 +05:30
encoding : videoStream [ " encoding " ] . string ,
2024-05-13 11:24:24 +05:30
videoFormat : videoStream [ " type " ] . string ,
bitrate : videoStream [ " bitrate " ] . int
2021-10-22 04:59:10 +05:30
)
}
}
2021-11-03 04:32:02 +05:30
2022-07-22 04:14:21 +05:30
private func extractHLSStreams ( from content : JSON ) -> [ Stream ] {
if let hlsURL = content . dictionaryValue [ " hlsUrl " ] ? . url {
2022-08-17 02:46:35 +05:30
return [ Stream ( instance : account . instance , hlsURL : hlsURL ) ]
2022-07-22 04:14:21 +05:30
}
return [ ]
}
2021-12-17 22:09:26 +05:30
private func extractRelated ( from content : JSON ) -> [ Video ] {
2021-11-03 04:32:02 +05:30
content
. dictionaryValue [ " recommendedVideos " ] ?
. arrayValue
. compactMap ( extractVideo ( from : ) ) ? ? [ ]
}
2021-12-17 22:09:26 +05:30
private func extractPlaylist ( from content : JSON ) -> Playlist {
2022-09-12 20:53:20 +05:30
let id = content [ " playlistId " ] . stringValue
return Playlist (
id : id ,
2021-12-17 22:09:26 +05:30
title : content [ " title " ] . stringValue ,
visibility : content [ " isListed " ] . boolValue ? . public : . private ,
2022-09-12 20:53:20 +05:30
editable : id . starts ( with : " IV " ) ,
2021-12-17 22:09:26 +05:30
updated : content [ " updated " ] . doubleValue ,
videos : content [ " videos " ] . arrayValue . map { extractVideo ( from : $0 ) }
)
}
2022-07-02 03:44:04 +05:30
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 18:38:08 +05:30
let htmlContent = details [ " contentHtml " ] ? . string ? ? " "
let decodedContent = decodeHtml ( htmlContent )
2022-07-02 03:44:04 +05:30
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 18:38:08 +05:30
text : decodedContent ,
2022-07-02 03:44:04 +05:30
repliesPage : details [ " replies " ] ? . dictionaryValue [ " continuation " ] ? . string ,
2022-12-14 04:37:32 +05:30
channel : Channel ( app : . invidious , id : channelId , name : author )
2022-07-02 03:44:04 +05:30
)
}
2022-07-05 22:50:25 +05:30
2024-04-01 18:38:08 +05:30
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 22:50:25 +05:30
private func extractCaptions ( from content : JSON ) -> [ Captions ] {
content [ " captions " ] . arrayValue . compactMap { details in
2022-12-09 05:45:19 +05:30
guard let url = URL ( string : details [ " url " ] . stringValue , relativeTo : account . url ) else { return nil }
2022-07-05 22:50:25 +05:30
return Captions (
label : details [ " label " ] . stringValue ,
code : details [ " language_code " ] . stringValue ,
url : url
)
}
}
2023-03-01 01:33:02 +05:30
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 17:39:51 +05:30
}
if type = = " playlist " {
2023-03-01 01:33:02 +05:30
return ContentItem ( playlist : extractChannelPlaylist ( from : json ) )
2023-06-17 17:39:51 +05:30
}
if type = = " video " {
2023-03-01 01:33:02 +05:30
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 16:13:07 +05:30
}