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-09-25 13:48:22 +05:30
|
|
|
final class InvidiousAPI: Service, ObservableObject {
|
|
|
|
static let basePath = "/api/v1"
|
2021-06-28 16:13:07 +05:30
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
@Published var account: Instance.Account!
|
2021-06-28 16:13:07 +05:30
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
@Published var validInstance = true
|
|
|
|
@Published var signedIn = true
|
2021-06-28 16:13:07 +05:30
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
init() {
|
|
|
|
super.init()
|
2021-06-28 16:13:07 +05:30
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
#if os(tvOS)
|
|
|
|
// TODO: remove
|
|
|
|
setAccount(.init(id: UUID(), name: "", url: "https://invidious.home.arekf.net", sid: "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8="))
|
|
|
|
#endif
|
2021-06-28 16:13:07 +05:30
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
func setAccount(_ account: Instance.Account) {
|
|
|
|
self.account = account
|
|
|
|
|
|
|
|
validInstance = false
|
|
|
|
signedIn = false
|
|
|
|
|
|
|
|
configure()
|
|
|
|
validate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func validate() {
|
|
|
|
validateInstance()
|
|
|
|
validateSID()
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateInstance() {
|
|
|
|
guard !validInstance else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
home
|
|
|
|
.load()
|
|
|
|
.onSuccess { _ in
|
|
|
|
self.validInstance = true
|
|
|
|
}
|
|
|
|
.onFailure { _ in
|
|
|
|
self.validInstance = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateSID() {
|
|
|
|
guard !signedIn else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
feed
|
|
|
|
.load()
|
|
|
|
.onSuccess { _ in
|
|
|
|
self.signedIn = true
|
|
|
|
}
|
|
|
|
.onFailure { _ in
|
|
|
|
self.signedIn = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static func proxyURLForAsset(_ url: String) -> URL? {
|
|
|
|
URL(string: url)
|
|
|
|
// TODO: Switching instances, move up to player
|
|
|
|
// guard let instanceURLComponents = URLComponents(string: InvidiousAPI.instance),
|
|
|
|
// var urlComponents = URLComponents(string: url) else { return nil }
|
|
|
|
//
|
|
|
|
// urlComponents.scheme = instanceURLComponents.scheme
|
|
|
|
// urlComponents.host = instanceURLComponents.host
|
|
|
|
//
|
|
|
|
// return urlComponents.url
|
|
|
|
}
|
|
|
|
|
|
|
|
func configure() {
|
2021-07-08 04:09:18 +05:30
|
|
|
SiestaLog.Category.enabled = .common
|
|
|
|
|
|
|
|
let SwiftyJSONTransformer =
|
|
|
|
ResponseContentTransformer(transformErrors: true) { JSON($0.content as AnyObject) }
|
2021-06-28 16:13:07 +05:30
|
|
|
|
|
|
|
configure {
|
2021-09-25 13:48:22 +05:30
|
|
|
$0.headers["Cookie"] = self.cookieHeader
|
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-06-28 16:13:07 +05:30
|
|
|
content.json.arrayValue.map(Video.init)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
2021-06-28 16:13:07 +05:30
|
|
|
content.json.arrayValue.map(Video.init)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
2021-06-28 16:13:07 +05:30
|
|
|
content.json.arrayValue.map(Video.init)
|
|
|
|
}
|
|
|
|
|
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"] {
|
|
|
|
return suggestions.arrayValue.map(String.init)
|
|
|
|
}
|
|
|
|
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Playlist] in
|
2021-06-28 16:13:07 +05:30
|
|
|
content.json.arrayValue.map(Playlist.init)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
2021-08-30 03:06:18 +05:30
|
|
|
Playlist(content.json)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
|
2021-07-08 20:44:54 +05:30
|
|
|
// hacky, to verify if possible to get it in easier way
|
|
|
|
Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!))
|
|
|
|
}
|
|
|
|
|
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-08-26 03:42:59 +05:30
|
|
|
return feedVideos.arrayValue.map(Video.init)
|
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-08-26 03:42:59 +05:30
|
|
|
content.json.arrayValue.map(Channel.init)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
2021-09-01 02:47:50 +05:30
|
|
|
Channel(json: 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
|
2021-09-19 02:06:42 +05:30
|
|
|
content.json.arrayValue.map(Video.init)
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
2021-06-28 16:13:07 +05:30
|
|
|
Video(content.json)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-25 13:48:22 +05:30
|
|
|
fileprivate func pathPattern(_ path: String) -> String {
|
|
|
|
"**\(InvidiousAPI.basePath)/\(path)"
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func basePathAppending(_ path: String) -> String {
|
|
|
|
"\(InvidiousAPI.basePath)/\(path)"
|
|
|
|
}
|
|
|
|
|
|
|
|
private var cookieHeader: String {
|
|
|
|
"SID=\(account.sid)"
|
|
|
|
}
|
2021-06-28 16:13:07 +05:30
|
|
|
|
|
|
|
var popular: Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/popular")
|
2021-06-28 16:13:07 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func trending(category: TrendingCategory, country: Country) -> Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/trending")
|
2021-06-28 16:13:07 +05:30
|
|
|
.withParam("type", category.name)
|
|
|
|
.withParam("region", country.rawValue)
|
|
|
|
}
|
|
|
|
|
2021-09-19 23:01:21 +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
|
|
|
}
|
|
|
|
|
2021-08-26 03:42:59 +05:30
|
|
|
var feed: Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: "\(InvidiousAPI.basePath)/auth/feed")
|
|
|
|
}
|
|
|
|
|
|
|
|
var stats: Resource {
|
|
|
|
resource(baseURL: account.url, path: basePathAppending("stats"))
|
2021-06-28 16:13:07 +05:30
|
|
|
}
|
|
|
|
|
2021-08-26 03:42:59 +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
|
|
|
}
|
|
|
|
|
|
|
|
func channelSubscription(_ id: String) -> Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")).child(id)
|
2021-08-26 03:42:59 +05:30
|
|
|
}
|
|
|
|
|
2021-09-01 02:47:50 +05:30
|
|
|
func channel(_ id: String) -> Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
|
2021-06-28 16:13:07 +05:30
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
var playlists: Resource {
|
2021-09-25 13:48:22 +05:30
|
|
|
resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
2021-06-28 16:13:07 +05:30
|
|
|
}
|
|
|
|
|
2021-07-08 22:48:36 +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-07-09 20:23:53 +05:30
|
|
|
func playlistVideos(_ id: String) -> Resource {
|
|
|
|
playlist(id).child("videos")
|
|
|
|
}
|
|
|
|
|
|
|
|
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource {
|
|
|
|
playlist(playlistID).child("videos").child(videoID)
|
|
|
|
}
|
|
|
|
|
2021-07-08 04:09:18 +05:30
|
|
|
func search(_ query: SearchQuery) -> 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-07-08 20:44:54 +05:30
|
|
|
if let date = query.date?.rawValue {
|
|
|
|
resource = resource.withParam("date", date)
|
2021-07-08 04:09:18 +05:30
|
|
|
}
|
|
|
|
|
2021-07-08 20:44:54 +05:30
|
|
|
if let duration = query.duration?.rawValue {
|
|
|
|
resource = resource.withParam("duration", duration)
|
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())
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|