Skip to content

Commit

Permalink
Rework the user management methods, model and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Kutu committed Jan 9, 2024
1 parent 6201b69 commit 486c5be
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 200 deletions.
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11.0
8 changes: 6 additions & 2 deletions src/knuckles/media_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def remove_rating(self, id_: str) -> "Subsonic":
return self.subsonic

def scrobble(
self, id_: list[str], time: datetime, submission: bool = True
self, id_: list[str], time: list[datetime], submission: bool = True
) -> "Subsonic":
"""Calls to the "scrobble" endpoint of the API
Expand All @@ -155,7 +155,11 @@ def scrobble(
"scrobble",
# Multiply by 1000 because the API uses
# milliseconds instead of seconds for UNIX time
{"id": id_, "time": int(time.timestamp() * 1000), "submission": submission},
{
"id": id_,
"time": [int(seconds.timestamp()) * 1000 for seconds in time],
"submission": submission,
},
)

return self.subsonic
97 changes: 59 additions & 38 deletions src/knuckles/media_retrieval.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from enum import Enum
from mimetypes import guess_extension
from pathlib import Path
from typing import Any
from typing import Any, Callable

from requests import Response
from requests.models import PreparedRequest
Expand Down Expand Up @@ -64,6 +64,20 @@ def _download_file(response: Response, downloaded_file_path: Path) -> Path:

return downloaded_file_path

@classmethod
def _handle_download(
cls,
response: Response,
file_or_directory_path: Path,
determinate_filename: Callable[[Response], str],
) -> Path:
if not file_or_directory_path.is_dir():
return cls._download_file(response, file_or_directory_path)

filename = determinate_filename(response)

return cls._download_file(response, file_or_directory_path / filename)

def stream(
self,
id_: str,
Expand Down Expand Up @@ -126,20 +140,26 @@ def download(self, id_: str, file_or_directory_path: Path) -> Path:

response = self.api.raw_request("download", {"id": id_})

if not file_or_directory_path.is_dir():
return self._download_file(response, file_or_directory_path)
def determinate_filename(file_response: Response) -> str:
filename = (
file_response.headers["Content-Disposition"]
.split("filename=")[1]
.strip()
)

filename = response.headers["Content-Disposition"].split("filename=")[1].strip()
# Remove leading quote char
if filename[0] == '"':
filename = filename[1:]

# Remove leading quote char
if filename[0] == '"':
filename = filename[1:]
# Remove trailing quote char
if filename[-1] == '"':
filename = filename[:-1]

# Remove trailing quote char
if filename[-1] == '"':
filename = filename[:-1]
return filename

return self._download_file(response, file_or_directory_path / filename)
return self._handle_download(
response, file_or_directory_path, determinate_filename
)

def hls(
self,
Expand Down Expand Up @@ -195,20 +215,21 @@ def get_captions(
{"id": id_, "format": subtitles_file_format.value},
)

if not file_or_directory_path.is_dir():
return self._download_file(response, file_or_directory_path)

mime_type = response.headers["content-type"].partition(";")[0].strip()
def determinate_filename(file_response: Response) -> str:
mime_type = file_response.headers["content-type"].partition(";")[0].strip()

# As application/x-subrip is not a valid MIME TYPE so a manual check is done
if mime_type != "application/x-subrip":
file_extension = guess_extension(mime_type)
else:
file_extension = ".srt"
# application/x-subrip is not a valid MIME TYPE so a manual check is needed
file_extension: str | None = None
if mime_type == "application/x-subrip":
file_extension = ".srt"
else:
file_extension = guess_extension(mime_type)

filename = id_ + file_extension if file_extension else id_
return id_ + file_extension if file_extension else id_

return self._download_file(response, file_or_directory_path / filename)
return self._handle_download(
response, file_or_directory_path, determinate_filename
)

def get_cover_art(
self, id_: str, file_or_directory_path: Path, size: int | None = None
Expand All @@ -230,16 +251,16 @@ def get_cover_art(

response = self.api.raw_request("getCoverArt", {"id": id_, "size": size})

if not file_or_directory_path.is_dir():
return self._download_file(response, file_or_directory_path)
def determinate_filename(file_response: Response) -> str:
file_extension = guess_extension(
file_response.headers["content-type"].partition(";")[0].strip()
)

file_extension = guess_extension(
response.headers["content-type"].partition(";")[0].strip()
)
return id_ + file_extension if file_extension else id_

filename = id_ + file_extension if file_extension else id_

return self._download_file(response, file_or_directory_path / filename)
return self._handle_download(
response, file_or_directory_path, determinate_filename
)

def get_lyrics(self) -> None:
...
Expand All @@ -260,13 +281,13 @@ def get_avatar(self, username: str, file_or_directory_path: Path) -> Path:

response = self.api.raw_request("getAvatar", {"username": username})

if not file_or_directory_path.is_dir():
return self._download_file(response, file_or_directory_path)

file_extension = guess_extension(
response.headers["content-type"].partition(";")[0].strip()
)
def determinate_filename(file_response: Response) -> str:
file_extension = guess_extension(
file_response.headers["content-type"].partition(";")[0].strip()
)

filename = username + file_extension if file_extension else username
return username + file_extension if file_extension else username

return self._download_file(response, file_or_directory_path / filename)
return self._handle_download(
response, file_or_directory_path, determinate_filename
)
2 changes: 1 addition & 1 deletion src/knuckles/models/play_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(
self.__subsonic = subsonic
self.current = Song(self.__subsonic, current) if current else None
self.position = position
self.user = User(username) if username else None
self.user = User(self.__subsonic, username) if username else None
self.changed = parser.parse(changed) if changed else None
self.changed_by = changedBy
self.songs = (
Expand Down
4 changes: 2 additions & 2 deletions src/knuckles/models/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ def __init__(
self.created = parser.parse(created) if created else None
self.changed = parser.parse(changed) if changed else None
self.comment = comment
self.owner = User(owner, subsonic=self.__subsonic) if owner else None
self.owner = User(self.__subsonic, owner) if owner else None
self.public = public
self.cover_art = CoverArt(coverArt) if coverArt else None
self.allowed_users = (
[User(username) for username in allowedUser] if allowedUser else None
[User(self.__subsonic, username) for username in allowedUser] if allowedUser else None
)
self.songs = (
[Song(self.__subsonic, **song) for song in entry] if entry else None
Expand Down
2 changes: 1 addition & 1 deletion src/knuckles/models/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __init__(
self.id = id
self.url = url
self.description = description
self.user = User(username) if username else None
self.user = User(self.__subsonic, username) if username else None
self.created = parser.parse(created) if created else None
self.expires = parser.parse(expires) if expires else None
self.last_visited = parser.parse(lastVisited) if lastVisited else None
Expand Down
2 changes: 1 addition & 1 deletion src/knuckles/models/song.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,6 @@ def scrobble(self, time: datetime, submission: bool = True) -> Self:
:rtype: Self
"""

