Add related videos

This commit is contained in:
Arkadiusz Fal 2021-11-03 00:02:02 +01:00
parent f49453e871
commit f8e6560698
13 changed files with 185 additions and 46 deletions

View File

@ -314,8 +314,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
likes: json["likeCount"].int,
dislikes: json["dislikeCount"].int,
keywords: json["keywords"].arrayValue.map { $0.stringValue },
streams: extractFormatStreams(from: json["formatStreams"].arrayValue) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
streams: extractStreams(from: json),
related: extractRelated(from: json)
)
}
@ -349,6 +349,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private static func extractStreams(from json: JSON) -> [Stream] {
extractFormatStreams(from: json["formatStreams"].arrayValue) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
}
private static func extractFormatStreams(from streams: [JSON]) -> [Stream] {
streams.map {
SingleAssetStream(
@ -378,4 +383,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
)
}
}
private static func extractRelated(from content: JSON) -> [Video] {
content
.dictionaryValue["recommendedVideos"]?
.arrayValue
.compactMap(extractVideo(from:)) ?? []
}
}

View File

@ -224,7 +224,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
thumbnails: thumbnails,
likes: details["likes"]?.int,
dislikes: details["dislikes"]?.int,
streams: extractStreams(from: content)
streams: extractStreams(from: content),
related: extractRelated(from: content)
)
}
@ -310,6 +311,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return streams
}
private static func extractRelated(from content: JSON) -> [Video] {
content
.dictionaryValue["relatedStreams"]?
.arrayValue
.compactMap(extractVideo(from:)) ?? []
}
private static func compatibleAudioStreams(from content: JSON) -> [JSON] {
content
.dictionaryValue["audioStreams"]?

View File

@ -30,6 +30,8 @@ struct Video: Identifiable, Equatable, Hashable {
var channel: Channel
var related = [Video]()
init(
id: String? = nil,
videoID: String,
@ -49,7 +51,8 @@ struct Video: Identifiable, Equatable, Hashable {
likes: Int? = nil,
dislikes: Int? = nil,
keywords: [String] = [],
streams: [Stream] = []
streams: [Stream] = [],
related: [Video] = []
) {
self.id = id ?? UUID().uuidString
self.videoID = videoID
@ -70,6 +73,7 @@ struct Video: Identifiable, Equatable, Hashable {
self.dislikes = dislikes
self.keywords = keywords
self.streams = streams
self.related = related
}
var publishedDate: String? {

View File

@ -82,6 +82,8 @@
372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
373197DA2732060100EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
@ -525,6 +527,7 @@
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
@ -783,6 +786,7 @@
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */,
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */,
373197D82732015300EF734F /* RelatedView.swift */,
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
@ -1743,6 +1747,7 @@
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
37599F30272B42810087F250 /* FavoriteItem.swift in Sources */,
373197D92732015300EF734F /* RelatedView.swift in Sources */,
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
@ -2011,6 +2016,7 @@
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
37D4B19926717E1500C925CA /* Video.swift in Sources */,
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,

View File

@ -10,12 +10,12 @@ struct PlayerQueueView: View {
List {
Group {
playingNext
related
playedPreviously
}
#if !os(iOS)
.padding(.vertical, 5)
.listRowInsets(EdgeInsets())
#if os(iOS)
.padding(.horizontal, 10)
#endif
}
@ -71,7 +71,27 @@ struct PlayerQueueView: View {
}
}
func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
private var related: some View {
Group {
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
Section(header: Text("Related")) {
ForEach(player.currentVideo!.related) { video in
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen)
.contextMenu {
Button("Play Next") {
player.playNext(video)
}
Button("Play Last") {
player.enqueueVideo(video)
}
}
}
}
}
}
}
private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
Button(role: .destructive) {
if history {
player.removeHistory(item)
@ -83,7 +103,7 @@ struct PlayerQueueView: View {
}
}
func removeAllButton(history: Bool) -> some View {
private func removeAllButton(history: Bool) -> some View {
Button(role: .destructive) {
if history {
player.removeHistoryItems()

View File

@ -32,23 +32,29 @@ final class PlayerViewController: UIViewController {
#if os(tvOS)
playerModel.avPlayerViewController = playerViewController
playerViewController.customInfoViewControllers = [playerQueueInfoViewController]
playerViewController.customInfoViewControllers = [
infoViewController([.related], title: "Related"),
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
]
#else
embedViewController()
#endif
}
#if os(tvOS)
var playerQueueInfoViewController: UIHostingController<AnyView> {
func infoViewController(
_ sections: [NowPlayingView.ViewSection],
title: String
) -> UIHostingController<AnyView> {
let controller = UIHostingController(rootView:
AnyView(
NowPlayingView(inInfoViewController: true)
NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600)
.environmentObject(playerModel)
)
)
controller.title = "Playing Next"
controller.title = title
return controller
}

View File

@ -0,0 +1,30 @@
import SwiftUI
struct RelatedView: View {
@EnvironmentObject<PlayerModel> private var player
var body: some View {
List {
if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
Section(header: Text("Related")) {
ForEach(player.currentVideo!.related) { video in
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false))
}
}
}
}
#if os(macOS)
.listStyle(.inset)
#elseif os(iOS)
.listStyle(.grouped)
#else
.listStyle(.plain)
#endif
}
}
struct RelatedView_Previews: PreviewProvider {
static var previews: some View {
RelatedView()
}
}

View File

@ -3,7 +3,7 @@ import SwiftUI
struct VideoDetails: View {
enum Page {
case details, queue
case details, queue, related
}
@Binding var sidebarQueue: Bool
@ -89,6 +89,14 @@ struct VideoDetails: View {
case .queue:
PlayerQueueView(fullScreen: $fullScreen)
.edgesIgnoringSafeArea(.horizontal)
case .related:
#if os(macOS)
EmptyView()
#else
RelatedView()
.edgesIgnoringSafeArea(.horizontal)
#endif
}
}
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
@ -109,7 +117,9 @@ struct VideoDetails: View {
.onChange(of: sidebarQueue) { queue in
#if !os(macOS)
if queue {
if currentPage == .queue {
currentPage = .details
}
} else {
currentPage = .queue
}
@ -216,6 +226,7 @@ struct VideoDetails: View {
var pagePicker: some View {
Picker("Page", selection: $currentPage) {
Text("Details").tag(Page.details)
Text("Related").tag(Page.related)
Text("Queue").tag(Page.queue)
}

View File

@ -62,8 +62,14 @@ struct VideoPlayerView: View {
if player.currentItem.isNil {
playerPlaceholder(geometry: geometry)
} else {
#if os(macOS)
Player()
.modifier(VideoPlayerSizeModifier(geometry: geometry))
#else
player.playerView
.modifier(VideoPlayerSizeModifier(geometry: geometry))
#endif
}
}
#if os(iOS)
@ -107,6 +113,11 @@ struct VideoPlayerView: View {
.frame(minWidth: 250)
#endif
}
.onDisappear {
if !player.playingInPictureInPicture {
player.pause()
}
}
}
func playerPlaceholder(geometry: GeometryProxy) -> some View {

View File

@ -21,6 +21,7 @@ struct ShareButton: View {
var body: some View {
Menu {
instanceActions
Divider()
youtubeActions
} label: {
Label("Share", systemImage: "square.and.arrow.up")

View File

@ -9,20 +9,26 @@ final class PictureInPictureDelegate: NSObject, AVPlayerViewPictureInPictureDele
}
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
playerModel.playingInPictureInPicture = true
playerModel.presentingPlayer = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.playerModel.playingInPictureInPicture = true
self?.playerModel.presentingPlayer = false
}
}
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
playerModel.playingInPictureInPicture = false
playerModel.presentPlayer()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.playerModel.playingInPictureInPicture = false
self?.playerModel.presentPlayer()
}
}
func playerView(
_: AVPlayerView,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
) {
playerModel.presentingPlayer = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.playerModel.presentingPlayer = true
}
completionHandler(true)
}
}

View File

@ -7,9 +7,6 @@ final class PlayerViewController: NSViewController {
var pictureInPictureDelegate = PictureInPictureDelegate()
override func viewDidDisappear() {
if !playerModel.playingInPictureInPicture {
playerModel.pause()
}
super.viewDidDisappear()
}

View File

@ -1,6 +1,11 @@
import SwiftUI
struct NowPlayingView: View {
enum ViewSection: CaseIterable {
case nowPlaying, playingNext, playedPreviously, related
}
var sections = ViewSection.allCases
var inInfoViewController = false
@EnvironmentObject<PlayerModel> private var player
@ -18,7 +23,7 @@ struct NowPlayingView: View {
var content: some View {
List {
Group {
if !inInfoViewController, let item = player.currentItem {
if sections.contains(.nowPlaying), let item = player.currentItem {
Section(header: Text("Now Playing")) {
Button {
player.presentPlayer()
@ -29,6 +34,7 @@ struct NowPlayingView: View {
.onPlayPauseCommand(perform: player.togglePlay)
}
if sections.contains(.playingNext) {
Section(header: Text("Playing Next")) {
if player.queue.isEmpty {
Text("Playback queue is empty")
@ -50,8 +56,31 @@ struct NowPlayingView: View {
}
}
}
}
if !player.history.isEmpty {
if sections.contains(.related), !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty {
Section(header: inInfoViewController ? AnyView(EmptyView()) : AnyView(Text("Related"))) {
ForEach(player.currentVideo!.related) { video in
Button {
player.playNow(video)
player.presentPlayer()
} label: {
VideoBanner(video: video)
}
.contextMenu {
Button("Play Next") {
player.playNext(video)
}
Button("Play Last") {
player.enqueueVideo(video)
}
Button("Cancel", role: .cancel) {}
}
}
}
}
if sections.contains(.playedPreviously), !player.history.isEmpty {
Section(header: Text("Played Previously")) {
ForEach(player.history) { item in
Button {
@ -75,7 +104,6 @@ struct NowPlayingView: View {
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
.padding(.vertical, 20)
// .padding(.horizontal, 40)
}
.padding(.horizontal, inInfoViewController ? 40 : 0)
.listStyle(.grouped)
@ -86,7 +114,6 @@ struct NowPlayingView: View {
Text(text)
.font((inInfoViewController ? Font.system(size: 40) : .title3).bold())
.foregroundColor(.secondary)
// .padding(.leading, 40)
}
}