2021-11-02 19:40:49 +00:00
|
|
|
import AVFoundation
|
2021-10-20 22:21:50 +00:00
|
|
|
import Foundation
|
|
|
|
import Siesta
|
|
|
|
|
|
|
|
protocol VideosAPI {
|
2021-10-26 22:59:59 +00:00
|
|
|
var account: Account! { get }
|
2021-10-20 22:21:50 +00:00
|
|
|
var signedIn: Bool { get }
|
|
|
|
|
2022-12-09 00:15:19 +00:00
|
|
|
static func withAnonymousAccountForInstanceURL(_ url: URL) -> Self
|
|
|
|
|
2023-02-28 20:03:02 +00:00
|
|
|
func channel(_ id: String, contentType: Channel.ContentType, data: String?, page: String?) -> Resource
|
2022-06-24 22:48:57 +00:00
|
|
|
func channelByName(_ name: String) -> Resource?
|
2022-06-29 23:31:51 +00:00
|
|
|
func channelByUsername(_ username: String) -> Resource?
|
2021-11-01 21:56:18 +00:00
|
|
|
func channelVideos(_ id: String) -> Resource
|
2021-10-20 22:21:50 +00:00
|
|
|
func trending(country: Country, category: TrendingCategory?) -> Resource
|
2022-01-04 23:18:01 +00:00
|
|
|
func search(_ query: SearchQuery, page: String?) -> Resource
|
2021-10-20 22:21:50 +00:00
|
|
|
func searchSuggestions(query: String) -> Resource
|
|
|
|
|
|
|
|
func video(_ id: Video.ID) -> Resource
|
|
|
|
|
2022-12-10 02:01:59 +00:00
|
|
|
func feed(_ page: Int?) -> Resource?
|
2021-10-20 22:21:50 +00:00
|
|
|
var subscriptions: Resource? { get }
|
|
|
|
var home: Resource? { get }
|
|
|
|
var popular: Resource? { get }
|
|
|
|
var playlists: Resource? { get }
|
|
|
|
|
2021-11-14 23:06:01 +00:00
|
|
|
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void)
|
|
|
|
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void)
|
2021-10-20 22:21:50 +00:00
|
|
|
|
2021-11-01 21:56:18 +00:00
|
|
|
func playlist(_ id: String) -> Resource?
|
2021-10-20 22:21:50 +00:00
|
|
|
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
|
|
|
func playlistVideos(_ id: String) -> Resource?
|
2021-10-22 23:04:03 +00:00
|
|
|
|
2022-05-21 22:29:51 +00:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2021-10-22 23:04:03 +00:00
|
|
|
func channelPlaylist(_ id: String) -> Resource?
|
2021-10-24 18:01:08 +00:00
|
|
|
|
2022-06-29 22:44:32 +00:00
|
|
|
func loadDetails(
|
|
|
|
_ item: PlayerQueueItem,
|
|
|
|
failureHandler: ((RequestError) -> Void)?,
|
|
|
|
completionHandler: @escaping (PlayerQueueItem) -> Void
|
|
|
|
)
|
2021-11-02 19:40:49 +00:00
|
|
|
func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL?
|
2021-12-04 19:35:41 +00:00
|
|
|
|
|
|
|
func comments(_ id: Video.ID, page: String?) -> Resource?
|
2021-10-24 18:01:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extension VideosAPI {
|
2023-02-28 20:03:02 +00:00
|
|
|
func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil, page: String? = nil) -> Resource {
|
|
|
|
channel(id, contentType: contentType, data: data, page: page)
|
2022-11-27 10:42:16 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 22:44:32 +00:00
|
|
|
func loadDetails(
|
|
|
|
_ item: PlayerQueueItem,
|
|
|
|
failureHandler: ((RequestError) -> Void)? = nil,
|
|
|
|
completionHandler: @escaping (PlayerQueueItem) -> Void = { _ in }
|
|
|
|
) {
|
2021-10-24 18:01:08 +00:00
|
|
|
guard (item.video?.streams ?? []).isEmpty else {
|
|
|
|
completionHandler(item)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-10 17:11:28 +00:00
|
|
|
if let video = item.video, video.isLocal {
|
|
|
|
completionHandler(item)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-05 17:26:03 +00:00
|
|
|
video(item.videoID).load()
|
|
|
|
.onSuccess { response in
|
|
|
|
guard let video: Video = response.typedContent() else {
|
|
|
|
return
|
|
|
|
}
|
2021-10-24 18:01:08 +00:00
|
|
|
|
2022-12-12 23:38:26 +00:00
|
|
|
VideosCacheModel.shared.storeVideo(video)
|
|
|
|
|
2022-07-05 17:26:03 +00:00
|
|
|
var newItem = item
|
2022-11-10 17:11:28 +00:00
|
|
|
newItem.id = UUID()
|
2022-07-05 17:26:03 +00:00
|
|
|
newItem.video = video
|
2021-10-24 18:01:08 +00:00
|
|
|
|
2022-07-05 17:26:03 +00:00
|
|
|
completionHandler(newItem)
|
|
|
|
}
|
|
|
|
.onFailure { failureHandler?($0) }
|
2021-10-24 18:01:08 +00:00
|
|
|
}
|
2021-10-26 22:59:59 +00:00
|
|
|
|
2021-11-02 19:40:49 +00:00
|
|
|
func shareURL(_ item: ContentItem, frontendHost: String? = nil, time: CMTime? = nil) -> URL? {
|
2021-12-17 20:01:05 +00:00
|
|
|
guard let frontendHost = frontendHost ?? account?.instance?.frontendHost,
|
|
|
|
var urlComponents = account?.instance?.urlComponents
|
|
|
|
else {
|
2021-10-28 17:14:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
urlComponents.host = frontendHost
|
2021-10-27 21:11:38 +00:00
|
|
|
|
2024-03-19 04:58:16 +00:00
|
|
|
if frontendHost.contains("youtube.com") {
|
|
|
|
urlComponents.port = nil
|
|
|
|
}
|
|
|
|
|
2021-11-02 19:40:49 +00:00
|
|
|
var queryItems = [URLQueryItem]()
|
|
|
|
|
2021-10-26 22:59:59 +00:00
|
|
|
switch item.contentType {
|
|
|
|
case .video:
|
|
|
|
urlComponents.path = "/watch"
|
2021-11-02 19:40:49 +00:00
|
|
|
queryItems.append(.init(name: "v", value: item.video.videoID))
|
2021-10-26 22:59:59 +00:00
|
|
|
case .channel:
|
|
|
|
urlComponents.path = "/channel/\(item.channel.id)"
|
|
|
|
case .playlist:
|
|
|
|
urlComponents.path = "/playlist"
|
2021-11-02 19:40:49 +00:00
|
|
|
queryItems.append(.init(name: "list", value: item.playlist.id))
|
2022-03-27 10:49:57 +00:00
|
|
|
default:
|
|
|
|
return nil
|
2021-11-02 19:40:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !time.isNil, time!.seconds.isFinite {
|
|
|
|
queryItems.append(.init(name: "t", value: "\(Int(time!.seconds))s"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if !queryItems.isEmpty {
|
|
|
|
urlComponents.queryItems = queryItems
|
2021-10-26 22:59:59 +00:00
|
|
|
}
|
|
|
|
|
2021-11-13 15:45:47 +00:00
|
|
|
return urlComponents.url
|
2021-10-26 22:59:59 +00:00
|
|
|
}
|
2022-06-18 12:39:49 +00:00
|
|
|
|
|
|
|
func extractChapters(from description: String) -> [Chapter] {
|
2023-12-27 21:36:52 +00:00
|
|
|
/*
|
|
|
|
The following chapter patterns are covered:
|
|
|
|
|
|
|
|
start - end - title / start - end: Title / start - end title
|
|
|
|
start - title / start: title / start title / [start] - title / [start]: title / [start] title
|
|
|
|
index. title - start / index. title start
|
|
|
|
title: (start)
|
|
|
|
|
|
|
|
The order is important!
|
|
|
|
*/
|
|
|
|
let patterns = [
|
|
|
|
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?<end>(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?<title>.*)(?=\\n|$)",
|
|
|
|
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?\\s*[-:]?\\s*(?<title>.+)(?=\\n|$)",
|
|
|
|
"(?<=\\n|^)(?<index>[0-9]+\\.\\s)(?<title>.+?)(?:\\s*-\\s*)?(?<start>(?:[0-9]+:){1,2}[0-9]+)(?=\\n|$)",
|
|
|
|
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)"
|
|
|
|
]
|
|
|
|
|
|
|
|
for pattern in patterns {
|
|
|
|
guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue }
|
|
|
|
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
|
|
|
|
|
|
|
|
if !chapterLines.isEmpty {
|
|
|
|
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]).trimmingCharacters(in: .whitespaces)
|
|
|
|
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 }
|
|
|
|
|
|
|
|
startSeconds += (minutes ?? 0) * 60
|
|
|
|
startSeconds += (hours ?? 0) * 60 * 60
|
|
|
|
|
|
|
|
return .init(title: titleCapture, start: startSeconds)
|
|
|
|
}
|
2022-06-18 12:39:49 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-27 21:36:52 +00:00
|
|
|
return []
|
2022-06-18 12:39:49 +00:00
|
|
|
}
|
2021-10-20 22:21:50 +00:00
|
|
|
}
|