@@ -746,11 +746,99 @@ final class YTMusicClient: YTMusicClientProtocol {
746746 let topKeys = Array ( data. keys)
747747 self . logger. debug ( " Artist response top-level keys: \( topKeys) " )
748748
749- let detail = ArtistParser . parseArtistDetail ( data, artistId: id)
749+ var detail = ArtistParser . parseArtistDetail ( data, artistId: id)
750+
751+ // Artist page top songs don't include duration — fetch via queue endpoint (best-effort)
752+ let songsNeedingDuration = detail. songs. filter { $0. duration == nil }
753+ if !songsNeedingDuration. isEmpty {
754+ do {
755+ let durations = try await self . fetchSongDurations ( videoIds: songsNeedingDuration. map ( \. videoId) )
756+ let enrichedSongs = detail. songs. map { song -> Song in
757+ if song. duration == nil , let duration = durations [ song. videoId] {
758+ return Song (
759+ id: song. id,
760+ title: song. title,
761+ artists: song. artists,
762+ album: song. album,
763+ duration: duration,
764+ thumbnailURL: song. thumbnailURL,
765+ videoId: song. videoId,
766+ hasVideo: song. hasVideo,
767+ musicVideoType: song. musicVideoType,
768+ likeStatus: song. likeStatus,
769+ isInLibrary: song. isInLibrary,
770+ feedbackTokens: song. feedbackTokens
771+ )
772+ }
773+ return song
774+ }
775+ detail = ArtistDetail (
776+ artist: detail. artist,
777+ description: detail. description,
778+ songs: enrichedSongs,
779+ albums: detail. albums,
780+ thumbnailURL: detail. thumbnailURL,
781+ channelId: detail. channelId,
782+ isSubscribed: detail. isSubscribed,
783+ subscriberCount: detail. subscriberCount,
784+ hasMoreSongs: detail. hasMoreSongs,
785+ songsBrowseId: detail. songsBrowseId,
786+ songsParams: detail. songsParams,
787+ mixPlaylistId: detail. mixPlaylistId,
788+ mixVideoId: detail. mixVideoId
789+ )
790+ } catch is CancellationError {
791+ throw CancellationError ( )
792+ } catch {
793+ self . logger. debug ( " Best-effort duration fetch failed: \( error. localizedDescription) " )
794+ }
795+ }
796+
750797 self . logger. info ( " Parsed artist ' \( detail. artist. name) ' with \( detail. songs. count) songs and \( detail. albums. count) albums " )
751798 return detail
752799 }
753800
801+ /// Fetches durations for a batch of video IDs using the queue endpoint.
802+ private func fetchSongDurations( videoIds: [ String ] ) async throws -> [ String : TimeInterval ] {
803+ guard !videoIds. isEmpty else { return [ : ] }
804+
805+ let body : [ String : Any ] = [
806+ " videoIds " : videoIds,
807+ ]
808+
809+ let data = try await request ( " music/get_queue " , body: body, ttl: APICache . TTL. artist)
810+
811+ var durations : [ String : TimeInterval ] = [ : ]
812+ if let queueDatas = data [ " queueDatas " ] as? [ [ String : Any ] ] {
813+ for queueData in queueDatas {
814+ guard let content = queueData [ " content " ] as? [ String : Any ] else { continue }
815+ // Handle both direct and wrapped renderer structures
816+ let renderer : [ String : Any ] ? = if let direct = content [ " playlistPanelVideoRenderer " ] as? [ String : Any ] {
817+ direct
818+ } else if let wrapper = content [ " playlistPanelVideoWrapperRenderer " ] as? [ String : Any ] ,
819+ let primary = wrapper [ " primaryRenderer " ] as? [ String : Any ] ,
820+ let wrapped = primary [ " playlistPanelVideoRenderer " ] as? [ String : Any ]
821+ {
822+ wrapped
823+ } else {
824+ nil
825+ }
826+ if let renderer,
827+ let videoId = renderer [ " videoId " ] as? String ,
828+ let lengthText = renderer [ " lengthText " ] as? [ String : Any ] ,
829+ let runs = lengthText [ " runs " ] as? [ [ String : Any ] ] ,
830+ let durationText = runs. first ? [ " text " ] as? String ,
831+ let duration = ParsingHelpers . parseDuration ( durationText)
832+ {
833+ durations [ videoId] = duration
834+ }
835+ }
836+ }
837+
838+ self . logger. debug ( " Fetched durations for \( durations. count) / \( videoIds. count) songs " )
839+ return durations
840+ }
841+
754842 /// Fetches all songs for an artist using the songs browse endpoint.
755843 func getArtistSongs( browseId: String , params: String ? ) async throws -> [ Song ] {
756844 self . logger. info ( " Fetching artist songs: \( browseId) " )
0 commit comments