self.__subsonic.media_annotation.scrobble([self.id], time, submission)
self.__subsonic.media_annotation.scrobble([self.id], [time], submission)

return self
154 changes: 78 additions & 76 deletions src/knuckles/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,87 +11,47 @@ class User:

def __init__(
self,
# Internal
subsonic: "Subsonic",
# Subsonic fields
username: str,
password: str | None = None,
email: str | None = None,
scrobblingEnabled: bool = False,
adminRole: bool = False,
settingsRole: bool = False,
downloadRole: bool = False,
uploadRole: bool = False,
playlistRole: bool = False,
coverArtRole: bool = False,
commentRole: bool = False,
podcastRole: bool = False,
streamRole: bool = False,
jukeboxRole: bool = False,
shareRole: bool = False,
videoConversionRole: bool = False,
# Internal
subsonic: "Subsonic | None" = None,
ldap_authenticated: bool | None = None,
admin_role: bool | None = None,
settings_role: bool | None = None,
stream_role: bool | None = None,
jukebox_role: bool | None = None,
download_role: bool | None = None,
upload_role: bool | None = None,
playlist_role: bool | None = None,
cover_art_role: bool | None = None,
comment_role: bool | None = None,
podcast_role: bool | None = None,
share_role: bool | None = None,
video_conversion_role: bool | None = None,
music_folder_id: list[str] | None = None,
max_bit_rate: int | None = None,
) -> None:
"""Representation of all the data related to a user in Subsonic.
:param username: The username of the user
:type username: str
:param email: The email of the user
:type email: str
:param scrobblingEnabled: If the user can do scrobbling,
defaults to False.
:type scrobblingEnabled: bool, optional
:param adminRole: If the user has admin privileges,
overrides all the rest of roles,defaults to False.
:type adminRole: bool, optional
:param settingsRole: If the user can modify global settings,
defaults to False.
:type settingsRole: bool, optional
:param downloadRole: If the user can download songs, defaults to False.
:type downloadRole: bool, optional
:param uploadRole: If the user can upload data to the server,
defaults to False.
:type uploadRole: bool, optional
:param playlistRole: If the user can use playlist, defaults to False.
:type playlistRole: bool, optional
:param coverArtRole: If the user can access cover arts,
defaults to False.
:type coverArtRole: bool, optional
:param commentRole: If the user can do comments, defaults to False.
:type commentRole: bool, optional
:param podcastRole: If the user can listen to podcasts,
defaults to False.
:type podcastRole: bool, optional
:param streamRole: If the user can listen media with streaming,
defaults to False.
:type streamRole: bool, optional
:param jukeboxRole: If the user can use the jukebox, defaults to False
:type jukeboxRole: bool, optional
:param shareRole: If the user can use sharing capabilities,
defaults to False
:type shareRole: bool, optional
:param videoConversionRole: If the user can do video conversion,
defaults to False
:type videoConversionRole: bool, optional
:param subsonic: The subsonic object to make all the internal requests with it,
defaults to None
:type subsonic: Subsonic | None, optional
"""

