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:
2026-10-22 18:28:49 +02:00
parent 98e6dcb3c5
commit 4576080955
8 changed files with 287 additions and 85 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ struct AutomaticQualityPlaybackChecks {
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let clock = FixedQualityClock()
let model = PlayerModel(defaults: defaults, qualityClock: { clock.now })
let model = PlayerModel(defaults: defaults, youtubeTitleResolver: { _ in nil }, qualityClock: { clock.now })
defer { model.shutdown() }
model.setVolume(0)
// Control only the external extractor; media decoding and all model transitions stay real.
+67 -55
View File
@@ -117,28 +117,31 @@ struct ResolvedPlaybackCheck {
let suite = "terebi.queue-metadata-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let urls = ["AAAAAAAAAAA", "BBBBBBBBBBB", "CCCCCCCCCCC", "DDDDDDDDDDD"].map {
let urls = ["AAAAAAAAAAA", "BBBBBBBBBBB", "CCCCCCCCCCC"].map {
URL(string: "https://www.youtube.com/watch?v=\($0)")!
}
} + [URL(string: "https://video.example/watch/fixture")!]
let episode = URL(string: "https://barelystarted.miruro.tv/watch/fixture-ID/episode?ep=1")!
var foreground: CheckedContinuation<YouTubeVideo, Never>?
var background: CheckedContinuation<YouTubeVideo, Never>?
var background: CheckedContinuation<String?, Never>?
var holdFirst = true
var episodeRequests = 0
func video(_ url: URL, title: String) -> YouTubeVideo {
var titleRequests: [URL] = []
func video(_ url: URL, title: String, height: Int = 1080) -> YouTubeVideo {
YouTubeVideo(title: title, video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
duration: 0, isLive: false, webpageURL: url, height: height, availableHeights: [1080, 720])
}
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, _ in
if url == urls[0], holdFirst {
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, height in
if holdFirst || height != nil {
holdFirst = false
return await withCheckedContinuation { foreground = $0 }
}
if url == urls[1], Task.currentPriority <= .utility {
return await withCheckedContinuation { background = $0 }
}
if url == urls[2] { throw CocoaError(.fileReadCorruptFile) }
return video(url, title: url == urls[0] ? "Playing title" : "Fresh playback title")
}, youtubeTitleResolver: { url in
require(Task.currentPriority <= .utility, "title-only lookups use lower priority")
titleRequests.append(url)
if url == urls[1] { return await withCheckedContinuation { background = $0 } }
if url == urls[2] { throw CocoaError(.fileReadCorruptFile) }
return url == urls[0] ? "Early current title" : "Generic page title"
}, miruroResolver: { _, _ in
fatalError("FAIL: sidebar enrichment must not fetch Miruro playback sources")
}, miruroMetadataResolver: { _, _ in
@@ -154,68 +157,79 @@ struct ResolvedPlaybackCheck {
})
}
model.openStreamQueue(queue([urls[0], urls[1], episode, urls[2], urls[3]]))
try await until("foreground extraction starts first", model: model) { foreground != nil }
require(background == nil && episodeRequests == 0, "sidebar lookups yield to selected extraction")
foreground!.resume(returning: video(urls[0], title: "Playing title"))
foreground = nil
try await until("native playback and background extraction start", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && background != nil
try await until("early current title and next title request while full extraction is held", model: model) {
foreground != nil && background != nil && model.queue.currentItem?.displayName == "Early current title"
}
require(episodeRequests == 0, "sidebar lookups are serial")
require(model.setPlayback(playing: false), "pause for background title resolution")
try await until("metadata smoke pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
require(model.resolvingStream && model.fileName == nil && episodeRequests == 0 &&
titleRequests == [urls[0], urls[1]], "serial sidebar work is independent of foreground extraction")
let selected = model.queue.currentID
let position = model.state.position
let pendingID = model.queue.items[1].id
model.moveQueueItems(fromOffsets: IndexSet(integer: 1), toOffset: 5)
background!.resume(returning: video(urls[1], title: "Queued title"))
background!.resume(returning: "Queued title")
background = nil
let deadline = ContinuousClock.now.advanced(by: .seconds(2))
while model.queue.items.last?.displayName != "Queued title", ContinuousClock.now < deadline {
try await Task.sleep(for: .milliseconds(10))
}
require(model.queue.items.last?.id == pendingID && model.queue.items.last?.displayName == "Queued title",
"unplayed queue entry resolves by identity after reorder")
try await until("mixed queue metadata continues after failure", model: model) {
model.queue.items.first { $0.url == episode }?.displayName == "Queued episode" &&
model.queue.items.first { $0.url == urls[3] }?.displayName == "Fresh playback title"
try await until("mixed queue titles continue before foreground extraction finishes", model: model) {
model.queue.items.last?.displayName == "Queued title" &&
model.queue.items.first { $0.url == episode }?.displayName == "Queued episode" &&
model.queue.items.first { $0.url == urls[3] }?.displayName == "Generic page title"
}
require(model.queue.items.last?.id == pendingID && model.queue.currentID == selected &&
model.resolvingStream && model.fileName == nil, "reordered metadata updates only its original row")
require(model.queue.items.first { $0.url == urls[2] }?.displayName == urls[2].absoluteString &&
model.commandError == nil, "failed enrichment preserves fallback and does not block later titles")
require(model.queue.currentID == selected &&
model.state.position == position && model.state.status == PLAYER_PAUSED &&
model.fileName == "Playing title", "background metadata preserves selection, position, paused state and current title")
model.queue.items.first { $0.url == urls[2] }?.metadataAttempted == true &&
model.commandError == nil, "failed title preserves fallback and does not block later titles")
foreground!.resume(returning: video(urls[0], title: "Playing title"))
foreground = nil
try await until("foreground title takes precedence and native playback starts", model: model) {
model.state.status == PLAYER_PLAYING && model.queue.currentItem?.displayName == "Playing title"
}
require(model.setPlayback(playing: false), "pause for metadata preservation")
try await until("metadata smoke pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
let position = model.state.position
for _ in 0..<10 { await Task.yield() }
require(model.queue.currentID == selected && model.state.position == position &&
model.fileName == "Playing title", "title-only metadata preserves native selection, position and current title")
model.openStreamQueue(queue([urls[0], urls[1]]))
try await until("next background lookup starts", model: model) { background != nil }
let cancelled = background!
try await until("next title-only lookup starts", model: model) { background != nil }
let selectedLookup = background!
background = nil
model.selectQueueItem(model.queue.items[1].id)
try await until("selection preempts pending metadata with fresh playback extraction", model: model) {
try await until("selection resolves fresh playback independently", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == "Fresh playback title"
}
cancelled.resume(returning: video(urls[1], title: "Stale title"))
selectedLookup.resume(returning: "Stale title")
for _ in 0..<10 { await Task.yield() }
require(model.queue.currentItem?.displayName == "Fresh playback title" &&
model.fileName == "Fresh playback title", "cancelled metadata cannot overwrite selected title")
model.fileName == "Fresh playback title", "late title-only result cannot overwrite foreground metadata")
model.openStreamQueue(queue([urls[0], urls[1]]))
try await until("removable background lookup starts", model: model) { background != nil }
try await until("removable title-only lookup starts", model: model) { background != nil }
let removed = background!
background = nil
model.removeQueueItem(model.queue.items[1].id)
model.openStreamQueue(queue([urls[0], urls[1]]))
try await until("replacement background lookup starts", model: model) { background != nil }
removed.resume(returning: video(urls[1], title: "Removed title"))
try await until("replacement title-only lookup starts", model: model) { background != nil }
removed.resume(returning: "Removed title")
for _ in 0..<10 { await Task.yield() }
require(model.queue.items[1].displayName == urls[1].absoluteString,
"removed/replaced item cannot rename a new row for the same URL")
background!.resume(returning: video(urls[1], title: "Replacement title"))
require(model.setPlayback(playing: false), "pause before quality extraction")
try await until("quality smoke pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
model.setYouTubeQuality(720)
try await until("quality extraction is held", model: model) { foreground != nil }
background!.resume(returning: "Replacement title")
background = nil
try await until("replacement result updates its own row", model: model) {
try await until("title arrives while quality extraction is held", model: model) {
model.queue.items[1].displayName == "Replacement title"
}
print("PASS: background mixed-stream titles, utility priority, serial lookup, reorder, failure, playback preemption and stale removal/replacement")
require(model.resolvingStream && model.fileName == "Playing title" &&
model.queue.currentItem?.displayName == "Playing title", "title work does not replace current quality metadata")
foreground!.resume(returning: video(urls[0], title: "Quality title", height: 720))
foreground = nil
try await until("quality extraction resumes paused native playback", model: model) {
!model.isBusy && model.state.status == PLAYER_PAUSED && model.fileName == "Quality title"
}
print("PASS: early current/queued titles during held playback and quality extraction; serial utility lookup, generic/Miruro titles, reorder, failure and stale replacement/foreground precedence")
}
static func streamQueueLifecycle(source: ResolvedSource) async throws {
@@ -242,14 +256,12 @@ struct ResolvedPlaybackCheck {
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: youtubeURL, height: nil, availableHeights: [])
let model = PlayerModel(defaults: defaults, streamQueue: queue, youtubeResolver: { _, _ in
if Task.currentPriority > .utility {
youtubeRequests += 1
if youtubeRequests == 1 {
return await withCheckedContinuation { stale = $0 }
}
youtubeRequests += 1
if youtubeRequests == 1 {
return await withCheckedContinuation { stale = $0 }
}
return video
}, miruroResolver: { _, _ in
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in
episodeRequests += 1
return source
}, miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
@@ -349,7 +361,7 @@ struct ResolvedPlaybackCheck {
return YouTubeVideo(title: url.absoluteString,
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
}, miruroResolver: { _, _ in episodeRequests += 1; return source },
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in episodeRequests += 1; return source },
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
defer { model.shutdown() }
func deliver(_ items: [(URL, URL)]) throws {
@@ -468,7 +480,7 @@ struct ResolvedPlaybackCheck {
let empty = PlayerModel(defaults: defaults, youtubeResolver: { url, _ in
YouTubeVideo(title: "Cold enqueue", video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
})
}, youtubeTitleResolver: { _ in nil })
defer { empty.shutdown() }
let coldReport = directory.appendingPathComponent("cold.jsonl")
empty.enqueueStreamQueue(StreamQueue(entries: [.init(url: fourth, identity: fourth.absoluteString,
@@ -498,7 +510,7 @@ struct ResolvedPlaybackCheck {
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
if url == cancelledURL { return await withCheckedContinuation { pending = $0 } }
return video
}, miruroResolver: { _, _ in source },
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in source },
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
defer { model.shutdown() }
try await model.addStreamToQueue(first.absoluteString)
+110 -3
View File
@@ -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")
}