Skip to content

fix: Show durations for artist top songs#113

Merged
sozercan merged 5 commits intomainfrom
fix/artist-top-songs-duration
Feb 25, 2026
Merged

fix: Show durations for artist top songs#113
sozercan merged 5 commits intomainfrom
fix/artist-top-songs-duration

Conversation

@sozercan
Copy link
Owner

Problem

Artist top songs displayed "--:--" instead of actual durations. The YouTube Music API's artist page browse response does not include duration data for inline top songs.

Solution

Fetch durations via the music/get_queue endpoint after parsing artist details, enriching songs with their actual durations.

Changes

  • YTMusicClient.swift: Added fetchSongDurations(videoIds:) method that batch-fetches durations via music/get_queue. Modified getArtist() to enrich songs with fetched durations (best-effort, with proper CancellationError propagation).
  • ParsingHelpers.swift: Fixed flex column duration scanning to check all runs in reverse order, with navigationEndpoint guard to prevent false positives from album titles like "4:44".
  • ParsingHelpersTests.swift: Added test for combined flex column duration pattern.

Key design decisions

  • Best-effort enrichment: Duration fetch failure does not break artist page loading — errors are logged and swallowed, except CancellationError which propagates correctly for proper ViewModel state management.
  • Wrapper handling: Parses both direct playlistPanelVideoRenderer and wrapped playlistPanelVideoWrapperRenderer structures from the queue response.
  • All Song properties preserved: Enrichment passes through all 12 Song properties to prevent silent data loss.

The YouTube Music API artist page browse response does not include
duration data for inline top songs. After parsing, fetch durations
via a single music/get_queue API call and enrich the songs.
The music/get_queue API sometimes wraps results in
playlistPanelVideoWrapperRenderer instead of returning
playlistPanelVideoRenderer directly. Handle both structures
to ensure all song durations are fetched.
- Make duration fetch best-effort (try? instead of try) so artist
  page still loads if the queue endpoint fails
- Pass all 12 Song properties through during enrichment to prevent
  silent data loss of hasVideo, musicVideoType, likeStatus, etc.
- Add navigationEndpoint guard in flex column duration scanning to
  prevent album titles like '4:44' from being parsed as durations
Replace try? with do/catch to rethrow CancellationError while still
treating other failures as best-effort. This ensures the ViewModel's
cancellation handler fires correctly on navigation away.
Copilot AI review requested due to automatic review settings February 24, 2026 22:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes missing durations ("--:--") for artist top songs by improving duration parsing and enriching artist song data with durations fetched from the music/get_queue endpoint.

Changes:

  • Enrich getArtist() results by batch-fetching missing song durations via music/get_queue (best-effort; propagates cancellation).
  • Improve flex-column duration extraction by scanning all runs in reverse and ignoring navigable runs to avoid false positives (e.g., album titles like “4:44”).
  • Add a unit test covering the combined flex-column duration pattern used on artist pages.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Tests/KasetTests/ParsingHelpersTests.swift Adds coverage for extracting duration from combined flex-column runs.
Core/Services/API/YTMusicClient.swift Adds best-effort duration enrichment for artist top songs via music/get_queue.
Core/Services/API/Parsers/ParsingHelpers.swift Improves duration detection in artist-page flex columns (reverse scan + navigation guard).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"videoIds": videoIds,
]

let data = try await request("music/get_queue", body: body, ttl: APICache.TTL.artist)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchSongDurations(videoIds:) caches music/get_queue responses using APICache.TTL.artist, but elsewhere in this file music/get_queue is explicitly called with ttl: nil (“No caching for queue endpoint”). Caching per unique videoIds batch can also thrash the small (50 entry) LRU cache and return stale duration data. Consider switching this call to ttl: nil (or introducing a dedicated short TTL specifically for queue duration lookups if caching is desired).

Suggested change
let data = try await request("music/get_queue", body: body, ttl: APICache.TTL.artist)
let data = try await request("music/get_queue", body: body, ttl: nil)

Copilot uses AI. Check for mistakes.
Comment on lines +815 to +825
// Handle both direct and wrapped renderer structures
let renderer: [String: Any]? = if let direct = content["playlistPanelVideoRenderer"] as? [String: Any] {
direct
} else if let wrapper = content["playlistPanelVideoWrapperRenderer"] as? [String: Any],
let primary = wrapper["primaryRenderer"] as? [String: Any],
let wrapped = primary["playlistPanelVideoRenderer"] as? [String: Any]
{
wrapped
} else {
nil
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queue renderer extraction logic here duplicates PlaylistParser.extractQueueRenderer(from:) (which already handles direct vs playlistPanelVideoWrapperRenderer). Duplicating this parsing increases drift risk if the queue response shape changes; consider refactoring to share a single helper (e.g., move the extractor to a common utility or make it non-private and reuse it here).

Suggested change
// Handle both direct and wrapped renderer structures
let renderer: [String: Any]? = if let direct = content["playlistPanelVideoRenderer"] as? [String: Any] {
direct
} else if let wrapper = content["playlistPanelVideoWrapperRenderer"] as? [String: Any],
let primary = wrapper["primaryRenderer"] as? [String: Any],
let wrapped = primary["playlistPanelVideoRenderer"] as? [String: Any]
{
wrapped
} else {
nil
}
let renderer = PlaylistParser.extractQueueRenderer(from: content)

Copilot uses AI. Check for mistakes.
@sozercan sozercan merged commit 8092995 into main Feb 25, 2026
10 checks passed
@sozercan sozercan deleted the fix/artist-top-songs-duration branch February 25, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants