Channel playlists support

This commit is contained in:
Arkadiusz Fal
2021-10-23 01:04:03 +02:00
parent 4307da57c5
commit 734bb31260
22 changed files with 402 additions and 89 deletions

View File

@@ -95,8 +95,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
if type == "channel" {
return ContentItem(channel: InvidiousAPI.extractChannel(from: $0))
} else if type == "playlist" {
// TODO: fix playlists
return ContentItem(playlist: Playlist(JSON(parseJSON: "{}")))
return ContentItem(playlist: InvidiousAPI.extractChannelPlaylist(from: $0))
}
return ContentItem(video: InvidiousAPI.extractVideo($0))
}
@@ -143,6 +142,10 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.map(InvidiousAPI.extractVideo)
}
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
InvidiousAPI.extractChannelPlaylist(from: content.json)
}
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
InvidiousAPI.extractVideo(content.json)
}
@@ -214,6 +217,10 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
playlist(playlistID)?.child("videos").child(videoID)
}
func channelPlaylist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
}
func search(_ query: SearchQuery) -> Resource {
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
.withParam("q", searchQuery(query.query))
@@ -325,6 +332,21 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
)
}
static func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist {
let details = json.dictionaryValue
return ChannelPlaylist(
id: details["playlistId"]!.stringValue,
title: details["title"]!.stringValue,
thumbnailURL: details["playlistThumbnail"]?.url,
channel: extractChannel(from: json),
videos: details["videos"]?.arrayValue.compactMap(InvidiousAPI.extractVideo) ?? []
)
}
static func extractChannelPlaylists(from json: JSON) -> [ChannelPlaylist] {
json.arrayValue.map(InvidiousAPI.extractChannelPlaylist)
}
private static func extractThumbnails(from details: JSON) -> [Thumbnail] {
details["videoThumbnails"].arrayValue.map { json in
Thumbnail(url: json["url"].url!, quality: .init(rawValue: json["quality"].string!)!)

View File

@@ -35,6 +35,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
PipedAPI.extractChannel(content.json)
}
configureTransformer(pathPattern("playlists/*")) { (content: Entity<JSON>) -> ChannelPlaylist? in
PipedAPI.extractChannelPlaylist(from: content.json)
}
configureTransformer(pathPattern("streams/*")) { (content: Entity<JSON>) -> Video? in
PipedAPI.extractVideo(content.json)
}
@@ -56,6 +60,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "channel/\(id)")
}
func channelPlaylist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: "playlists/\(id)")
}
func trending(country: Country, category _: TrendingCategory? = nil) -> Resource {
resource(baseURL: account.instance.url, path: "trending")
.withParam("region", country.rawValue)
@@ -118,7 +126,9 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
}
case .playlist:
return nil
if let playlist = PipedAPI.extractChannelPlaylist(from: content) {
return ContentItem(playlist: playlist)
}
case .channel:
if let channel = PipedAPI.extractChannel(content) {
@@ -136,7 +146,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
private static func extractChannel(_ content: JSON) -> Channel? {
let attributes = content.dictionaryValue
guard let id = attributes["id"]?.stringValue ??
attributes["url"]?.stringValue.components(separatedBy: "/").last
(attributes["url"] ?? attributes["uploaderUrl"])?.stringValue.components(separatedBy: "/").last
else {
return nil
}
@@ -157,6 +167,28 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
)
}
static func extractChannelPlaylist(from json: JSON) -> ChannelPlaylist? {
let details = json.dictionaryValue
let id = details["url"]?.stringValue.components(separatedBy: "?list=").last ?? UUID().uuidString
let thumbnailURL = details["thumbnail"]?.url ?? details["thumbnailUrl"]?.url
var videos = [Video]()
if let relatedStreams = details["relatedStreams"] {
videos = PipedAPI.extractVideos(relatedStreams)
}
return ChannelPlaylist(
id: id,
title: details["name"]!.stringValue,
thumbnailURL: thumbnailURL,
channel: extractChannel(json)!,
videos: videos,
videosCount: details["videos"]?.int
)
}
static func extractChannelPlaylists(from json: JSON) -> [ChannelPlaylist] {
json.arrayValue.compactMap(PipedAPI.extractChannelPlaylist)
}
private static func extractVideo(_ content: JSON) -> Video? {
let details = content.dictionaryValue
let url = details["url"]?.string

View File

@@ -21,4 +21,6 @@ protocol VideosAPI {
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
func playlistVideos(_ id: String) -> Resource?
func channelPlaylist(_ id: String) -> Resource?
}

View File

@@ -0,0 +1,10 @@
import Foundation
struct ChannelPlaylist: Identifiable {
var id: String = UUID().uuidString
var title: String
var thumbnailURL: URL?
var channel: Channel?
var videos = [Video]()
var videosCount: Int?
}

View File

@@ -8,7 +8,7 @@ struct ContentItem: Identifiable {
switch self {
case .channel:
return 1
case .video:
case .playlist:
return 2
default:
return 3
@@ -21,7 +21,7 @@ struct ContentItem: Identifiable {
}
var video: Video!
var playlist: Playlist!
var playlist: ChannelPlaylist!
var channel: Channel!
static func array(of videos: [Video]) -> [ContentItem] {

View File

@@ -26,7 +26,8 @@ final class NavigationModel: ObservableObject {
@Published var presentingUnsubscribeAlert = false
@Published var channelToUnsubscribe: Channel!
@Published var isChannelOpen = false
@Published var presentingChannel = false
@Published var presentingPlaylist = false
@Published var sidebarSectionChanged = false
@Published var presentingSettings = false

View File

@@ -27,17 +27,16 @@ final class PlayerModel: ObservableObject {
@Published var queue = [PlayerQueueItem]()
@Published var currentItem: PlayerQueueItem!
@Published var live = false
@Published var history = [PlayerQueueItem]()
@Published var savedTime: CMTime?
@Published var composition = AVMutableComposition()
@Published var playerNavigationLinkActive = false
var accounts: AccountsModel
var instances: InstancesModel
var composition = AVMutableComposition()
var timeObserver: Any?
private var shouldResumePlaying = true
private var statusObservation: NSKeyValueObservation?
@@ -61,6 +60,10 @@ final class PlayerModel: ObservableObject {
currentItem?.playbackTime
}
var live: Bool {
currentItem?.video.live ?? false
}
var playerItemDuration: CMTime? {
player.currentItem?.asset.duration
}
@@ -335,7 +338,6 @@ final class PlayerModel: ObservableObject {
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
self.currentRate = self.player.rate
self.live = self.currentVideo?.live ?? false
self.currentItem?.playbackTime = self.player.currentTime()
self.currentItem?.videoDuration = self.player.currentItem?.asset.duration.seconds
}

View File

@@ -39,13 +39,21 @@ final class RecentsModel: ObservableObject {
return nil
}
var presentedPlaylist: ChannelPlaylist? {
if let recent = items.last(where: { $0.type == .playlist }) {
return recent.playlist
}
return nil
}
}
struct RecentItem: Defaults.Serializable, Identifiable {
static var bridge = RecentItemBridge()
enum ItemType: String {
case channel, query
case channel, playlist, query
}
var type: ItemType
@@ -72,6 +80,14 @@ struct RecentItem: Defaults.Serializable, Identifiable {
return Channel(id: id, name: title)
}
var playlist: ChannelPlaylist? {
guard type == .playlist else {
return nil
}
return ChannelPlaylist(id: id, title: title)
}
init(type: ItemType, identifier: String, title: String) {
self.type = type
id = identifier
@@ -89,6 +105,12 @@ struct RecentItem: Defaults.Serializable, Identifiable {
id = query
title = query
}
init(from playlist: ChannelPlaylist) {
type = .playlist
id = playlist.id
title = playlist.title
}
}
struct RecentItemBridge: Defaults.Bridge {