feat: add YouTube search to !play and fix NA URL fallback

Non-URL input (e.g. !play classical music) searches YouTube for 10
results and picks one randomly. Also fixes --flat-playlist returning
"NA" as the URL for single videos by falling back to the original
input URL.
This commit is contained in:
user
2026-02-21 23:52:01 +01:00
parent 6b7d733650
commit e4e1e219f0
4 changed files with 67 additions and 5 deletions

View File

@@ -134,6 +134,25 @@ class TestPlayCommand:
assert len(ps["queue"]) == 1
assert ps["queue"][0].title == "Test Track"
def test_play_search_query(self):
bot = _FakeBot()
msg = _Msg(text="!play classical music")
tracks = [
("https://youtube.com/watch?v=a", "Result 1"),
("https://youtube.com/watch?v=b", "Result 2"),
("https://youtube.com/watch?v=c", "Result 3"),
]
with patch.object(_mod, "_resolve_tracks", return_value=tracks) as mock_rt:
with patch.object(_mod, "_ensure_loop"):
asyncio.run(_mod.cmd_play(bot, msg))
# Should prepend ytsearch10: for non-URL input
mock_rt.assert_called_once()
assert mock_rt.call_args[0][0] == "ytsearch10:classical music"
# Should pick one random result, not enqueue all
ps = _mod._ps(bot)
assert len(ps["queue"]) == 1
assert any("Playing" in r for r in bot.replied)
def test_play_shows_queued_when_busy(self):
bot = _FakeBot()
ps = _mod._ps(bot)
@@ -354,6 +373,21 @@ class TestMusicHelpers:
assert len(result) == 80
assert result.endswith("...")
def test_is_url_http(self):
assert _mod._is_url("https://youtube.com/watch?v=abc") is True
def test_is_url_plain_http(self):
assert _mod._is_url("http://example.com") is True
def test_is_url_ytsearch(self):
assert _mod._is_url("ytsearch:classical music") is True
def test_is_url_search_query(self):
assert _mod._is_url("classical music") is False
def test_is_url_single_word(self):
assert _mod._is_url("jazz") is False
# ---------------------------------------------------------------------------
# TestPlaylistExpansion
@@ -431,6 +465,14 @@ class TestPlaylistExpansion:
tracks = _mod._resolve_tracks("https://example.com/v1")
assert tracks == [("https://example.com/v1", "Single Video")]
def test_resolve_tracks_na_url_fallback(self):
"""--flat-playlist prints NA for single videos; use original URL."""
result = MagicMock()
result.stdout = "NA\nSingle Video\n"
with patch("subprocess.run", return_value=result):
tracks = _mod._resolve_tracks("https://example.com/v1")
assert tracks == [("https://example.com/v1", "Single Video")]
def test_resolve_tracks_playlist(self):
"""Subprocess returning multiple url+title pairs."""
result = MagicMock()