mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 18:54:11 +00:00
Channel playlists support
This commit is contained in:
@@ -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!)!)
|
||||
|
@@ -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
|
||||
|
@@ -21,4 +21,6 @@ protocol VideosAPI {
|
||||
|
||||
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource?
|
||||
func playlistVideos(_ id: String) -> Resource?
|
||||
|
||||
func channelPlaylist(_ id: String) -> Resource?
|
||||
}
|
||||
|
10
Model/ChannelPlaylist.swift
Normal file
10
Model/ChannelPlaylist.swift
Normal 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?
|
||||
}
|
@@ -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] {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user