570 lines
18 KiB
Python
570 lines
18 KiB
Python
import hashlib
|
|
import json
|
|
import logging
|
|
import re
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Generator, List, Tuple
|
|
|
|
import pytest
|
|
from dateutil.tz import tzutc
|
|
|
|
from sublime_music.adapters import ConfigurationStore
|
|
from sublime_music.adapters.subsonic import api_objects as SubsonicAPI, SubsonicAdapter
|
|
|
|
MOCK_DATA_FILES = Path(__file__).parent.joinpath("mock_data")
|
|
|
|
|
|
@pytest.fixture
|
|
def adapter(tmp_path: Path):
|
|
ConfigurationStore.MOCK = True
|
|
config = ConfigurationStore(
|
|
server_address="https://subsonic.example.com",
|
|
username="test",
|
|
verify_cert=True,
|
|
salt_auth=False,
|
|
)
|
|
config.set_secret("password", "testpass")
|
|
|
|
adapter = SubsonicAdapter(config, tmp_path)
|
|
adapter._is_mock = True
|
|
|
|
yield adapter
|
|
adapter.shutdown()
|
|
|
|
|
|
@pytest.fixture
|
|
def salt_auth_adapter(tmp_path: Path):
|
|
ConfigurationStore.MOCK = True
|
|
config = ConfigurationStore(
|
|
server_address="https://subsonic.example.com",
|
|
username="test",
|
|
verify_cert=True,
|
|
salt_auth=True,
|
|
)
|
|
config.set_secret("password", "testpass")
|
|
|
|
adapter = SubsonicAdapter(config, tmp_path)
|
|
adapter._is_mock = True
|
|
|
|
yield adapter
|
|
adapter.shutdown()
|
|
|
|
|
|
def mock_data_files(
|
|
request_name: str, mode: str = "r"
|
|
) -> Generator[Tuple[Path, Any], None, None]:
|
|
"""
|
|
Yields all of the files, and each of the elements of in the file (separated by a
|
|
line of ='s), for all files in the mock_data directory that start with
|
|
``request_name``. This only works for text such as JSON.
|
|
"""
|
|
sep_re = re.compile(r"=+\n")
|
|
|
|
num_files = 0
|
|
for file in MOCK_DATA_FILES.iterdir():
|
|
if file.name.split("-")[0] == request_name:
|
|
with open(file, mode) as f:
|
|
parts: List[str] = []
|
|
aggregate: List[str] = []
|
|
for line in f:
|
|
if sep_re.match(line):
|
|
parts.append("\n".join(aggregate))
|
|
aggregate = []
|
|
continue
|
|
aggregate.append(line)
|
|
|
|
parts.append("\n".join(aggregate))
|
|
print(file) # noqa: T001
|
|
num_files += 1
|
|
yield file, iter(parts)
|
|
|
|
# Make sure that is at least one test file
|
|
assert num_files > 0
|
|
|
|
|
|
def mock_json(**obj: Any) -> str:
|
|
return json.dumps(
|
|
{"subsonic-response": {"status": "ok", "version": "1.15.0", **obj}}
|
|
)
|
|
|
|
|
|
def camel_to_snake(name: str) -> str:
|
|
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
|
|
|
|
|
def test_config_form():
|
|
# Just make sure that the functions work. That's half of the battle.
|
|
config_store = ConfigurationStore()
|
|
SubsonicAdapter.get_configuration_form(config_store)
|
|
|
|
|
|
def test_plain_auth_logic(adapter: SubsonicAdapter):
|
|
expected = {
|
|
"u": "test",
|
|
"p": "testpass",
|
|
"c": "Sublime Music",
|
|
"f": "json",
|
|
"v": "1.8.0",
|
|
}
|
|
assert sorted(expected.items()) == sorted(adapter._get_params().items())
|
|
|
|
|
|
def test_salt_auth_logic(salt_auth_adapter: SubsonicAdapter):
|
|
expected = {"u": "test", "c": "Sublime Music", "f": "json", "v": "1.8.0"}
|
|
|
|
params = salt_auth_adapter._get_params()
|
|
assert "p" not in params
|
|
assert "s" in params
|
|
salt = params["s"]
|
|
assert "t" in params
|
|
assert params["t"] == hashlib.md5(f"testpass{salt}".encode()).hexdigest()
|
|
assert all(key in params and params[key] == expected[key] for key in expected)
|
|
|
|
|
|
def test_migrate_configuration_populate_salt_auth():
|
|
config = ConfigurationStore(
|
|
server_address="https://subsonic.example.com",
|
|
username="test",
|
|
verify_cert=True,
|
|
)
|
|
SubsonicAdapter.migrate_configuration(config)
|
|
assert "salt_auth" in config
|
|
assert config["salt_auth"]
|
|
|
|
|
|
def test_migrate_configuration_salt_auth_present():
|
|
config = ConfigurationStore(
|
|
server_address="https://subsonic.example.com",
|
|
username="test",
|
|
verify_cert=True,
|
|
salt_auth=False,
|
|
)
|
|
SubsonicAdapter.migrate_configuration(config)
|
|
assert "salt_auth" in config
|
|
assert not config["salt_auth"]
|
|
|
|
|
|
def test_make_url(adapter: SubsonicAdapter):
|
|
assert adapter._make_url("foo") == "https://subsonic.example.com/rest/foo.view"
|
|
|
|
|
|
def test_ping_status(adapter: SubsonicAdapter):
|
|
# Mock a connection error
|
|
adapter._set_mock_data(Exception())
|
|
assert not adapter.ping_status
|
|
|
|
# Simulate some sort of ping error
|
|
for filename, data in mock_data_files("ping_failed"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
assert not adapter.ping_status
|
|
|
|
# Simulate valid ping
|
|
adapter._set_mock_data(mock_json())
|
|
adapter._last_ping_timestamp.value = 0.0
|
|
adapter._set_ping_status()
|
|
assert adapter.ping_status
|
|
|
|
|
|
def test_get_playlists(adapter: SubsonicAdapter):
|
|
expected = [
|
|
SubsonicAPI.Playlist(
|
|
id="2",
|
|
name="Test",
|
|
song_count=132,
|
|
duration=timedelta(seconds=33072),
|
|
created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=tzutc()),
|
|
changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=tzutc()),
|
|
comment="Foo",
|
|
owner="foo",
|
|
public=True,
|
|
cover_art="pl-2",
|
|
),
|
|
SubsonicAPI.Playlist(
|
|
id="3",
|
|
name="Bar",
|
|
song_count=23,
|
|
duration=timedelta(seconds=847),
|
|
created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=tzutc()),
|
|
changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=tzutc()),
|
|
comment="",
|
|
owner="foo",
|
|
public=False,
|
|
cover_art="pl-3",
|
|
),
|
|
]
|
|
|
|
for filename, data in mock_data_files("get_playlists"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
assert adapter.get_playlists() == sorted(expected, key=lambda e: e.name)
|
|
|
|
# When playlists is null, expect an empty list.
|
|
adapter._set_mock_data(mock_json())
|
|
assert adapter.get_playlists() == []
|
|
|
|
|
|
def test_get_playlist_details(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_playlist_details"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
playlist_details = adapter.get_playlist_details("2")
|
|
|
|
# Make sure that the song count is correct even if it's not provided.
|
|
# Old versions of Subsonic don't have these properties.
|
|
assert len(playlist_details.songs) == 2
|
|
assert playlist_details.duration == timedelta(seconds=470)
|
|
|
|
# Make sure that at least the first song got decoded properly.
|
|
assert playlist_details.songs[0] == SubsonicAPI.Song(
|
|
id="202",
|
|
parent_id="318",
|
|
title="What a Beautiful Name",
|
|
_album="What a Beautiful Name - Single",
|
|
album_id="48",
|
|
_artist="Hillsong Worship",
|
|
artist_id="38",
|
|
track=1,
|
|
year=2016,
|
|
_genre="Christian & Gospel",
|
|
cover_art="318",
|
|
size=8381640,
|
|
duration=timedelta(seconds=238),
|
|
path="/".join(
|
|
(
|
|
"Hillsong Worship",
|
|
"What a Beautiful Name - Single",
|
|
"01 What a Beautiful Name.m4a",
|
|
)
|
|
),
|
|
disc_number=1,
|
|
)
|
|
|
|
|
|
def test_create_playlist(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("create_playlist"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
adapter.create_playlist(
|
|
name="Foo",
|
|
songs=[
|
|
SubsonicAPI.Song(
|
|
id="202",
|
|
parent_id="318",
|
|
title="What a Beautiful Name",
|
|
_album="What a Beautiful Name - Single",
|
|
album_id="48",
|
|
_artist="Hillsong Worship",
|
|
artist_id="38",
|
|
track=1,
|
|
year=2016,
|
|
_genre="Christian & Gospel",
|
|
cover_art="318",
|
|
duration=timedelta(seconds=238),
|
|
path="/".join(
|
|
(
|
|
"Hillsong Worship",
|
|
"What a Beautiful Name - Single",
|
|
"01 What a Beautiful Name.m4a",
|
|
)
|
|
),
|
|
disc_number=1,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
def test_update_playlist(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("update_playlist"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
result_playlist = adapter.update_playlist(
|
|
"1", name="Foo", comment="Bar", public=True, song_ids=["202"]
|
|
)
|
|
|
|
assert result_playlist.comment == "Bar"
|
|
assert result_playlist.public is False
|
|
|
|
|
|
def test_get_song_details(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_song_details"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
song = adapter.get_song_details("1")
|
|
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
|
|
"1",
|
|
"Sweet Caroline",
|
|
2017,
|
|
"544",
|
|
timedelta(seconds=203),
|
|
)
|
|
assert song.path and song.path.endswith("Sweet Caroline.mp3")
|
|
assert song.parent_id == "544"
|
|
assert song.artist
|
|
assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
|
|
assert song.album
|
|
assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
|
|
assert song.genre and song.genre.name == "Pop"
|
|
|
|
|
|
def test_get_song_details_missing_data(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_song_details_no_albumid"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
song = adapter.get_song_details("1")
|
|
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
|
|
"1",
|
|
"Sweet Caroline",
|
|
2017,
|
|
"544",
|
|
timedelta(seconds=203),
|
|
)
|
|
assert song.path and song.path.endswith("Sweet Caroline.mp3")
|
|
assert song.parent_id == "544"
|
|
assert song.artist
|
|
assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
|
|
assert song.album
|
|
assert (song.album.id, song.album.name) == (None, "50th Anniversary Collection")
|
|
assert song.genre and song.genre.name == "Pop"
|
|
|
|
for filename, data in mock_data_files("get_song_details_no_artistid"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
song = adapter.get_song_details("1")
|
|
assert (song.id, song.title, song.year, song.cover_art, song.duration) == (
|
|
"1",
|
|
"Sweet Caroline",
|
|
2017,
|
|
"544",
|
|
timedelta(seconds=203),
|
|
)
|
|
assert song.path and song.path.endswith("Sweet Caroline.mp3")
|
|
assert song.parent_id == "544"
|
|
assert song.artist
|
|
assert (song.artist.id, song.artist.name) == (None, "Neil Diamond")
|
|
assert song.album
|
|
assert (song.album.id, song.album.name) == ("88", "50th Anniversary Collection")
|
|
assert song.genre and song.genre.name == "Pop"
|
|
|
|
|
|
def test_get_genres(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_genres"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
genres = adapter.get_genres()
|
|
|
|
assert len(genres) == 2
|
|
assert [g.name for g in genres] == ["Country", "Pop"]
|
|
|
|
|
|
def test_get_artists(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_artists"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
artists = adapter.get_artists()
|
|
assert len(artists) == 7
|
|
assert {a.name for a in artists} == {
|
|
"Adele",
|
|
"Austin French",
|
|
"The Afters",
|
|
"The Band Perry",
|
|
"Basshunter",
|
|
"Zac Brown Band",
|
|
"Zach Williams",
|
|
}
|
|
|
|
|
|
def test_get_ignored_articles(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_artists"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
ignored_articles = adapter.get_ignored_articles()
|
|
assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}
|
|
|
|
|
|
def test_get_ignored_articles_from_cached_get_artists(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_artists"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
adapter.get_artists()
|
|
ignored_articles = adapter.get_ignored_articles()
|
|
assert ignored_articles == {"The", "El", "La", "Los", "Las", "Le", "Les"}
|
|
|
|
|
|
def test_get_artist(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_artist"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
artist = adapter.get_artist("3")
|
|
assert artist.album_count == 1
|
|
assert artist.albums and len(artist.albums) == 1
|
|
assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
|
|
assert artist.artist_image_url == "ar-3"
|
|
assert artist.biography and len(artist.biography) > 0
|
|
assert artist.name == "Kane Brown"
|
|
assert artist.similar_artists
|
|
assert len(artist.similar_artists) == 20
|
|
assert (first_similar := artist.similar_artists[0])
|
|
assert first_similar
|
|
assert first_similar.name == "Luke Combs"
|
|
assert first_similar.artist_image_url == "ar-158"
|
|
|
|
|
|
def test_get_artist_with_good_image_url(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_artist_good_image_url"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
artist = adapter.get_artist("3")
|
|
assert artist.album_count == 1
|
|
assert artist.albums and len(artist.albums) == 1
|
|
assert artist.biography and len(artist.biography) > 0
|
|
assert artist.name == "Kane Brown"
|
|
assert ("3", "Kane Brown") == (artist.albums[0].id, artist.albums[0].name)
|
|
assert (
|
|
artist.artist_image_url
|
|
== "http://entertainermag.com/wp-content/uploads/2017/04/Kane-Brown-Web-Optimized.jpg" # noqa: E501
|
|
)
|
|
|
|
|
|
def test_get_play_queue(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_play_queue"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
play_queue = adapter.get_play_queue()
|
|
assert play_queue
|
|
assert play_queue.current_index and play_queue.current_index == 1
|
|
assert play_queue.position == timedelta(milliseconds=98914)
|
|
assert play_queue.username == "sumner"
|
|
assert play_queue.changed == datetime(
|
|
2020, 5, 12, 5, 16, 32, 114000, tzinfo=timezone.utc
|
|
)
|
|
assert play_queue.songs and len(play_queue.songs) == 5
|
|
|
|
song = play_queue.songs[0]
|
|
assert song.album and song.album.name == "Despacito"
|
|
assert song.artist and song.artist.name == "Peter Bence"
|
|
assert song.genre and song.genre.name == "Classical"
|
|
|
|
|
|
def test_get_album(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_album"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
album = adapter.get_album("243")
|
|
assert (
|
|
album.id,
|
|
album.name,
|
|
album.cover_art,
|
|
album.song_count,
|
|
album.year,
|
|
album.duration,
|
|
) == (
|
|
"243",
|
|
"What You See Is What You Get",
|
|
"al-243",
|
|
17,
|
|
2019,
|
|
timedelta(seconds=3576),
|
|
)
|
|
assert album.artist
|
|
assert (album.artist.id, album.artist.name) == ("158", "Luke Combs")
|
|
assert album.genre and album.genre.name == "Country"
|
|
assert album.songs
|
|
assert len(album.songs) == 17
|
|
assert [s.title for s in album.songs] == [
|
|
"Beer Never Broke My Heart",
|
|
"Refrigerator Door",
|
|
"Even Though I'm Leaving",
|
|
"Lovin' On You",
|
|
"Moon Over Mexico",
|
|
"1, 2 Many",
|
|
"Blue Collar Boys",
|
|
"New Every Day",
|
|
"Reasons",
|
|
"Every Little Bit Helps",
|
|
"Dear Today",
|
|
"What You See Is What You Get",
|
|
"Does To Me",
|
|
"Angels Workin' Overtime",
|
|
"All Over Again",
|
|
"Nothing Like You",
|
|
"Better Together",
|
|
]
|
|
|
|
|
|
def test_get_music_directory(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("get_music_directory"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
directory = adapter.get_directory("3")
|
|
assert directory.id == "60"
|
|
assert directory.name == "Luke Bryan"
|
|
assert directory.parent_id == "root"
|
|
assert directory.children and len(directory.children) == 1
|
|
child = directory.children[0]
|
|
assert isinstance(child, SubsonicAPI.Directory)
|
|
assert child.id == "542"
|
|
assert child.name == "Crash My Party"
|
|
assert child.parent_id == "60"
|
|
|
|
for filename, data in mock_data_files("get_indexes"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
directory = adapter.get_directory("root")
|
|
assert directory.id == "root"
|
|
assert directory.parent_id is None
|
|
assert len(directory.children) == 7
|
|
child = directory.children[0]
|
|
assert isinstance(child, SubsonicAPI.Directory)
|
|
assert child.id == "73"
|
|
assert child.name == "The Afters"
|
|
assert child.parent_id == "root"
|
|
|
|
|
|
def test_search(adapter: SubsonicAdapter):
|
|
for filename, data in mock_data_files("search3"):
|
|
logging.info(filename)
|
|
logging.debug(data)
|
|
adapter._set_mock_data(data)
|
|
|
|
search_results = adapter.search("3")
|
|
assert len(search_results._songs) == 7
|
|
assert len(search_results._artists) == 2
|
|
assert len(search_results._albums) == 4
|