From b8a2d2d253d9ca5559ec883a554ef5a3bfc96007 Mon Sep 17 00:00:00 2001 From: Kutu Date: Sat, 30 Sep 2023 17:34:00 +0200 Subject: [PATCH] Implement and test avatar file downloading --- src/knuckles/media_retrieval.py | 44 +++++++++++++- tests/api/test_media_retrieval.py | 98 ++++++++++++++++++++++++++----- tests/mocks/media_retrieval.py | 60 +++++++++++-------- 3 files changed, 159 insertions(+), 43 deletions(-) diff --git a/src/knuckles/media_retrieval.py b/src/knuckles/media_retrieval.py index 5c55a15..b882227 100644 --- a/src/knuckles/media_retrieval.py +++ b/src/knuckles/media_retrieval.py @@ -1,3 +1,4 @@ +from mimetypes import guess_extension from pathlib import Path from typing import Any @@ -53,7 +54,9 @@ def download(self, id: str, file_or_directory_path: Path) -> Path: response.raise_for_status() if file_or_directory_path.is_dir(): - filename = response.headers["Content-Disposition"].split("filename=")[1] + filename = ( + response.headers["Content-Disposition"].split("filename=")[1].strip() + ) # Remove leading quote char if filename[0] == '"': @@ -73,6 +76,7 @@ def download(self, id: str, file_or_directory_path: Path) -> Path: with open(download_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) + return download_path def hls(self, id: str) -> str: @@ -95,5 +99,39 @@ def get_cover_art(self) -> None: def get_lyrics(self) -> None: ... - def get_avatar(self) -> None: - ... + def get_avatar(self, username: str, file_or_directory_path: Path) -> Path: + """Calls the "getAvatar" endpoint of the API. + + :param username: The username of the profile picture to download. + :type username: str + :param file_or_directory_path: If a directory path is passed the file will be + inside of it with the filename being the name of the user and + a guessed file extension, if not the file will be saved + directly in the given path. + :type file_or_directory_path: Path + :return Returns the given path + :rtype Path + """ + + response = self.api.raw_request("getAvatar", {"username": username}) + response.raise_for_status() + + if file_or_directory_path.is_dir(): + file_extension = guess_extension( + response.headers["content-type"].partition(";")[0].strip() + ) + + filename = username + file_extension if file_extension else username + + download_path = Path( + file_or_directory_path, + filename, + ) + else: + download_path = file_or_directory_path + + with open(download_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + return download_path diff --git a/tests/api/test_media_retrieval.py b/tests/api/test_media_retrieval.py index 2076dd7..263fdc8 100644 --- a/tests/api/test_media_retrieval.py +++ b/tests/api/test_media_retrieval.py @@ -19,43 +19,57 @@ def test_stream(subsonic: Subsonic, song: dict[str, Any]) -> None: @responses.activate def test_download_with_a_given_filename( tmp_path: Path, - output_filename: str, - placeholder_text: str, - mock_download: MockDownload, + output_song_filename: str, + placeholder_data: str, + mock_download_file: MockDownload, subsonic: Subsonic, song: dict[str, Any], + song_content_type: str, ) -> None: - responses.add(mock_download(song["id"], tmp_path)) + responses.add( + mock_download_file("download", {"id": song["id"]}, tmp_path, song_content_type) + ) - subsonic.media_retrieval.download(song["id"], tmp_path / output_filename) + download_path = subsonic.media_retrieval.download( + song["id"], tmp_path / output_song_filename + ) # Check if the file data has been unaltered - with open(tmp_path / output_filename, "r") as file: - assert placeholder_text == file.read() + with open(tmp_path / output_song_filename, "r") as file: + assert placeholder_data == file.read() + + assert download_path == tmp_path / output_song_filename @responses.activate def test_download_without_a_given_filename( tmp_path: Path, - input_filename: str, - placeholder_text: str, - mock_download: MockDownload, + default_song_filename: str, + placeholder_data: str, + mock_download_file: MockDownload, subsonic: Subsonic, song: dict[str, Any], + song_content_type: str, ) -> None: responses.add( - mock_download( - song["id"], + mock_download_file( + "download", + {"id": song["id"]}, tmp_path, - headers={"Content-Disposition": f'attachment; filename="{input_filename}"'}, + song_content_type, + headers={ + "Content-Disposition": f'attachment; filename="{default_song_filename}"' + }, ) ) - subsonic.media_retrieval.download(song["id"], tmp_path) + download_path = subsonic.media_retrieval.download(song["id"], tmp_path) # Check if the file data has been unaltered - with open(tmp_path / input_filename, "r") as file: - assert placeholder_text == file.read() + with open(tmp_path / default_song_filename, "r") as file: + assert placeholder_data == file.read() + + assert download_path == tmp_path / default_song_filename def test_hls(subsonic: Subsonic, song: dict[str, Any]) -> None: @@ -63,3 +77,55 @@ def test_hls(subsonic: Subsonic, song: dict[str, Any]) -> None: assert stream_url.path == "/rest/hls.m3u8" assert parse.parse_qs(stream_url.query)["id"][0] == song["id"] + + +@responses.activate +def test_get_avatar_with_a_given_filename( + tmp_path: Path, + output_avatar_filename: str, + placeholder_data: str, + mock_download_file: MockDownload, + subsonic: Subsonic, + username: str, + avatar_content_type: str, +) -> None: + responses.add( + mock_download_file( + "getAvatar", {"username": username}, tmp_path, avatar_content_type + ) + ) + + download_path = subsonic.media_retrieval.get_avatar( + username, tmp_path / output_avatar_filename + ) + + # Check if the file data has been unaltered + with open(tmp_path / output_avatar_filename, "r") as file: + assert placeholder_data == file.read() + + assert download_path == tmp_path / output_avatar_filename + + +@responses.activate +def test_get_avatar_without_a_given_filename( + tmp_path: Path, + default_avatar_filename: str, + placeholder_data: str, + mock_download_file: MockDownload, + subsonic: Subsonic, + username: str, + avatar_content_type: str, +) -> None: + responses.add( + mock_download_file( + "getAvatar", {"username": username}, tmp_path, avatar_content_type + ) + ) + + download_path = subsonic.media_retrieval.get_avatar(username, tmp_path) + + # Check if the file data has been unaltered + with open(tmp_path / default_avatar_filename, "r") as file: + assert placeholder_data == file.read() + + assert download_path == tmp_path / default_avatar_filename diff --git a/tests/mocks/media_retrieval.py b/tests/mocks/media_retrieval.py index 1c6bb73..440d49f 100644 --- a/tests/mocks/media_retrieval.py +++ b/tests/mocks/media_retrieval.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Protocol +from typing import Protocol, Any import pytest from responses import Response @@ -7,63 +7,75 @@ from tests.conftest import MockGenerator -@pytest.fixture() -def mock_stream(): - ... - - class MockDownload(Protocol): def __call__( self, - song_id: str, + endpoint: str, + extra_params: dict[str, Any], temp_dir_path: Path, + content_type: str, headers: dict[str, str] = {}, ) -> Response: ... @pytest.fixture() -def placeholder_text() -> str: +def placeholder_data() -> str: return "Lorem Ipsum" @pytest.fixture -def input_filename() -> str: - return "input.wav" +def default_song_filename() -> str: + return "default.wav" @pytest.fixture -def output_filename() -> str: +def output_song_filename() -> str: return "output.wav" @pytest.fixture -def content_type() -> str: +def song_content_type() -> str: return "audio/wav" @pytest.fixture -def mock_download( - input_filename: str, - placeholder_text: str, - content_type: str, +def default_avatar_filename(username: str) -> str: + return f"{username}.png" + + +@pytest.fixture +def output_avatar_filename() -> str: + return "output.png" + + +@pytest.fixture +def avatar_content_type() -> str: + return "image/png" + + +@pytest.fixture +def mock_download_file( + placeholder_data: str, mock_generator: MockGenerator, ) -> MockDownload: def inner( - song_id: str, + endpoint: str, + extra_params: dict[str, Any], temp_dir_path: Path, + content_type: str, headers: dict[str, str] = {}, ): - fake_song = temp_dir_path / input_filename - fake_song.touch() + fake_file = temp_dir_path / "file.mock" + fake_file.touch() - with open(fake_song, "w") as file: - file.write(placeholder_text) + with open(fake_file, "w") as file: + file.write(placeholder_data) - with open(fake_song, "r") as file: + with open(fake_file, "r") as file: return mock_generator( - "download", - {"id": song_id}, + endpoint, + extra_params, headers=headers, content_type=content_type, body=file.read(),