Comments UI fixes

This commit is contained in:
Arkadiusz Fal 2021-12-17 17:39:26 +01:00
parent 8d49934fe8
commit 008cd1553d
3 changed files with 92 additions and 65 deletions

View File

@ -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) }
)
}
} }

View File

@ -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

View File

@ -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 {