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

View File

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

View File

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