Open more YouTube link types and playlists from links

Route YouTube search results links to search and nocookie embeds to the
video, match YouTube hosts exactly, and report YouTube pages without a
video as unsupported instead of sending them to yt-dlp. A watch link
inside a playlist shows both the video and the playlist when loaded, and
plays the video with the playlist queued around it. URL scheme play,
queue and bookmark actions accept playlist links.
This commit is contained in:
Arkadiusz Fal
2026-10-08 22:50:36 +02:00
parent 8a7218ed1b
commit 45e19e6536
10 changed files with 388 additions and 60 deletions

View File

@@ -37,11 +37,25 @@ final class OpenedLinksViewModel {
private static let maxConcurrentResolutions = 4
init(urls: [URL]) {
// A watch link inside a playlist (`watch?v=X&list=Y`) shows both the
// video and the playlist
let router = URLRouter()
let expanded = urls.flatMap { url -> [URL] in
guard let listID = router.youTubePlaylistContext(url),
let playlistURL = Self.youTubePlaylistURL(listID) else { return [url] }
return [url, playlistURL]
}
var seen = Set<String>()
let unique = urls.filter { seen.insert($0.absoluteString).inserted }
let unique = expanded.filter { seen.insert($0.absoluteString).inserted }
links = unique.enumerated().map { OpenedLink(id: $0.offset, url: $0.element) }
}
private static func youTubePlaylistURL(_ listID: String) -> URL? {
var components = URLComponents(string: "https://www.youtube.com/playlist")
components?.queryItems = [URLQueryItem(name: "list", value: listID)]
return components?.url
}
/// Resolved videos, in link order.
var videos: [Video] {
links.compactMap { link in
@@ -111,13 +125,9 @@ final class OpenedLinksViewModel {
case .video(.loaded(let video), _):
return .video(video)
case .playlist(.remote(let playlistID, let instance, _, _)):
guard let instance = instance
?? playlistID.source.flatMap({ appEnvironment.instancesManager.playlistInstance(for: $0) }) else {
return .unresolved(destination, message: String(localized: "openLink.noInstance"))
}
case .playlist(let source):
do {
let playlist = try await appEnvironment.contentService.playlist(id: playlistID.playlistID, instance: instance)
let playlist = try await LinkVideoResolver(appEnvironment: appEnvironment).playlist(for: source)
return .playlist(playlist, destination: destination)
} catch {
return .unresolved(destination, message: error.localizedDescription)