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

@@ -647,7 +647,8 @@ struct OpenLinkFormView: View {
if case .externalVideo = destination { return true }
return false
}
return true
// YouTube pages that route nowhere are unsupported, not external
return !router.isYouTubeHost(url.host)
}
}
@@ -657,7 +658,8 @@ struct OpenLinkFormView: View {
let router = URLRouter()
return parsedURLs.allSatisfy { url in
switch router.route(url) {
case .video, .externalVideo, .directMedia, nil: return true
case .video, .externalVideo, .directMedia: return true
case nil: return !router.isYouTubeHost(url.host)
default: return false
}
}
@@ -704,7 +706,8 @@ struct OpenLinkFormView: View {
private func loadURLs() async {
let urls = parsedURLs
guard let appEnvironment, !urls.isEmpty else { return }
if urls.count > 1 {
// A watch link inside a playlist shows both, like several links
if urls.count > 1 || URLRouter().youTubePlaylistContext(urls[0]) != nil {
extractedItems = []
dismissIfRequested()
appEnvironment.navigationCoordinator.navigateToOpenedLink(.openedLinks(urls))
@@ -764,7 +767,7 @@ struct OpenLinkFormView: View {
if !firstVideoPlayed {
// Play first video - this expands player
playVideo(video, sourceURL: url, appEnvironment: appEnvironment)
await playVideo(video, sourceURL: url, appEnvironment: appEnvironment)
firstVideoPlayed = true
} else {
// Add to queue
@@ -805,13 +808,16 @@ struct OpenLinkFormView: View {
}
}
private func playVideo(_ video: Video, sourceURL: URL, appEnvironment: AppEnvironment) {
private func playVideo(_ video: Video, sourceURL: URL, appEnvironment: AppEnvironment) async {
// Don't pass a specific stream - let the player's selectStreamAndBackend
// choose the best video+audio combination. Using streams.first would
// incorrectly select audio-only streams for sites like Bilibili.
let router = URLRouter()
let startTime = router.parseTimestamp(router.unwrapped(sourceURL))
appEnvironment.playerService.openVideo(video, startTime: startTime, forceStartTime: startTime != nil)
// A watch link inside a playlist queues the playlist around it.
// Awaited, so later links queue after the playlist instead of being
// cleared by it.
await LinkVideoResolver(appEnvironment: appEnvironment).play(video, from: sourceURL, startTime: startTime)
}
// MARK: - Download Action

View File

@@ -1378,22 +1378,6 @@ struct UnifiedPlaylistDetailView: View {
}
}
/// Generates a unique playlist name by appending (2), (3), etc. if needed.
private func generateUniqueName(_ baseName: String, existingTitles: Set<String>) -> String {
if !existingTitles.contains(baseName) {
return baseName
}
var counter = 2
while true {
let candidateName = "\(baseName) (\(counter))"
if !existingTitles.contains(candidateName) {
return candidateName
}
counter += 1
}
}
/// Imports the remote playlist to local storage.
private func importToLocal() async {
guard let appEnvironment, let remotePlaylist else { return }
@@ -1402,8 +1386,7 @@ struct UnifiedPlaylistDetailView: View {
importProgress = nil
// Generate unique name
let existingTitles = Set(appEnvironment.dataManager.playlists().map(\.title))
let uniqueName = generateUniqueName(remotePlaylist.title, existingTitles: existingTitles)
let uniqueName = appEnvironment.dataManager.uniquePlaylistTitle(remotePlaylist.title)
// Create local playlist
let newLocalPlaylist = appEnvironment.dataManager.createPlaylist(