fix: queue shows 0:00 duration for sourced tracks

YouTubeEngine passed track duration in seconds while
SpotubeAudioSourceMatchObject.fromJson interprets `duration` as
microseconds, so durations collapsed to ~0 and the queue displayed 0:00.

- lib/services/metadata/metadata.dart: send duration in microseconds
  (search() and getVideo() callbacks)
- lib/components/track_tile/track_tile.dart,
  lib/modules/player/player_queue.dart: add resolveMissingDuration so
  the queue actively resolves the sourced track's real duration when
  the track's own metadata duration is effectively zero

Fixes #3021
This commit is contained in:
Tomasz Jabłonowski 2026-07-26 10:37:09 +02:00
parent 69a310c78f
commit 3558ad3b12
3 changed files with 17 additions and 4 deletions

View File

@ -21,6 +21,7 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/utils/platform.dart';
final isBlacklistedProvider =
@ -46,6 +47,7 @@ class TrackTile extends HookConsumerWidget {
final bool userPlaylist;
final String? playlistId;
final AudioPlayerState playlist;
final bool resolveMissingDuration;
final List<Widget>? leadingActions;
@ -62,6 +64,7 @@ class TrackTile extends HookConsumerWidget {
this.userPlaylist = false,
this.playlistId,
this.leadingActions,
this.resolveMissingDuration = false,
});
@override
@ -88,6 +91,16 @@ class TrackTile extends HookConsumerWidget {
// toggling a dedicated `selectionMode` flag (e.g. playlists), so we must
// disable inner navigation in both cases.
final effectiveSelection = selectionMode || onChanged != null;
final sourcedTrack = resolveMissingDuration &&
track is SpotubeFullTrackObject &&
track.durationMs < Duration.millisecondsPerSecond
? ref.watch(sourcedTrackProvider(track as SpotubeFullTrackObject))
: null;
final duration = sourcedTrack?.maybeWhen(
data: (track) => track.info.duration,
orElse: () => Duration(milliseconds: this.track.durationMs),
) ??
Duration(milliseconds: track.durationMs);
return LayoutBuilder(builder: (context, constrains) {
return Listener(
@ -322,8 +335,7 @@ class TrackTile extends HookConsumerWidget {
children: [
const SizedBox(width: 8),
Text(
Duration(milliseconds: track.durationMs)
.toHumanReadableString(padZero: false),
duration.toHumanReadableString(padZero: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),

View File

@ -308,6 +308,7 @@ class PlayerQueue extends HookConsumerWidget {
playlist: playlist,
index: i,
track: track,
resolveMissingDuration: true,
selectionMode: selectionMode.value,
selected:
selectedTrackIds.value.contains(track.id),

View File

@ -90,7 +90,7 @@ class MetadataPlugin {
'id': video.id.value,
'title': video.title,
'author': video.author,
'duration': video.duration?.inSeconds,
'duration': video.duration?.inMicroseconds,
'description': video.description,
'uploadDate': video.uploadDate?.toIso8601String(),
'viewCount': video.engagement.viewCount,
@ -105,7 +105,7 @@ class MetadataPlugin {
'id': video.id.value,
'title': video.title,
'author': video.author,
'duration': video.duration?.inSeconds,
'duration': video.duration?.inMicroseconds,
'description': video.description,
'uploadDate': video.uploadDate?.toIso8601String(),
'viewCount': video.engagement.viewCount,