/
/
/
1"""Tests for music_assistant.helpers.audio."""
2
3from __future__ import annotations
4
5from types import SimpleNamespace
6from unittest.mock import MagicMock
7
8from music_assistant_models.enums import ContentType
9from music_assistant_models.media_items import AudioFormat
10
11from music_assistant.helpers.audio import (
12 build_concat_filelist,
13 calculate_content_length,
14 resolve_output_player_ids,
15)
16from music_assistant.helpers.ffmpeg import DEFAULT_MP3_BIT_RATE
17
18
19def test_resolve_output_player_ids_resolves_parents_and_duplicates() -> None:
20 """Output destinations use visible protocol parents without duplicates."""
21 mass = MagicMock()
22 players = {
23 "leader": SimpleNamespace(protocol_parent_id=None),
24 "protocol-child": SimpleNamespace(protocol_parent_id="child"),
25 "child": SimpleNamespace(protocol_parent_id=None),
26 }
27 mass.players.get_player.side_effect = players.get
28
29 result = resolve_output_player_ids(
30 mass,
31 ("leader", "leader", "protocol-child", "child", "missing", "missing"),
32 )
33
34 assert result == {"leader", "child", "missing"}
35
36
37def test_mp3_content_length_uses_encoder_bitrate() -> None:
38 """MP3 size estimation uses the bitrate configured for FFmpeg encoding."""
39 seconds = 2
40 assert calculate_content_length(
41 AudioFormat(content_type=ContentType.MP3),
42 seconds,
43 ) == int(((DEFAULT_MP3_BIT_RATE * 1000) / 8) * seconds)
44
45
46def test_build_concat_filelist_plain_paths() -> None:
47 """Paths without special characters are wrapped verbatim, one per line."""
48 result = build_concat_filelist(["/music/a.mp3", "/music/b.mp3"])
49 assert result == "file '/music/a.mp3'\nfile '/music/b.mp3'\n"
50
51
52def test_build_concat_filelist_escapes_apostrophes() -> None:
53 r"""
54 A single quote in the path is escaped as '\'' for the concat demuxer.
55
56 Regression test for multipart playback failing on paths such as
57 "Amelia Bedelia's", where the demuxer truncated the path at the apostrophe.
58 """
59 path = "/audiobooks/Herman Parish - Young Amelia Bedelia's Audio Collection/01.mp3"
60 result = build_concat_filelist([path])
61 assert (
62 result
63 == "file '/audiobooks/Herman Parish - Young Amelia Bedelia'\\''s Audio Collection/01.mp3'\n"
64 )
65 # The original apostrophe must survive once the escaping is unwrapped.
66 assert path in result.replace("'\\''", "'")
67
68
69def test_build_concat_filelist_escapes_multiple_apostrophes() -> None:
70 """Every apostrophe in a path is escaped, not just the first."""
71 result = build_concat_filelist(["/x/it's a, b's & c's.mp3"])
72 assert result == "file '/x/it'\\''s a, b'\\''s & c'\\''s.mp3'\n"
73