feat: choose YouTube quality with a 1080p default

This commit is contained in:
2026-10-11 02:38:06 +02:00
parent 5f0287f489
commit 4d4c3e6b0a
4 changed files with 193 additions and 21 deletions
+36 -5
View File
@@ -14,6 +14,8 @@ struct YouTubeVideo: Sendable {
let duration: Double
let isLive: Bool
let webpageURL: URL
let height: Int?
let availableHeights: [Int]
}
private struct YouTubeResolutionError: LocalizedError {
@@ -58,7 +60,7 @@ enum YouTubeResolver {
return URL(string: "https://www.youtube.com/watch?v=\(id)")!
}
static func decode(_ data: Data, webpageURL: URL) throws -> YouTubeVideo {
static func decode(_ data: Data, webpageURL: URL, height: Int? = nil) throws -> YouTubeVideo {
guard data.count <= outputLimit else {
throw YouTubeResolutionError(message: "yt-dlp returned too much metadata for one video.")
}
@@ -102,6 +104,27 @@ enum YouTubeResolver {
guard videoFormat.dynamic_range == nil || videoFormat.dynamic_range?.uppercased() == "SDR" else {
throw YouTubeResolutionError(message: "This video has no supported SDR stream. HDR playback is not supported.")
}
guard videoFormat.height == nil || videoFormat.height! > 0 else {
throw YouTubeResolutionError(message: "yt-dlp returned an invalid video resolution.")
}
if let height {
guard height > 0, videoFormat.height == height else {
throw YouTubeResolutionError(message: "The requested \(height)p resolution is no longer available. Choose another quality.")
}
} else if let selectedHeight = videoFormat.height, selectedHeight > 1080 {
throw YouTubeResolutionError(message: "YouTube did not provide a supported stream at or below 1080p.")
}
let formats = metadata.formats ?? [videoFormat] + (audioFormat.map { [$0] } ?? [])
let hasSeparateAudio = formats.contains {
$0.hasAudio && !$0.hasVideo && (try? stream($0, fallbackHeaders: metadata.http_headers)) != nil
}
let availableHeights = Set(formats.compactMap { format -> Int? in
guard format.hasVideo, format.hasAudio || hasSeparateAudio,
format.dynamic_range == nil || format.dynamic_range?.uppercased() == "SDR",
let height = format.height, height > 0,
(try? stream(format, fallbackHeaders: metadata.http_headers)) != nil else { return nil }
return height
}).sorted(by: >)
let video = try stream(videoFormat, fallbackHeaders: metadata.http_headers)
let audio = try audioFormat.map { try stream($0, fallbackHeaders: metadata.http_headers) }
let title = metadata.title?.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -109,13 +132,17 @@ enum YouTubeResolver {
title: title?.isEmpty == false ? title! : "YouTube video",
video: video, audio: audio, duration: duration,
isLive: metadata.is_live == true || metadata.live_status == "is_live",
webpageURL: try validateURL(webpageURL.absoluteString)
webpageURL: try validateURL(webpageURL.absoluteString),
height: videoFormat.height, availableHeights: availableHeights
)
}
static func resolve(_ url: URL, executableURL: URL? = nil, runtimeURL: URL? = nil) async throws -> YouTubeVideo {
static func resolve(_ url: URL, height: Int? = nil, executableURL: URL? = nil, runtimeURL: URL? = nil) async throws -> YouTubeVideo {
try Task.checkCancellation()
let webpageURL = try validateURL(url.absoluteString)
guard height == nil || height! > 0 else {
throw YouTubeResolutionError(message: "Choose a positive video resolution.")
}
let directories = searchDirectories()
let executable = try executableURL.map { try executableFile($0, name: "yt-dlp") }
?? findExecutable("yt-dlp", directories: directories)
@@ -135,13 +162,15 @@ enum YouTubeResolver {
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = directories.joined(separator: ":")
let transport = "[protocol~='^(https?|m3u8(_native)?)$']"
let resolution = height.map { "[height=\($0)]" } ?? "[height<=1080]"
let arguments = [
"--ignore-config", "--no-plugin-dirs", "--no-cache-dir", "--no-remote-components",
"--no-playlist", "--simulate", "--skip-download", "--dump-single-json",
"--no-progress", "--color", "never", "--no-check-formats",
"--no-js-runtimes", "--js-runtimes", "\(runtimeName):\(runtime.path)",
"--socket-timeout", "20", "--extractor-retries", "0", "--retries", "0",
"--format", "bestvideo[dynamic_range=?SDR]\(transport)+bestaudio\(transport)/best[dynamic_range=?SDR]\(transport)",
"--format-sort", "res", "--format-sort-force",
"--format", "bestvideo*[dynamic_range=?SDR]\(resolution)\(transport)+bestaudio\(transport)/best[dynamic_range=?SDR]\(resolution)\(transport)",
"--", webpageURL.absoluteString
]
let output = try await YouTubeExtraction(executable: executable, arguments: arguments, environment: environment).run()
@@ -149,7 +178,7 @@ enum YouTubeResolver {
if let error = dependencyError(String(decoding: output.stderr, as: UTF8.self).lowercased()) {
throw error
}
let video = try decode(output.stdout, webpageURL: webpageURL)
let video = try decode(output.stdout, webpageURL: webpageURL, height: height)
try Task.checkCancellation()
return video
}
@@ -250,6 +279,8 @@ private struct YouTubeMetadata: Decodable {
let vcodec: String?
let acodec: String?
let dynamic_range: String?
let height: Int?
let formats: [YouTubeMetadata]?
let http_headers: [String: String]?
let requested_formats: [YouTubeMetadata]?