Skip to content

Commit

Permalink
Merge pull request #31 from kutu-dev/feat/avatar
Browse files Browse the repository at this point in the history
Implement and test avatar file downloading
  • Loading branch information
kutu-dev authored Sep 30, 2023
2 parents 83c2936 + b8a2d2d commit 091103a
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 43 deletions.
44 changes: 41 additions & 3 deletions src/knuckles/media_retrieval.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from mimetypes import guess_extension
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -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] == '"':
Expand All @@ -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:
Expand All @@ -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
98 changes: 82 additions & 16 deletions tests/api/test_media_retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,113 @@ 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:
stream_url = parse.urlparse(subsonic.media_retrieval.hls(song["id"]))

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
60 changes: 36 additions & 24 deletions tests/mocks/media_retrieval.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,81 @@
from pathlib import Path
from typing import Protocol
from typing import Protocol, Any

import pytest
from responses import Response

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(),
Expand Down

0 comments on commit 091103a

Please sign in to comment.