self.subsonic = subsonic
self.username = username
self.password = password
self.email = email
self.scrobbling_enabled = scrobblingEnabled
self.admin_role = adminRole
self.settings_role = settingsRole
self.download_role = downloadRole
self.upload_role = uploadRole
self.playlist_role = playlistRole
self.cover_art_role = coverArtRole
self.comment_role = commentRole
self.podcast_role = podcastRole
self.stream_role = streamRole
self.jukebox_role = jukeboxRole
self.share_role = shareRole
self.video_conversion_role = videoConversionRole
self.ldap_authenticated = ldap_authenticated
self.admin_role = admin_role
self.settings_role = settings_role
self.stream_role = stream_role
self.jukebox_role = jukebox_role
self.download_role = download_role
self.upload_role = upload_role
self.playlist_role = playlist_role
self.cover_art_role = cover_art_role
self.comment_role = comment_role
self.podcast_role = podcast_role
self.share_role = share_role
self.video_conversion_role = video_conversion_role
self.music_folder_id = music_folder_id
self.max_bit_rate = max_bit_rate

def __check_api_access(self) -> None:
"""Check if the object has a valid subsonic property
Expand Down Expand Up @@ -135,7 +95,30 @@ def create(self) -> Self:

self.__check_api_access()

self.subsonic.user_management.create_user(self) # type: ignore[union-attr]
#! TODO This is bad
if not self.password or not self.email:
raise NoApiAccess()

self.subsonic.user_management.create_user(
self.username,
self.password,
self.email,
self.ldap_authenticated,
self.admin_role,
self.settings_role,
self.stream_role,
self.jukebox_role,
self.download_role,
self.upload_role,
self.playlist_role,
self.cover_art_role,
self.comment_role,
self.podcast_role,
self.share_role,
self.video_conversion_role,
self.music_folder_id,
self.max_bit_rate,
) # type: ignore[union-attr]

return self

Expand All @@ -152,7 +135,26 @@ def update(self) -> Self:

self.__check_api_access()

self.subsonic.user_management.update_user(self) # type: ignore[union-attr]
self.subsonic.user_management.update_user(
self.username,
self.password,
self.email,
self.ldap_authenticated,
self.admin_role,
self.settings_role,
self.stream_role,
self.jukebox_role,
self.download_role,
self.upload_role,
self.playlist_role,
self.cover_art_role,
self.comment_role,
self.podcast_role,
self.share_role,
self.video_conversion_role,
self.music_folder_id,
self.max_bit_rate,
) # type: ignore[union-attr]

return self

Expand Down
Loading

0 comments on commit 486c5be

Please sign in to comment.