From 8d36f572718d96407e6672d7c90126116310f23b Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 10 Apr 2022 17:07:10 +0200 Subject: [PATCH] Preliminary support for Piped playlist (listing playlists and videos) --- Model/Applications/PipedAPI.swift | 23 ++++++++++++++++++--- Model/Applications/VideosApp.swift | 4 ++++ Model/Playlist.swift | 4 ++-- Shared/Navigation/AppSidebarPlaylists.swift | 16 +++++++++++--- Shared/Player/PlaybackBar.swift | 3 ++- Shared/Views/PlaylistVideosView.swift | 16 +++++++++++++- 6 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index fc4879f3..df9e61f1 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -4,7 +4,7 @@ import Siesta import SwiftyJSON final class PipedAPI: Service, ObservableObject, VideosAPI { - static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe"] + static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"] @Published var account: Account! @@ -81,6 +81,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled) } + configureTransformer(pathPattern("user/playlists")) { (content: Entity) -> [Playlist] in + content.json.arrayValue.map { self.extractUserPlaylist(from: $0)! } + } + if account.token.isNil { updateToken() } @@ -166,7 +170,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { var home: Resource? { nil } var popular: Resource? { nil } - var playlists: Resource? { nil } + var playlists: Resource? { + resource(baseURL: account.instance.apiURL, path: "user/playlists") + } func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) { resource(baseURL: account.instance.apiURL, path: "subscribe") @@ -180,7 +186,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { .onCompletion { _ in onCompletion() } } - func playlist(_: String) -> Resource? { nil } + func playlist(_ id: String) -> Resource? { + channelPlaylist(id) + } + func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideos(_: String) -> Resource? { nil } @@ -359,6 +368,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { )! } + private func extractUserPlaylist(from json: JSON) -> Playlist? { + let id = json["id"].stringValue + let title = json["name"].stringValue + let visibility = Playlist.Visibility.private + + return Playlist(id: id, title: title, visibility: visibility) + } + private func extractDescription(from content: JSON) -> String? { guard var description = content.dictionaryValue["description"]?.string else { return nil diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 0b8b95aa..0ea6fc35 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -32,6 +32,10 @@ enum VideosApp: String, CaseIterable { } var supportsUserPlaylists: Bool { + true + } + + var userPlaylistsEndpointIncludesVideos: Bool { self == .invidious } diff --git a/Model/Playlist.swift b/Model/Playlist.swift index 590edd7a..854efb71 100644 --- a/Model/Playlist.swift +++ b/Model/Playlist.swift @@ -18,11 +18,11 @@ struct Playlist: Identifiable, Equatable, Hashable { var title: String var visibility: Visibility - var updated: TimeInterval + var updated: TimeInterval? var videos = [Video]() - init(id: String, title: String, visibility: Visibility, updated: TimeInterval, videos: [Video] = []) { + init(id: String, title: String, visibility: Visibility, updated: TimeInterval? = nil, videos: [Video] = []) { self.id = id self.title = title self.visibility = visibility diff --git a/Shared/Navigation/AppSidebarPlaylists.swift b/Shared/Navigation/AppSidebarPlaylists.swift index 69342351..4dd254c2 100644 --- a/Shared/Navigation/AppSidebarPlaylists.swift +++ b/Shared/Navigation/AppSidebarPlaylists.swift @@ -11,9 +11,7 @@ struct AppSidebarPlaylists: View { NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) { LazyView(PlaylistVideosView(playlist)) } label: { - Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title)) - .backport - .badge(Text("\(playlist.videos.count)")) + playlistLabel(playlist) } .id(playlist.id) .contextMenu { @@ -34,6 +32,18 @@ struct AppSidebarPlaylists: View { } } + @ViewBuilder func playlistLabel(_ playlist: Playlist) -> some View { + let label = Label(playlist.title, systemImage: RecentsModel.symbolSystemImage(playlist.title)) + + if player.accounts.app.userPlaylistsEndpointIncludesVideos { + label + .backport + .badge(Text("\(playlist.videos.count)")) + } else { + label + } + } + var newPlaylistButton: some View { Button(action: { navigation.presentNewPlaylistForm() }) { Label("New Playlist", systemImage: "plus.circle") diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index e27e80cd..e3fdab8b 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -104,7 +104,8 @@ struct PlaybackBar: View { } guard let video = player.currentVideo, - let time = player.time else { + let time = player.time + else { return "" } diff --git a/Shared/Views/PlaylistVideosView.swift b/Shared/Views/PlaylistVideosView.swift index 31f40c5e..73232c4d 100644 --- a/Shared/Views/PlaylistVideosView.swift +++ b/Shared/Views/PlaylistVideosView.swift @@ -7,8 +7,17 @@ struct PlaylistVideosView: View { @Environment(\.inNavigationView) private var inNavigationView @EnvironmentObject private var player + @StateObject private var store = Store() + var contentItems: [ContentItem] { - ContentItem.array(of: playlist.videos) + ContentItem.array(of: playlist.videos.isEmpty ? (store.item?.videos ?? []) : playlist.videos) + } + + private var resource: Resource? { + let resource = player.accounts.api.playlist(playlist.id) + resource?.addObserver(store) + + return resource } var videos: [Video] { @@ -22,6 +31,11 @@ struct PlaylistVideosView: View { var body: some View { PlayerControlsView { VerticalCells(items: contentItems) + .onAppear { + if !player.accounts.app.userPlaylistsEndpointIncludesVideos { + resource?.loadIfNeeded() + } + } #if !os(tvOS) .navigationTitle("\(playlist.title) Playlist") #endif