fix: populate queue titles independently of playback extraction
The sidebar scheduler waited for playback and used full stream extraction for titles. Use independent utility-priority title requests, native YouTube oEmbed, and metadata-only extraction for generic sites. Preserve foreground title precedence and stale row protection. Verified: make check-youtube check-queue check-resolved check-adaptive check-stream all.
This commit is contained in:
@@ -309,13 +309,109 @@ struct YouTubeResolverChecks {
|
||||
throw Failure(description: "FAIL: extractor did not reach \(name) within the isolated check deadline")
|
||||
}
|
||||
|
||||
static func cancelled(_ task: Task<YouTubeVideo, Error>, _ message: String) async throws {
|
||||
static func cancelled<T: Sendable>(_ task: Task<T, Error>, _ message: String) async throws {
|
||||
do { _ = try await task.value }
|
||||
catch is CancellationError { return }
|
||||
catch { throw Failure(description: "FAIL: \(message) returned an extraction error instead of cancellation") }
|
||||
throw Failure(description: "FAIL: \(message) returned media instead of cancellation")
|
||||
}
|
||||
|
||||
final class TitleProtocol: URLProtocol, @unchecked Sendable {
|
||||
static let stalled = AsyncStream<Void>.makeStream()
|
||||
override class func canInit(with request: URLRequest) -> Bool { true }
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
||||
override func startLoading() {
|
||||
let parts = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)!
|
||||
precondition(parts.scheme == "https" && parts.host == "www.youtube.com" && parts.path == "/oembed")
|
||||
precondition(request.value(forHTTPHeaderField: "Cookie") == nil)
|
||||
let page = parts.queryItems!.first { $0.name == "url" }!.value!
|
||||
precondition(parts.queryItems!.first { $0.name == "format" }!.value == "json")
|
||||
let id = URLComponents(string: page)!.queryItems!.first!.value!
|
||||
if id == "CancelledAA" {
|
||||
Self.stalled.continuation.yield()
|
||||
return
|
||||
}
|
||||
let status = id == "Unavailable" ? 404 : 200
|
||||
let mime = id == "WrongMimeAA" ? "text/html" : "application/json"
|
||||
let oversized = ["OversizedAA", "ChunkedAAAA"].contains(id)
|
||||
let body = oversized ? Data(repeating: 32, count: 65537) :
|
||||
Data((id == "MalformedAA" ? "not JSON" : #"{"title":" Early YouTube title "}"#).utf8)
|
||||
var headers = ["Content-Type": mime]
|
||||
if id != "ChunkedAAAA" { headers["Content-Length"] = "\(body.count)" }
|
||||
client!.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: status,
|
||||
httpVersion: nil, headerFields: headers)!,
|
||||
cacheStoragePolicy: .notAllowed)
|
||||
client!.urlProtocol(self, didLoad: body)
|
||||
client!.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
override func stopLoading() {}
|
||||
}
|
||||
|
||||
static func titles(root: URL) async throws {
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
configuration.protocolClasses = [TitleProtocol.self]
|
||||
let youtube = try await YouTubeResolver.resolveTitle(URL(string: "https://youtu.be/jNQXAC9IVRw")!,
|
||||
executableURL: URL(fileURLWithPath: "/missing-helper"), configuration: configuration)
|
||||
try require(youtube == "Early YouTube title", "native YouTube oEmbed needs no extractor, runtime or playable formats")
|
||||
for id in ["Unavailable", "WrongMimeAA", "OversizedAA", "ChunkedAAAA", "MalformedAA"] {
|
||||
do {
|
||||
_ = try await YouTubeResolver.resolveTitle(URL(string: "https://youtu.be/\(id)")!, configuration: configuration)
|
||||
throw Failure(description: "FAIL: invalid oEmbed response was accepted: \(id)")
|
||||
} catch let failure as Failure { throw failure }
|
||||
catch {}
|
||||
}
|
||||
let native = Task {
|
||||
try await YouTubeResolver.resolveTitle(URL(string: "https://youtu.be/CancelledAA")!, configuration: configuration)
|
||||
}
|
||||
var started = TitleProtocol.stalled.stream.makeAsyncIterator()
|
||||
_ = await started.next()
|
||||
native.cancel()
|
||||
do {
|
||||
_ = try await native.value
|
||||
throw Failure(description: "FAIL: cancelled oEmbed request returned a title")
|
||||
} catch let failure as Failure { throw failure }
|
||||
catch {
|
||||
try require(error is CancellationError || (error as? URLError)?.code == .cancelled,
|
||||
"native oEmbed cancellation ends its request without a title")
|
||||
}
|
||||
try require(try YouTubeResolver.decodeTitle(Data(#"{"title":" Metadata only ","formats":"ignored"}"#.utf8)) == "Metadata only",
|
||||
"title decoding does not require or interpret playback formats")
|
||||
try require(try YouTubeResolver.decodeTitle(Data(#"{"title":" "}"#.utf8)) == nil, "empty title retains fallback")
|
||||
for json in [#"{"_type":"playlist","title":"Collection"}"#, #"{"title":"bad\u0000title"}"#, #"{"title":42}"#] {
|
||||
try reject("unsafe, collection or malformed title") { _ = try YouTubeResolver.decodeTitle(Data(json.utf8)) }
|
||||
}
|
||||
let generic = URL(string: "https://video.example/watch?id=123")!
|
||||
let extractor = try Extractor(root: root, name: "title only", json: #"{"title":"Generic title"}"#, body: #"""
|
||||
printf '%s\n' "$@" > "$base/arguments"
|
||||
/bin/cat "$base/response.json"
|
||||
"""#)
|
||||
let title = try await YouTubeResolver.resolveTitle(generic, executableURL: extractor.executable)
|
||||
let arguments = try String(contentsOf: extractor.marker("arguments"), encoding: .utf8)
|
||||
.split(separator: "\n").map(String.init)
|
||||
try require(title == "Generic title" && arguments.suffix(2) == ["--", generic.absoluteString] &&
|
||||
arguments.contains("--ignore-config") && arguments.contains("--no-plugin-dirs") &&
|
||||
arguments.contains("--no-remote-components") && arguments.contains("--flat-playlist") &&
|
||||
arguments.contains("--skip-download") && arguments.contains("--ignore-no-formats-error") &&
|
||||
arguments.contains("--no-js-runtimes") && !arguments.contains("--js-runtimes") &&
|
||||
!arguments.contains("--format"), "generic title extraction is isolated from full playback format/runtime policy")
|
||||
do {
|
||||
_ = try await YouTubeResolver.resolveTitle(URL(string: "https://user:password@video.example/")!,
|
||||
executableURL: extractor.executable)
|
||||
throw Failure(description: "FAIL: title lookup accepted embedded credentials")
|
||||
} catch let failure as Failure { throw failure }
|
||||
catch {}
|
||||
let waiting = try Extractor(root: root, name: "cancel title", body: #"""
|
||||
trap '' TERM INT
|
||||
: > "$base/ready"
|
||||
while [ ! -f "$base/release" ]; do /bin/sleep 0.01; done
|
||||
/bin/cat "$base/response.json"
|
||||
"""#)
|
||||
let task = Task { try await YouTubeResolver.resolveTitle(generic, executableURL: waiting.executable) }
|
||||
try await awaitMarker(waiting, "ready")
|
||||
task.cancel()
|
||||
try await cancelled(task, "title-only subprocess cancellation")
|
||||
}
|
||||
|
||||
static func subprocesses(root: URL, runtime: URL) async throws {
|
||||
let successful = try Extractor(root: root, name: "successful split")
|
||||
let result = try await YouTubeResolver.resolve(page, executableURL: successful.executable, runtimeURL: runtime)
|
||||
@@ -458,6 +554,12 @@ struct YouTubeResolverChecks {
|
||||
try require(unknown.video.url == "https://media.example/unknown.mp4" &&
|
||||
unknown.audio == nil && unknown.height == nil && unknown.webpageURL == genericPage,
|
||||
"generic HTML5 video without codec or height hints remains playable and retains page identity")
|
||||
let metadataOnly: [String: Any] = ["id": "123", "title": "Title without playable formats",
|
||||
"webpage_url": genericPage.absoluteString, "extractor": "generic", "extractor_key": "Generic",
|
||||
"formats": []]
|
||||
try JSONSerialization.data(withJSONObject: metadataOnly).write(to: extractor.marker("response.json"))
|
||||
let title = try await YouTubeResolver.resolveTitle(genericPage, executableURL: extractor.executable)
|
||||
try require(title == "Title without playable formats", "bundled yt-dlp returns title metadata without playable formats or runtime")
|
||||
}
|
||||
|
||||
static func bundledHelpers(root: URL) throws {
|
||||
@@ -493,9 +595,9 @@ struct YouTubeResolverChecks {
|
||||
}
|
||||
|
||||
static func main() async throws {
|
||||
// A broken pipe/cancellation implementation must fail, not leave this check hanging.
|
||||
// Includes five cold starts of the bundled extractor against offline metadata.
|
||||
let watchdog = Task {
|
||||
try await Task.sleep(for: .seconds(30))
|
||||
try await Task.sleep(for: .seconds(45))
|
||||
fatalError("FAIL: resolver checks exceeded their bounded subprocess deadline")
|
||||
}
|
||||
defer { watchdog.cancel() }
|
||||
@@ -511,6 +613,11 @@ struct YouTubeResolverChecks {
|
||||
try qualities()
|
||||
try bundledHelpers(root: root)
|
||||
try await subprocesses(root: root, runtime: runtime)
|
||||
print("PASS: extractor subprocess isolation and cancellation")
|
||||
fflush(stdout)
|
||||
try await titles(root: root)
|
||||
print("PASS: independent native YouTube and generic title resolution")
|
||||
fflush(stdout)
|
||||
try await actualQualitySelection(root: root, runtime: runtime)
|
||||
print("YouTube resolver checks passed")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user