@@ -1750,6 +1750,7 @@ async def get_queue_item_stream_with_smartfade(
17501750 # Round down to nearest frame boundary
17511751 crossfade_buffer_size = (crossfade_buffer_size // frame_size ) * frame_size
17521752 fade_out_data : bytes | None = None
1753+ uncredited_tail_bytes = 0
17531754
17541755 # pin the body to DYNAMIC when the intro was baked DYNAMIC,
17551756 # else a late measurement flips it and causes a volume jump
@@ -1939,6 +1940,8 @@ async def _limited_fade_in() -> AsyncGenerator[bytes]:
19391940 bytes_written += len (mix_chunk )
19401941 else :
19411942 second_part_buf .extend (mix_chunk )
1943+ # tail consumed by the mix but not credited to bytes_written (#5494)
1944+ uncredited_tail_bytes = len (fade_out_data ) - first_part_written
19421945 self ._crossfade_data [queue_item .queue_id ] = CrossfadeData (
19431946 data = bytes (second_part_buf ),
19441947 fade_in_size = fade_in_bytes_consumed ,
@@ -1983,10 +1986,12 @@ async def _limited_fade_in() -> AsyncGenerator[bytes]:
19831986 # this also accounts for crossfade and silence stripping
19841987 seconds_streamed = bytes_written / pcm_format .pcm_sample_size
19851988 streamdetails .seconds_streamed = seconds_streamed
1989+ uncredited_tail_seconds = uncredited_tail_bytes / pcm_format .pcm_sample_size
19861990 # streamdetails.duration is in media-time; seconds_streamed is stream-time
19871991 # (post-atempo), so we scale by playback_speed to recover media-time.
19881992 streamdetails .duration = int (
1989- streamdetails .seek_position + seconds_streamed * playback_speed
1993+ streamdetails .seek_position
1994+ + (seconds_streamed + uncredited_tail_seconds ) * playback_speed
19901995 )
19911996 # propagate accurate duration to queue_item so UI displays it
19921997 queue_item .duration = streamdetails .duration
@@ -2355,10 +2360,13 @@ def _superseded() -> bool:
23552360 # this also accounts for crossfade and silence stripping
23562361 seconds_streamed = bytes_written / pcm_sample_size
23572362 queue_track .streamdetails .seconds_streamed = seconds_streamed
2363+ # the held-back crossfade tail still counts as this track's media-time (#5494)
2364+ tail_seconds = len (last_fadeout_part ) / pcm_sample_size
23582365 # streamdetails.duration is in media-time; seconds_streamed is stream-time
23592366 # (post-atempo), so we scale by the track's playback_speed to recover media-time.
23602367 queue_track .streamdetails .duration = int (
2361- queue_track .streamdetails .seek_position + seconds_streamed * track_playback_speed
2368+ queue_track .streamdetails .seek_position
2369+ + (seconds_streamed + tail_seconds ) * track_playback_speed
23622370 )
23632371 # propagate accurate duration to queue_item so UI displays it
23642372 queue_track .duration = queue_track .streamdetails .duration
@@ -2389,14 +2397,13 @@ def _superseded() -> bool:
23892397 for pcm_slice in iter_pcm_slices (last_fadeout_part , pcm_format , 1000 ):
23902398 yield pcm_slice
23912399 await asyncio .sleep (0 )
2392- # correct seconds streamed/ duration
2400+ # correct seconds streamed - the duration already includes the tail
23932401 last_part_seconds = len (last_fadeout_part ) / pcm_sample_size
23942402 streamdetails = queue_track .streamdetails
23952403 assert streamdetails is not None
23962404 streamdetails .seconds_streamed = (
23972405 streamdetails .seconds_streamed or 0
23982406 ) + last_part_seconds
2399- streamdetails .duration = int ((streamdetails .duration or 0 ) + last_part_seconds )
24002407 # also update the play log entry so elapsed time tracking stays in sync
24012408 if last_play_log_entry :
24022409 assert last_play_log_entry .seconds_streamed is not None
0 commit comments