/
/
/
1"""Tests that PlayerMedia separates the media length from the served stream length."""
2
3from __future__ import annotations
4
5from unittest.mock import MagicMock
6
7from music_assistant_models.media_items import AudioFormat
8from music_assistant_models.queue_item import QueueItem
9from music_assistant_models.streamdetails import StreamDetails
10
11from music_assistant.controllers.player_queues import PlayerQueuesController
12from music_assistant.controllers.player_queues.state import PlayerQueueData
13
14QUEUE_ID = "q1"
15FULL_DURATION = 300
16
17
18def _controller_with_item(
19 seek_position: int = 0, streamdetails_duration: int | None = FULL_DURATION
20) -> tuple[PlayerQueuesController, QueueItem]:
21 """Build a bare controller holding a single track, optionally seeked into."""
22 ctrl = PlayerQueuesController.__new__(PlayerQueuesController)
23 queue_item = QueueItem(
24 queue_id=QUEUE_ID,
25 queue_item_id="item1",
26 name="Track 1",
27 duration=FULL_DURATION,
28 )
29 queue_item.streamdetails = StreamDetails(
30 provider="test",
31 item_id="item1",
32 audio_format=AudioFormat(),
33 duration=streamdetails_duration,
34 seek_position=seek_position,
35 )
36 queue_data = PlayerQueueData(queue=MagicMock())
37 queue_data.session_id = "session1"
38 ctrl._queue_data = {QUEUE_ID: queue_data}
39 ctrl.mass = MagicMock()
40 return ctrl, queue_item
41
42
43async def test_unseeked_item_has_no_separate_stream_duration() -> None:
44 """Without a seek the stream matches the media item, so stream_duration stays unset."""
45 ctrl, queue_item = _controller_with_item()
46
47 media = await ctrl.player_media_from_queue_item(queue_item)
48
49 assert media.duration == FULL_DURATION
50 assert media.stream_duration is None
51
52
53async def test_seeked_item_keeps_full_duration_and_reports_stream_duration() -> None:
54 """A seek shortens only the stream; duration stays the full media length."""
55 ctrl, queue_item = _controller_with_item(seek_position=120)
56
57 media = await ctrl.player_media_from_queue_item(queue_item)
58
59 assert media.duration == FULL_DURATION
60 assert media.stream_duration == FULL_DURATION - 120
61
62
63async def test_seek_to_the_end_leaves_no_stream_duration() -> None:
64 """A seek that leaves no audio to play keeps the full media length."""
65 ctrl, queue_item = _controller_with_item(seek_position=FULL_DURATION)
66
67 media = await ctrl.player_media_from_queue_item(queue_item)
68
69 assert media.duration == FULL_DURATION
70 assert media.stream_duration is None
71
72
73async def test_seek_past_the_streamed_length_never_goes_negative() -> None:
74 """Seeking beyond what the stream itself holds never yields a negative length."""
75 ctrl, queue_item = _controller_with_item(seek_position=295, streamdetails_duration=290)
76
77 media = await ctrl.player_media_from_queue_item(queue_item)
78
79 assert media.stream_duration is None
80
81
82async def test_item_without_streamdetails_duration_falls_back_to_item_duration() -> None:
83 """The queue item's own duration is used when streamdetails carry none."""
84 ctrl, queue_item = _controller_with_item(seek_position=60, streamdetails_duration=None)
85
86 media = await ctrl.player_media_from_queue_item(queue_item)
87
88 assert media.duration == FULL_DURATION
89 assert media.stream_duration == FULL_DURATION - 60
90