import AVFoundation import Foundation import Siesta protocol VideosAPI { var account: Account! { get } var signedIn: Bool { get } static func withAnonymousAccountForInstanceURL(_ url: URL) -> Self func channel(_ id: String, contentType: Channel.ContentType, data: String?) -> Resource func channelByName(_ name: String) -> Resource? func channelByUsername(_ username: String) -> Resource? func channelVideos(_ id: String) -> Resource func trending(country: Country, category: TrendingCategory?) -> Resource func search(_ query: SearchQuery, page: String?) -> Resource func searchSuggestions(query: String) -> Resource func video(_ id: Video.ID) -> Resource func feed(_ page: Int?) -> Resource? var subscriptions: Resource? { get } var home: Resource? { get } var popular: Resource? { get } var playlists: Resource? { get } func subscribe(_ channelID: String, onCompletion: @escaping () -> Void) func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) func playlist(_ id: String) -> Resource? func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource? func playlistVideos(_ id: String) -> Resource? func addVideoToPlaylist( _ videoID: String, _ playlistID: String, onFailure: @escaping (RequestError) -> Void, onSuccess: @escaping () -> Void ) func removeVideoFromPlaylist( _ index: String, _ playlistID: String, onFailure: @escaping (RequestError) -> Void, onSuccess: @escaping () -> Void ) func playlistForm( _ name: String, _ visibility: String, playlist: Playlist?, onFailure: @escaping (RequestError) -> Void, onSuccess: @escaping (Playlist?) -> Void ) func deletePlaylist( _ playlist: Playlist, onFailure: @escaping (RequestError) -> Void, onSuccess: @escaping () -> Void ) func channelPlaylist(_ id: String) -> Resource? func loadDetails( _ item: PlayerQueueItem, failureHandler: ((RequestError) -> Void)?, completionHandler: @escaping (PlayerQueueItem) -> Void ) func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL? func comments(_ id: Video.ID, page: String?) -> Resource? } extension VideosAPI { func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil) -> Resource { channel(id, contentType: contentType, data: data) } func loadDetails( _ item: PlayerQueueItem, failureHandler: ((RequestError) -> Void)? = nil, completionHandler: @escaping (PlayerQueueItem) -> Void = { _ in } ) { guard (item.video?.streams ?? []).isEmpty else { completionHandler(item) return } if let video = item.video, video.isLocal { completionHandler(item) return } video(item.videoID).load() .onSuccess { response in guard let video: Video = response.typedContent() else { return } var newItem = item newItem.id = UUID() newItem.video = video completionHandler(newItem) } .onFailure { failureHandler?($0) } } func shareURL(_ item: ContentItem, frontendHost: String? = nil, time: CMTime? = nil) -> URL? { guard let frontendHost = frontendHost ?? account?.instance?.frontendHost, var urlComponents = account?.instance?.urlComponents else { return nil } urlComponents.host = frontendHost var queryItems = [URLQueryItem]() switch item.contentType { case .video: urlComponents.path = "/watch" queryItems.append(.init(name: "v", value: item.video.videoID)) case .channel: urlComponents.path = "/channel/\(item.channel.id)" case .playlist: urlComponents.path = "/playlist" queryItems.append(.init(name: "list", value: item.playlist.id)) default: return nil } if !time.isNil, time!.seconds.isFinite { queryItems.append(.init(name: "t", value: "\(Int(time!.seconds))s")) } if !queryItems.isEmpty { urlComponents.queryItems = queryItems } return urlComponents.url } func extractChapters(from description: String) -> [Chapter] { guard let chaptersRegularExpression = try? NSRegularExpression( pattern: "(?(?:[0-9]+:){1,}(?:[0-9]+))(?:\\s)+(?:- ?)?(?.*)", options: .caseInsensitive ) else { return [] } let chapterLines = chaptersRegularExpression.matches( in: description, range: NSRange(description.startIndex..., in: description) ) return chapterLines.compactMap { line in let titleRange = line.range(withName: "title") let startRange = line.range(withName: "start") guard let titleSubstringRange = Range(titleRange, in: description), let startSubstringRange = Range(startRange, in: description) else { return nil } let titleCapture = String(description[titleSubstringRange]) let startCapture = String(description[startSubstringRange]) let startComponents = startCapture.components(separatedBy: ":") guard startComponents.count <= 3 else { return nil } var hours: Double? var minutes: Double? var seconds: Double? if startComponents.count == 3 { hours = Double(startComponents[0]) minutes = Double(startComponents[1]) seconds = Double(startComponents[2]) } else if startComponents.count == 2 { minutes = Double(startComponents[0]) seconds = Double(startComponents[1]) } guard var startSeconds = seconds else { return nil } if let minutes { startSeconds += 60 * minutes } if let hours { startSeconds += 60 * 60 * hours } return .init(title: titleCapture, start: startSeconds) } } }