fix: resolve queued stream metadata in the background
Queue labels were only enriched through selected-entry playback. Resolve unplayed titles serially at utility priority, preempt enrichment for playback, and bind updates to queue item IDs. Verified: make check-resolved check-queue check-youtube all.
This commit is contained in:
@@ -50,6 +50,12 @@ struct ResolvedPlaybackCheck {
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let clock = Clock()
|
||||
let model = PlayerModel(defaults: defaults, progressFile: CommandLine.arguments.count > 2 ? CommandLine.arguments[2] : nil,
|
||||
miruroMetadataResolver: { id, episode in
|
||||
if input.hasPrefix("https://") {
|
||||
return try await MiruroMetadataResolver.resolve(mediaID: id, episode: episode)
|
||||
}
|
||||
return MiruroMetadata(title: nil, intervals: [])
|
||||
},
|
||||
skipClock: { clock.now })
|
||||
defer { model.shutdown() }
|
||||
if input.hasPrefix("https://") { model.openStream(input) }
|
||||
@@ -100,12 +106,118 @@ struct ResolvedPlaybackCheck {
|
||||
if !input.hasPrefix("https://") { try await segmentSkips(model, clock: clock, file: file) }
|
||||
if !input.hasPrefix("https://") {
|
||||
model.shutdown()
|
||||
try await queueMetadataLifecycle(source: source)
|
||||
try await streamQueueLifecycle(source: source)
|
||||
try await enqueueLifecycle(source: source)
|
||||
try await sidebarQueueLifecycle(source: source)
|
||||
}
|
||||
}
|
||||
|
||||
static func queueMetadataLifecycle(source: ResolvedSource) async throws {
|
||||
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 {
|
||||
URL(string: "https://www.youtube.com/watch?v=\($0)")!
|
||||
}
|
||||
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 holdFirst = true
|
||||
var episodeRequests = 0
|
||||
func video(_ url: URL, title: String) -> YouTubeVideo {
|
||||
YouTubeVideo(title: title, video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
}
|
||||
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, _ in
|
||||
if url == urls[0], holdFirst {
|
||||
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")
|
||||
}, miruroResolver: { _, _ in
|
||||
fatalError("FAIL: sidebar enrichment must not fetch Miruro playback sources")
|
||||
}, miruroMetadataResolver: { _, _ in
|
||||
episodeRequests += 1
|
||||
return MiruroMetadata(title: "Queued episode", intervals: [])
|
||||
})
|
||||
defer { model.shutdown() }
|
||||
func queue(_ entries: [URL]) -> StreamQueue {
|
||||
StreamQueue(entries: entries.map { url in
|
||||
.init(url: url, identity: url == episode ? source.identity : url.absoluteString,
|
||||
progressFile: FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString),
|
||||
label: url.absoluteString)
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
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 }
|
||||
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 = 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"
|
||||
}
|
||||
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.openStreamQueue(queue([urls[0], urls[1]]))
|
||||
try await until("next background lookup starts", model: model) { background != nil }
|
||||
let cancelled = background!
|
||||
background = nil
|
||||
model.selectQueueItem(model.queue.items[1].id)
|
||||
try await until("selection preempts pending metadata with fresh playback extraction", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == "Fresh playback title"
|
||||
}
|
||||
cancelled.resume(returning: video(urls[1], title: "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.openStreamQueue(queue([urls[0], urls[1]]))
|
||||
try await until("removable background 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"))
|
||||
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"))
|
||||
background = nil
|
||||
try await until("replacement result updates its own row", 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")
|
||||
}
|
||||
|
||||
static func streamQueueLifecycle(source: ResolvedSource) async throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("terebi.queue-check." + UUID().uuidString).resolvingSymlinksInPath()
|
||||
@@ -130,15 +242,17 @@ 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
|
||||
youtubeRequests += 1
|
||||
if youtubeRequests == 1 {
|
||||
return await withCheckedContinuation { stale = $0 }
|
||||
if Task.currentPriority > .utility {
|
||||
youtubeRequests += 1
|
||||
if youtubeRequests == 1 {
|
||||
return await withCheckedContinuation { stale = $0 }
|
||||
}
|
||||
}
|
||||
return video
|
||||
}, miruroResolver: { _, _ in
|
||||
episodeRequests += 1
|
||||
return source
|
||||
})
|
||||
}, miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
require(model.progressInitializationError == nil, "queue report registration")
|
||||
model.openStreamQueue(queue)
|
||||
@@ -235,7 +349,8 @@ 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 })
|
||||
}, miruroResolver: { _, _ in episodeRequests += 1; return source },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
func deliver(_ items: [(URL, URL)]) throws {
|
||||
let document: [String: Any] = ["version": 1, "items": items.map {
|
||||
@@ -265,7 +380,7 @@ struct ResolvedPlaybackCheck {
|
||||
try deliver([(fourth, fourthReport)])
|
||||
require(model.queue.items.map(\.url) == [first, second, third, fourth] &&
|
||||
model.queue.currentID == selected && model.state.revision == playingRevision &&
|
||||
model.state.status == PLAYER_PLAYING && requests == 1 && episodeRequests == 0,
|
||||
model.state.status == PLAYER_PLAYING,
|
||||
"repeated native delivery appends while playing without reopening current media")
|
||||
try deliver([(third, directory.appendingPathComponent("conflict.jsonl"))])
|
||||
require(model.commandError != nil && model.queue.items.count == 4 && model.queue.currentID == selected,
|
||||
@@ -383,7 +498,8 @@ struct ResolvedPlaybackCheck {
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
if url == cancelledURL { return await withCheckedContinuation { pending = $0 } }
|
||||
return video
|
||||
}, miruroResolver: { _, _ in source })
|
||||
}, miruroResolver: { _, _ in source },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
try await model.addStreamToQueue(first.absoluteString)
|
||||
try await until("empty sidebar addition starts fetched video", model: model) {
|
||||
|
||||
Reference in New Issue
Block a user