"""Extended service tests - Audio, Lyrics, Musicbrainz, RadioBrowser.""" from unittest.mock import patch, MagicMock import tempfile import os class TestExtractMetadata: def test_nonexistent_file(self): from app.services.audio import extract_metadata result = extract_metadata("/nonexistent/file.mp3") assert isinstance(result, dict) assert result.get("title") is None def test_returns_dict(self): from app.services.audio import extract_metadata result = extract_metadata("/no/file.wav") assert isinstance(result, dict) def test_supported_formats(self): from app.services.audio import SUPPORTED_FORMATS assert SUPPORTED_FORMATS == {'.mp3', '.aac', '.flac', '.wav', '.ogg', '.m4a'} class TestTranscode: def test_nonexistent_input(self): from app.services.audio import transcode_to_ogg result = transcode_to_ogg("/nonexistent.mp3", "/tmp") assert result is None class TestScanDirectory: def test_empty_dir(self): from app.services.audio import scan_directory with tempfile.TemporaryDirectory() as tmpdir: mock_db = MagicMock() mock_db.query.return_value.first.return_value = None result = scan_directory(tmpdir, mock_db) assert result.scanned == 0 assert result.added == 0 assert result.errors == [] def test_skips_non_audio(self): from app.services.audio import scan_directory with tempfile.TemporaryDirectory() as tmpdir: with open(os.path.join(tmpdir, "readme.txt"), "w") as f: f.write("test") mock_db = MagicMock() mock_db.query.return_value.first.return_value = None result = scan_directory(tmpdir, mock_db) assert result.scanned == 0 class TestDeleteSong: def test_delete_files(self): from app.services.audio import delete_song tmpdir = tempfile.mkdtemp() f1 = os.path.join(tmpdir, "t1.ogg") f2 = os.path.join(tmpdir, "t2.mp3") open(f1, "w").close() open(f2, "w").close() song = MagicMock() song.file_path = f1 song.transcoded_path = f2 song.album_art_path = None delete_song(song) assert not os.path.exists(f1) assert not os.path.exists(f2) import shutil shutil.rmtree(tmpdir) def test_delete_no_paths(self): from app.services.audio import delete_song song = MagicMock() song.file_path = None song.transcoded_path = None song.album_art_path = None delete_song(song) # Should not raise class TestGetStreamPath: def test_transcoded_priority(self): from app.services.audio import get_stream_path tmpdir = tempfile.mkdtemp() f1 = os.path.join(tmpdir, "t.ogg") f2 = os.path.join(tmpdir, "t.mp3") open(f1, "w").close() open(f2, "w").close() song = MagicMock() song.transcoded_path = f1 song.file_path = f2 assert get_stream_path(song) == f1 import shutil shutil.rmtree(tmpdir) def test_fallback_original(self): from app.services.audio import get_stream_path tmpdir = tempfile.mkdtemp() f = os.path.join(tmpdir, "t.mp3") open(f, "w").close() song = MagicMock() song.transcoded_path = None song.file_path = f assert get_stream_path(song) == f import shutil shutil.rmtree(tmpdir) def test_no_files(self): from app.services.audio import get_stream_path song = MagicMock() song.transcoded_path = None song.file_path = None assert get_stream_path(song) is None class TestLyricsService: def test_no_api_key(self): from app.services.lyrics import fetch_lyrics assert fetch_lyrics("song", "artist") is None def test_returns_none_or_str(self): from app.services.lyrics import fetch_lyrics result = fetch_lyrics("s", "a") assert result is None or isinstance(result, str) class TestMusicbrainz: def test_search_returns_none_no_network(self): from app.services.musicbrainz import search_artist result = search_artist("nonexistent_xyz_artist_123") assert result is None or isinstance(result, dict) def test_releases_returns_none_no_network(self): from app.services.musicbrainz import get_artist_releases result = get_artist_releases("nonexistent-id") assert isinstance(result, list) def test_format_duration(self): from app.services.musicbrainz import _format_duration assert _format_duration(0) == "0:00" assert _format_duration(60) == "1:00" assert _format_duration(245) == "4:05" assert _format_duration(3661) == "61:01" def test_format_duration_negative(self): from app.services.musicbrainz import _format_duration assert _format_duration(-1) == "-1:59" class TestRadioBrowser: def test_fetch_stations_empty_on_error(self): from app.services.radio_browser import fetch_stations import requests as req_mod with patch.object(req_mod, "get", side_effect=req_mod.RequestException("fail")): result = fetch_stations(limit=5) assert result == [] def test_fetch_nearby_empty_on_error(self): from app.services.radio_browser import fetch_nearby_stations import requests as req_mod with patch.object(req_mod, "get", side_effect=req_mod.RequestException("fail")): result = fetch_nearby_stations(40.7, -74.0, 100) assert result == [] def test_format_station(self): from app.services.radio_browser import _format_station s = { "stationuuid": "u1", "name": "Test Radio", "codec": "mp3", "url_resolved": "http://stream.test", "geo_lat": "40.7", "geo_long": "-74.0", "tag": "jazz,lofi", "countryname": "USA", "language": "en", "bitrate": "128", "votes": "10", } result = _format_station(s) assert result["id"] == "u1" assert result["name"] == "Test Radio" assert result["stream_url"] == "http://stream.test" assert result["location_lat"] == 40.7 assert result["tags"] == ["jazz", "lofi"] assert result["bitrate"] == 128 assert result["votes"] == 10 def test_format_station_missing_fields(self): from app.services.radio_browser import _format_station s = {} result = _format_station(s) assert result["name"] == "Unknown" def test_haversine_same(self): from app.services.radio_browser import _haversine assert _haversine(0, 0, 0, 0) == 0.0 def test_haversine_symmetric(self): from app.services.radio_browser import _haversine a = _haversine(40.7, -74.0, 51.5, -0.1) b = _haversine(51.5, -0.1, 40.7, -74.0) assert abs(a - b) < 0.01 def test_haversine_nyc_london(self): from app.services.radio_browser import _haversine d = _haversine(40.7128, -74.0060, 51.5074, -0.1278) assert 5500 < d < 6000