mirror of
https://github.com/yattee/yattee.git
synced 2025-01-08 22:07:10 +00:00
Comments UI fixes
This commit is contained in:
parent
8d49934fe8
commit
008cd1553d
@ -27,10 +27,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
self.account = account
|
self.account = account
|
||||||
signedIn = false
|
signedIn = false
|
||||||
|
|
||||||
|
if account.anonymous {
|
||||||
|
validInstance = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
validInstance = false
|
validInstance = false
|
||||||
signedIn = !account.anonymous
|
|
||||||
|
|
||||||
configure()
|
configure()
|
||||||
|
validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate() {
|
func validate() {
|
||||||
@ -81,11 +86,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(InvidiousAPI.extractVideo)
|
content.json.arrayValue.map(self.extractVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(InvidiousAPI.extractVideo)
|
content.json.arrayValue.map(self.extractVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> [ContentItem] in
|
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> [ContentItem] in
|
||||||
@ -93,11 +98,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
let type = $0.dictionaryValue["type"]?.stringValue
|
let type = $0.dictionaryValue["type"]?.stringValue
|
||||||
|
|
||||||
if type == "channel" {
|
if type == "channel" {
|
||||||
return ContentItem(channel: InvidiousAPI.extractChannel(from: $0))
|
return ContentItem(channel: self.extractChannel(from: $0))
|
||||||
} else if type == "playlist" {
|
} else if type == "playlist" {
|
||||||
return ContentItem(playlist: InvidiousAPI.extractChannelPlaylist(from: $0))
|
return ContentItem(playlist: self.extractChannelPlaylist(from: $0))
|
||||||
}
|
}
|
||||||
return ContentItem(video: InvidiousAPI.extractVideo(from: $0))
|
return ContentItem(video: self.extractVideo(from: $0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,11 +115,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Playlist] in
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Playlist] in
|
||||||
content.json.arrayValue.map(Playlist.init)
|
content.json.arrayValue.map(self.extractPlaylist)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
configureTransformer(pathPattern("auth/playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
|
||||||
Playlist(content.json)
|
self.extractPlaylist(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
|
configureTransformer(pathPattern("auth/playlists"), requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
|
||||||
@ -124,30 +129,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
if let feedVideos = content.json.dictionaryValue["videos"] {
|
if let feedVideos = content.json.dictionaryValue["videos"] {
|
||||||
return feedVideos.arrayValue.map(InvidiousAPI.extractVideo)
|
return feedVideos.arrayValue.map(self.extractVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("auth/subscriptions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Channel] in
|
configureTransformer(pathPattern("auth/subscriptions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Channel] in
|
||||||
content.json.arrayValue.map(InvidiousAPI.extractChannel)
|
content.json.arrayValue.map(self.extractChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
||||||
InvidiousAPI.extractChannel(from: content.json)
|
self.extractChannel(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(InvidiousAPI.extractVideo)
|
content.json.arrayValue.map(self.extractVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
|
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
|
||||||
InvidiousAPI.extractChannelPlaylist(from: content.json)
|
self.extractChannelPlaylist(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
|
||||||
InvidiousAPI.extractVideo(from: content.json)
|
self.extractVideo(from: content.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +297,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return AVURLAsset(url: url)
|
return AVURLAsset(url: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractVideo(from json: JSON) -> Video {
|
func extractVideo(from json: JSON) -> Video {
|
||||||
let indexID: String?
|
let indexID: String?
|
||||||
var id: Video.ID
|
var id: Video.ID
|
||||||
var publishedAt: Date?
|
var publishedAt: Date?
|
||||||
@ -335,8 +340,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractChannel(from json: JSON) -> Channel {
|
func extractChannel(from json: JSON) -> Channel {
|
||||||
let thumbnailURL = "https:\(json["authorThumbnails"].arrayValue.first?.dictionaryValue["url"]?.stringValue ?? "")"
|
var thumbnailURL = json["authorThumbnails"].arrayValue.last?.dictionaryValue["url"]?.stringValue ?? ""
|
||||||
|
|
||||||
|
// append https protocol to unproxied thumbnail URL if it's missing
|
||||||
|
if thumbnailURL.count > 2,
|
||||||
|
String(thumbnailURL[..<thumbnailURL.index(thumbnailURL.startIndex, offsetBy: 2)]) == "//"
|
||||||
|
{
|
||||||
|
thumbnailURL = "https:\(thumbnailURL)"
|
||||||
|
}
|
||||||
|
|
||||||
return Channel(
|
return Channel(
|
||||||
id: json["authorId"].stringValue,
|
id: json["authorId"].stringValue,
|
||||||
@ -344,33 +356,33 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
thumbnailURL: URL(string: thumbnailURL),
|
thumbnailURL: URL(string: thumbnailURL),
|
||||||
subscriptionsCount: json["subCount"].int,
|
subscriptionsCount: json["subCount"].int,
|
||||||
subscriptionsText: json["subCountText"].string,
|
subscriptionsText: json["subCountText"].string,
|
||||||
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(InvidiousAPI.extractVideo) ?? []
|
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(extractVideo) ?? []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist {
|
func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist {
|
||||||
let details = json.dictionaryValue
|
let details = json.dictionaryValue
|
||||||
return ChannelPlaylist(
|
return ChannelPlaylist(
|
||||||
id: details["playlistId"]!.stringValue,
|
id: details["playlistId"]!.stringValue,
|
||||||
title: details["title"]!.stringValue,
|
title: details["title"]!.stringValue,
|
||||||
thumbnailURL: details["playlistThumbnail"]?.url,
|
thumbnailURL: details["playlistThumbnail"]?.url,
|
||||||
channel: extractChannel(from: json),
|
channel: extractChannel(from: json),
|
||||||
videos: details["videos"]?.arrayValue.compactMap(InvidiousAPI.extractVideo) ?? []
|
videos: details["videos"]?.arrayValue.compactMap(extractVideo) ?? []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||||
details["videoThumbnails"].arrayValue.map { json in
|
details["videoThumbnails"].arrayValue.map { json in
|
||||||
Thumbnail(url: json["url"].url!, quality: .init(rawValue: json["quality"].string!)!)
|
Thumbnail(url: json["url"].url!, quality: .init(rawValue: json["quality"].string!)!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractStreams(from json: JSON) -> [Stream] {
|
private func extractStreams(from json: JSON) -> [Stream] {
|
||||||
extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
||||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
|
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||||
streams.map {
|
streams.map {
|
||||||
SingleAssetStream(
|
SingleAssetStream(
|
||||||
avAsset: AVURLAsset(url: $0["url"].url!),
|
avAsset: AVURLAsset(url: $0["url"].url!),
|
||||||
@ -381,7 +393,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
|
private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] {
|
||||||
let audioAssetURL = streams.first { $0["type"].stringValue.starts(with: "audio/mp4") }
|
let audioAssetURL = streams.first { $0["type"].stringValue.starts(with: "audio/mp4") }
|
||||||
guard audioAssetURL != nil else {
|
guard audioAssetURL != nil else {
|
||||||
return []
|
return []
|
||||||
@ -400,10 +412,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractRelated(from content: JSON) -> [Video] {
|
private func extractRelated(from content: JSON) -> [Video] {
|
||||||
content
|
content
|
||||||
.dictionaryValue["recommendedVideos"]?
|
.dictionaryValue["recommendedVideos"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
.compactMap(extractVideo(from:)) ?? []
|
.compactMap(extractVideo(from:)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func extractPlaylist(from content: JSON) -> Playlist {
|
||||||
|
.init(
|
||||||
|
id: content["playlistId"].stringValue,
|
||||||
|
title: content["title"].stringValue,
|
||||||
|
visibility: content["isListed"].boolValue ? .public : .private,
|
||||||
|
updated: content["updated"].doubleValue,
|
||||||
|
videos: content["videos"].arrayValue.map { extractVideo(from: $0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,23 +40,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
||||||
PipedAPI.extractChannel(from: content.json)
|
self.extractChannel(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("playlists/*")) { (content: Entity<JSON>) -> ChannelPlaylist? in
|
configureTransformer(pathPattern("playlists/*")) { (content: Entity<JSON>) -> ChannelPlaylist? in
|
||||||
PipedAPI.extractChannelPlaylist(from: content.json)
|
self.extractChannelPlaylist(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
|
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
|
||||||
PipedAPI.extractVideo(from: content.json)
|
self.extractVideo(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("trending")) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("trending")) { (content: Entity<JSON>) -> [Video] in
|
||||||
PipedAPI.extractVideos(from: content.json)
|
self.extractVideos(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("search")) { (content: Entity<JSON>) -> [ContentItem] in
|
configureTransformer(pathPattern("search")) { (content: Entity<JSON>) -> [ContentItem] in
|
||||||
PipedAPI.extractContentItems(from: content.json.dictionaryValue["items"]!)
|
self.extractContentItems(from: content.json.dictionaryValue["items"]!)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
|
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
|
||||||
@ -64,16 +64,16 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("subscriptions")) { (content: Entity<JSON>) -> [Channel] in
|
configureTransformer(pathPattern("subscriptions")) { (content: Entity<JSON>) -> [Channel] in
|
||||||
content.json.arrayValue.map { PipedAPI.extractChannel(from: $0)! }
|
content.json.arrayValue.map { self.extractChannel(from: $0)! }
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("feed")) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("feed")) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! }
|
content.json.arrayValue.map { self.extractVideo(from: $0)! }
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
|
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
|
||||||
let details = content.json.dictionaryValue
|
let details = content.json.dictionaryValue
|
||||||
let comments = details["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? []
|
let comments = details["comments"]?.arrayValue.map { self.extractComment(from: $0)! } ?? []
|
||||||
let nextPage = details["nextpage"]?.stringValue
|
let nextPage = details["nextpage"]?.stringValue
|
||||||
let disabled = details["disabled"]?.boolValue ?? false
|
let disabled = details["disabled"]?.boolValue ?? false
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func needsAuthorization(_ url: URL) -> Bool {
|
func needsAuthorization(_ url: URL) -> Bool {
|
||||||
PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) }
|
Self.authorizedEndpoints.contains { url.absoluteString.contains($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateToken() {
|
func updateToken() {
|
||||||
@ -190,7 +190,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
"**\(path)"
|
"**\(path)"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractContentItem(from content: JSON) -> ContentItem? {
|
private func extractContentItem(from content: JSON) -> ContentItem? {
|
||||||
let details = content.dictionaryValue
|
let details = content.dictionaryValue
|
||||||
let url: String! = details["url"]?.string
|
let url: String! = details["url"]?.string
|
||||||
|
|
||||||
@ -210,17 +210,17 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case .video:
|
case .video:
|
||||||
if let video = PipedAPI.extractVideo(from: content) {
|
if let video = extractVideo(from: content) {
|
||||||
return ContentItem(video: video)
|
return ContentItem(video: video)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .playlist:
|
case .playlist:
|
||||||
if let playlist = PipedAPI.extractChannelPlaylist(from: content) {
|
if let playlist = extractChannelPlaylist(from: content) {
|
||||||
return ContentItem(playlist: playlist)
|
return ContentItem(playlist: playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .channel:
|
case .channel:
|
||||||
if let channel = PipedAPI.extractChannel(from: content) {
|
if let channel = extractChannel(from: content) {
|
||||||
return ContentItem(channel: channel)
|
return ContentItem(channel: channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,11 +228,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractContentItems(from content: JSON) -> [ContentItem] {
|
private func extractContentItems(from content: JSON) -> [ContentItem] {
|
||||||
content.arrayValue.compactMap { PipedAPI.extractContentItem(from: $0) }
|
content.arrayValue.compactMap { extractContentItem(from: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractChannel(from content: JSON) -> Channel? {
|
private func extractChannel(from content: JSON) -> Channel? {
|
||||||
let attributes = content.dictionaryValue
|
let attributes = content.dictionaryValue
|
||||||
guard let id = attributes["id"]?.stringValue ??
|
guard let id = attributes["id"]?.stringValue ??
|
||||||
(attributes["url"] ?? attributes["uploaderUrl"])?.stringValue.components(separatedBy: "/").last
|
(attributes["url"] ?? attributes["uploaderUrl"])?.stringValue.components(separatedBy: "/").last
|
||||||
@ -244,25 +244,28 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
var videos = [Video]()
|
var videos = [Video]()
|
||||||
if let relatedStreams = attributes["relatedStreams"] {
|
if let relatedStreams = attributes["relatedStreams"] {
|
||||||
videos = PipedAPI.extractVideos(from: relatedStreams)
|
videos = extractVideos(from: relatedStreams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = attributes["name"]?.stringValue ?? attributes["uploaderName"]?.stringValue ?? attributes["uploader"]?.stringValue ?? ""
|
||||||
|
let thumbnailURL = attributes["avatarUrl"]?.url ?? attributes["uploaderAvatar"]?.url ?? attributes["avatar"]?.url ?? attributes["thumbnail"]?.url
|
||||||
|
|
||||||
return Channel(
|
return Channel(
|
||||||
id: id,
|
id: id,
|
||||||
name: attributes["name"]!.stringValue,
|
name: name,
|
||||||
thumbnailURL: attributes["thumbnail"]?.url,
|
thumbnailURL: thumbnailURL,
|
||||||
subscriptionsCount: subscriptionsCount,
|
subscriptionsCount: subscriptionsCount,
|
||||||
videos: videos
|
videos: videos
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist? {
|
func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist? {
|
||||||
let details = json.dictionaryValue
|
let details = json.dictionaryValue
|
||||||
let id = details["url"]?.stringValue.components(separatedBy: "?list=").last ?? UUID().uuidString
|
let id = details["url"]?.stringValue.components(separatedBy: "?list=").last ?? UUID().uuidString
|
||||||
let thumbnailURL = details["thumbnail"]?.url ?? details["thumbnailUrl"]?.url
|
let thumbnailURL = details["thumbnail"]?.url ?? details["thumbnailUrl"]?.url
|
||||||
var videos = [Video]()
|
var videos = [Video]()
|
||||||
if let relatedStreams = details["relatedStreams"] {
|
if let relatedStreams = details["relatedStreams"] {
|
||||||
videos = PipedAPI.extractVideos(from: relatedStreams)
|
videos = extractVideos(from: relatedStreams)
|
||||||
}
|
}
|
||||||
return ChannelPlaylist(
|
return ChannelPlaylist(
|
||||||
id: id,
|
id: id,
|
||||||
@ -274,7 +277,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractVideo(from content: JSON) -> Video? {
|
private func extractVideo(from content: JSON) -> Video? {
|
||||||
let details = content.dictionaryValue
|
let details = content.dictionaryValue
|
||||||
let url = details["url"]?.string
|
let url = details["url"]?.string
|
||||||
|
|
||||||
@ -287,7 +290,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
let channelId = details["uploaderUrl"]!.stringValue.components(separatedBy: "/").last!
|
let channelId = details["uploaderUrl"]!.stringValue.components(separatedBy: "/").last!
|
||||||
|
|
||||||
let thumbnails: [Thumbnail] = Thumbnail.Quality.allCases.compactMap {
|
let thumbnails: [Thumbnail] = Thumbnail.Quality.allCases.compactMap {
|
||||||
if let url = PipedAPI.buildThumbnailURL(from: content, quality: $0) {
|
if let url = buildThumbnailURL(from: content, quality: $0) {
|
||||||
return Thumbnail(url: url, quality: $0)
|
return Thumbnail(url: url, quality: $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,18 +298,20 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
let author = details["uploaderName"]?.stringValue ?? details["uploader"]!.stringValue
|
||||||
|
let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url
|
||||||
|
|
||||||
let published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ??
|
let published = (details["uploadedDate"] ?? details["uploadDate"])?.stringValue ??
|
||||||
(details["uploaded"]!.double! / 1000).formattedAsRelativeTime()!
|
(details["uploaded"]!.double! / 1000).formattedAsRelativeTime()!
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
videoID: PipedAPI.extractID(from: content),
|
videoID: extractID(from: content),
|
||||||
title: details["title"]!.stringValue,
|
title: details["title"]!.stringValue,
|
||||||
author: author,
|
author: author,
|
||||||
length: details["duration"]!.doubleValue,
|
length: details["duration"]!.doubleValue,
|
||||||
published: published,
|
published: published,
|
||||||
views: details["views"]!.intValue,
|
views: details["views"]!.intValue,
|
||||||
description: PipedAPI.extractDescription(from: content),
|
description: extractDescription(from: content),
|
||||||
channel: Channel(id: channelId, name: author),
|
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL),
|
||||||
thumbnails: thumbnails,
|
thumbnails: thumbnails,
|
||||||
likes: details["likes"]?.int,
|
likes: details["likes"]?.int,
|
||||||
dislikes: details["dislikes"]?.int,
|
dislikes: details["dislikes"]?.int,
|
||||||
@ -315,16 +320,16 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractID(from content: JSON) -> Video.ID {
|
private func extractID(from content: JSON) -> Video.ID {
|
||||||
content.dictionaryValue["url"]?.stringValue.components(separatedBy: "?v=").last ??
|
content.dictionaryValue["url"]?.stringValue.components(separatedBy: "?v=").last ??
|
||||||
extractThumbnailURL(from: content)!.relativeString.components(separatedBy: "/")[4]
|
extractThumbnailURL(from: content)!.relativeString.components(separatedBy: "/")[4]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractThumbnailURL(from content: JSON) -> URL? {
|
private func extractThumbnailURL(from content: JSON) -> URL? {
|
||||||
content.dictionaryValue["thumbnail"]?.url! ?? content.dictionaryValue["thumbnailUrl"]!.url!
|
content.dictionaryValue["thumbnail"]?.url! ?? content.dictionaryValue["thumbnailUrl"]!.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func buildThumbnailURL(from content: JSON, quality: Thumbnail.Quality) -> URL? {
|
private func buildThumbnailURL(from content: JSON, quality: Thumbnail.Quality) -> URL? {
|
||||||
let thumbnailURL = extractThumbnailURL(from: content)
|
let thumbnailURL = extractThumbnailURL(from: content)
|
||||||
guard !thumbnailURL.isNil else {
|
guard !thumbnailURL.isNil else {
|
||||||
return nil
|
return nil
|
||||||
@ -337,7 +342,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)!
|
)!
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractDescription(from content: JSON) -> String? {
|
private func extractDescription(from content: JSON) -> String? {
|
||||||
guard var description = content.dictionaryValue["description"]?.string else {
|
guard var description = content.dictionaryValue["description"]?.string else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -359,22 +364,22 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return description
|
return description
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractVideos(from content: JSON) -> [Video] {
|
private func extractVideos(from content: JSON) -> [Video] {
|
||||||
content.arrayValue.compactMap(extractVideo(from:))
|
content.arrayValue.compactMap(extractVideo(from:))
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractStreams(from content: JSON) -> [Stream] {
|
private func extractStreams(from content: JSON) -> [Stream] {
|
||||||
var streams = [Stream]()
|
var streams = [Stream]()
|
||||||
|
|
||||||
if let hlsURL = content.dictionaryValue["hls"]?.url {
|
if let hlsURL = content.dictionaryValue["hls"]?.url {
|
||||||
streams.append(Stream(hlsURL: hlsURL))
|
streams.append(Stream(hlsURL: hlsURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let audioStream = PipedAPI.compatibleAudioStreams(from: content).first else {
|
guard let audioStream = compatibleAudioStreams(from: content).first else {
|
||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoStreams = PipedAPI.compatibleVideoStream(from: content)
|
let videoStreams = compatibleVideoStream(from: content)
|
||||||
|
|
||||||
videoStreams.forEach { videoStream in
|
videoStreams.forEach { videoStream in
|
||||||
let audioAsset = AVURLAsset(url: audioStream.dictionaryValue["url"]!.url!)
|
let audioAsset = AVURLAsset(url: audioStream.dictionaryValue["url"]!.url!)
|
||||||
@ -397,14 +402,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractRelated(from content: JSON) -> [Video] {
|
private func extractRelated(from content: JSON) -> [Video] {
|
||||||
content
|
content
|
||||||
.dictionaryValue["relatedStreams"]?
|
.dictionaryValue["relatedStreams"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
.compactMap(extractVideo(from:)) ?? []
|
.compactMap(extractVideo(from:)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func compatibleAudioStreams(from content: JSON) -> [JSON] {
|
private func compatibleAudioStreams(from content: JSON) -> [JSON] {
|
||||||
content
|
content
|
||||||
.dictionaryValue["audioStreams"]?
|
.dictionaryValue["audioStreams"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
@ -414,14 +419,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
} ?? []
|
} ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func compatibleVideoStream(from content: JSON) -> [JSON] {
|
private func compatibleVideoStream(from content: JSON) -> [JSON] {
|
||||||
content
|
content
|
||||||
.dictionaryValue["videoStreams"]?
|
.dictionaryValue["videoStreams"]?
|
||||||
.arrayValue
|
.arrayValue
|
||||||
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? []
|
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func extractComment(from content: JSON) -> Comment? {
|
private func extractComment(from content: JSON) -> Comment? {
|
||||||
let details = content.dictionaryValue
|
let details = content.dictionaryValue
|
||||||
let author = details["author"]?.stringValue ?? ""
|
let author = details["author"]?.stringValue ?? ""
|
||||||
let commentorUrl = details["commentorUrl"]?.stringValue
|
let commentorUrl = details["commentorUrl"]?.stringValue
|
||||||
|
@ -22,11 +22,12 @@ struct Playlist: Identifiable, Equatable, Hashable {
|
|||||||
|
|
||||||
var videos = [Video]()
|
var videos = [Video]()
|
||||||
|
|
||||||
init(id: String, title: String, visibility: Visibility, updated: TimeInterval) {
|
init(id: String, title: String, visibility: Visibility, updated: TimeInterval, videos: [Video] = []) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.updated = updated
|
self.updated = updated
|
||||||
|
self.videos = videos
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ json: JSON) {
|
init(_ json: JSON) {
|
||||||
@ -34,7 +35,6 @@ struct Playlist: Identifiable, Equatable, Hashable {
|
|||||||
title = json["title"].stringValue
|
title = json["title"].stringValue
|
||||||
visibility = json["isListed"].boolValue ? .public : .private
|
visibility = json["isListed"].boolValue ? .public : .private
|
||||||
updated = json["updated"].doubleValue
|
updated = json["updated"].doubleValue
|
||||||
videos = json["videos"].arrayValue.map { InvidiousAPI.extractVideo(from: $0) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: Playlist, rhs: Playlist) -> Bool {
|
static func == (lhs: Playlist, rhs: Playlist) -> Bool {
|
||||||
|
Loading…
Reference in New Issue
Block a user