Skip to content

Commit

Permalink
Add missing model properties
Browse files Browse the repository at this point in the history
  • Loading branch information
Kutu committed Apr 20, 2024
1 parent 517daa8 commit 227a604
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 51 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
Knuckles **only** works with music servers compatible with the REST API version 1.4.0 (Subsonic 4.2). It follows strictly the [OpenSubsonic API Spec](https://opensubsonic.netlify.app/docs/opensubsonic-api/) **without** implementing any endpoint related to video media and the legacy non-ID3 file-based system.

## Acknowledgements
Created with :heart: by [Jorge "Kutu" Dobón Blanco](dobon.dev).
Created with :heart: by [Jorge "Kutu" Dobón Blanco](https://dobon.dev).
38 changes: 2 additions & 36 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# TODO
- [ ] Add missing model properties.
- [ ] Add .
- [ ] Make a `model` class and add the following methods to it:
- [ ] `_check_api_access()`
- [ ] `_resource_not_found()`
Expand All @@ -11,6 +11,7 @@
- [ ] Add the `subsonic.system.check_subsonic_extension()` method.
- [ ] Check and rewrite all docstrings taking care about raising exceptions.
- [ ] Spin up a `MkDocs` documentation.
- [ ] Add the URL in the GitHub page.

## Implementation status
The final objetive of Knuckles to be a fully compatible implementation wrapper around the [OpenSubsonic API Spec](https://opensubsonic.netlify.app/), a superset of the [Subsonic API Spec](https://subsonic.org/pages/api.jsp) that tries to improve and extend the API without breaking changes.
Expand Down Expand Up @@ -139,38 +140,3 @@ The final objetive of Knuckles to be a fully compatible implementation wrapper a
#### Media library scanning
- [x] `getScanStatus`
- [x] `startScan`

### Missing model properties
#### Album
- [ ] `recordLabels`
- [ ] `musicBrainzId`
- [ ] `genres`
- [ ] `artists`
- [ ] `displayArtist`
- [ ] `releaseTypes`
- [ ] `moods`
- [ ] `sortName`
- [ ] `originalReleaseDate`
- [ ] `releaseDate`
- [ ] `isCompilation`
- [ ] `discTitles`

#### Artist
- [ ] `musicBrainzId`
- [ ] `sortName`
- [ ] `roles`

#### Song
- [ ] `bmp`
- [ ] `comment`
- [ ] `sortName`
- [ ] `musicBrainzId`
- [ ] `genres`
- [ ] `artists`
- [ ] `displayArtist`
- [ ] `albumArtists`
- [ ] `displayAlbumArtist`
- [ ] `contributors`
- [ ] `displayComposer`
- [ ] `moods`
- [ ] `replayGain`
64 changes: 58 additions & 6 deletions src/knuckles/models/album.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,35 @@

from .artist import Artist
from .cover_art import CoverArt
from .genre import ItemGenre

if TYPE_CHECKING:
from ..subsonic import Subsonic


class RecordLabel:
def __init__(self, name: str) -> None:
self.name = name


class Disc:
def __init__(self, disc: int, title: str) -> None:
self.disc_number = disc
self.title = title


class ReleaseDate:
def __init__(
self,
year: int,
month: int,
day: int,
) -> None:
self.year = year
self.month = month
self.day = day


class AlbumInfo:
"""Representation of all the data related to an album info in Subsonic."""

Expand Down Expand Up @@ -94,12 +118,18 @@ def __init__(
played: str | None = None,
userRating: int | None = None,
song: list[dict[str, Any]] | None = None,
# TODO WTF
# genres=None,
# isVideo=None,
# bpm=None,
# comment=None,
# musicBrainzId=None,
recordLabels: list[dict[str, Any]] | None = None,
musicBrainzId: str | None = None,
genres: list[dict[str, Any]] | None = None,
artists: list[dict[str, Any]] | None = None,
displayArtist: str | None = None,
releaseTypes: list[str] | None = None,
moods: list[str] | None = None,
sortName: str | None = None,
originalReleaseDate: dict[str, Any] | None = None,
releaseDate: dict[str, Any] | None = None,
isCompilation: bool | None = None,
discTitles: list[dict[str, Any]] | None = None,
) -> None:
"""Representation of all the data related to an album in Subsonic.
Expand Down Expand Up @@ -170,6 +200,28 @@ def __init__(
else None
)
self.info: AlbumInfo | None = None
self.record_labels = (
[RecordLabel(**record_label) for record_label in recordLabels]
if recordLabels
else None
)
self.music_brainz_id = musicBrainzId
self.genres = [ItemGenre(**genre) for genre in genres] if genres else None
self.artists = (
[Artist(self.__subsonic, **artist) for artist in artists]
if artists
else None
)
self.display_artist = displayArtist
self.release_types = releaseTypes
self.moods = moods
self.sort_name = sortName
self.original_release_date = (
ReleaseDate(**originalReleaseDate) if originalReleaseDate else None
)
self.release_date = ReleaseDate(**releaseDate) if releaseDate else None
self.is_compilation = isCompilation
self.discs = [Disc(**disc) for disc in discTitles] if discTitles else None

def generate(self) -> "Album":
"""Return a new album with all the data updated from the API,
Expand Down
6 changes: 6 additions & 0 deletions src/knuckles/models/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ def __init__(
userRating: int | None = None,
averageRating: float | None = None,
album: list[dict[str, Any]] | None = None,
musicBrainzId: str | None = None,
sortName: str | None = None,
roles: list[str] | None = None,
) -> None:
"""Representation of all the data related to an artist in Subsonic.
Expand Down Expand Up @@ -135,6 +138,9 @@ def __init__(
else None
)
self.info: ArtistInfo | None = None
self.music_brainz_id = musicBrainzId
self.sort_name = sortName
self.roles = roles

def generate(self) -> "Artist":
"""Return a new artist with all the data updated from the API,
Expand Down
5 changes: 5 additions & 0 deletions src/knuckles/models/genre.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from ..subsonic import Subsonic


class ItemGenre:
def __init__(self, name: str) -> None:
self.name = name


class Genre:
"""Representation of all the data related to a genre in Subsonic."""

Expand Down
76 changes: 68 additions & 8 deletions src/knuckles/models/song.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, Self
from typing import TYPE_CHECKING, Any, Self

# Avoid circular import error
from knuckles.models.genre import Genre
from knuckles.models.genre import Genre, ItemGenre

from ..exceptions import AlbumOrArtistArgumentsInSong, VideoArgumentsInSong
from .album import Album
Expand All @@ -16,14 +16,40 @@
from dateutil import parser


class Contributor:
def __init__(
self,
role: str,
artist: Artist,
subRole: str | None = None,
) -> None:
self.role = role
self.subrole = subRole
self.artist = artist


class ReplayGain:
def __init__(
self,
trackGain: str | None = None,
albumGain: str | None = None,
trackPeak: str | None = None,
albumPeak: str | None = None,
baseGain: str | None = None,
) -> None:
self.track_gain = trackGain
self.album_gain = albumGain
self.track_peak = trackPeak
self.album_peak = albumPeak
self.base_gain = baseGain


class Song:
"""Representation of all the data related to a song in Subsonic."""

def __init__(
self,
# Internal
subsonic: "Subsonic",
# Subsonic fields
id: str,
title: str | None = None,
isDir: bool = False,
Expand Down Expand Up @@ -55,11 +81,20 @@ def __init__(
bookmarkPosition: int | None = None,
originalWidth: None = None,
originalHeight: None = None,
# OpenSubsonic fields
played: str | None = None,
# genres=None,
# bpm=None,
# comment=None,
bpm: int | None = None,
comment: str | None = None,
sortName: str | None = None,
musicBrainzId: str | None = None,
genres: list[dict[str, Any]] | None = None,
artists: list[dict[str, Any]] | None = None,
displayArtist: str | None = None,
albumArtists: list[dict[str, Any]] | None = None,
displayAlbumArtist: str | None = None,
contributors: list[dict[str, Any]] | None = None,
displayComposer: str | None = None,
moods: list[str] | None = None,
replayGain: dict[str, Any] | None = None,
) -> None:
"""Representation of all the data related to song in Subsonic.
Expand Down Expand Up @@ -180,6 +215,31 @@ def __init__(
self.created = parser.parse(created) if created else None
self.starred = parser.parse(starred) if starred else None
self.played = parser.parse(played) if played else None
self.bpm = bpm
self.comment = comment
self.sort_name = sortName
self.music_brainz_id = musicBrainzId
self.genres = [ItemGenre(**genre) for genre in genres] if genres else None
self.artists = (
[Artist(self.__subsonic, **artist) for artist in artists]
if artists
else None
)
self.display_artist = displayArtist
self.album_artists = (
[Artist(self.__subsonic, **artist) for artist in albumArtists]
if albumArtists
else None
)
self.display_album_artist = displayAlbumArtist
self.contributors = (
[Contributor(**contributor) for contributor in contributors]
if contributors
else None
)
self.display_composer = displayComposer
self.moods = moods
self.replay_gain = ReplayGain(**replayGain) if replayGain else None

def generate(self) -> "Song":
"""Return a new song with all the data updated from the API,
Expand Down
37 changes: 37 additions & 0 deletions tests/api/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ def test_get_artist(
assert type(response.albums[0].songs) is list
assert response.albums[0].songs[0].title == song["title"]
assert response.cover_art.id == artist["coverArt"]
assert response.music_brainz_id == artist["musicBrainzId"]
assert response.sort_name == artist["sortName"]
assert response.roles == artist["roles"]


@responses.activate
Expand Down Expand Up @@ -142,6 +145,22 @@ def test_get_album(
assert response.songs[0].id == song["id"]
assert response.played == parser.parse(album["played"])
assert response.user_rating == album["userRating"]
assert type(response.record_labels) is list
assert response.record_labels[0].name == album["recordLabels"][0]["name"]
assert response.music_brainz_id == album["musicBrainzId"]
assert type(response.genres) is list
assert response.genres[0].name == album["genres"][0]["name"]
assert type(response.artists) is list
assert response.artists[0].id == album["artists"][0]["id"]
assert response.display_artist == album["displayArtist"]
assert response.release_types == album["releaseTypes"]
assert response.moods == album["moods"]
assert response.sort_name == album["sortName"]
assert response.original_release_date.year == album["originalReleaseDate"]["year"]
assert response.release_date.year == album["releaseDate"]["year"]
assert response.is_compilation == album["isCompilation"]
assert type(response.discs) is list
assert response.discs[0].disc_number == album["discTitles"][0]["disc"]


@responses.activate
Expand Down Expand Up @@ -184,6 +203,24 @@ def test_get_song(
assert response.type == "music"
assert response.bookmark_position is None
assert response.played == parser.parse(song["played"])
assert response.bpm == song["bpm"]
assert response.comment == song["comment"]
assert response.sort_name == song["sortName"]
assert response.music_brainz_id == song["musicBrainzId"]
assert type(response.genres) is list
assert response.genres[0].name == song["genres"][0]["name"]
assert type(response.artists) is list
assert response.artists[0].id == song["artists"][0]["id"]
assert response.display_artist == song["displayArtist"]
assert type(response.album_artists) is list
assert response.album_artists[0].name == song["albumArtists"][0]["name"]
assert response.display_album_artist == song["displayAlbumArtist"]
assert type(response.contributors) is list
assert response.contributors[0].role == song["contributors"][0]["role"]
assert response.display_composer == song["displayComposer"]
assert type(response.moods) is list
assert response.moods[0] == song["moods"][0]
assert response.replay_gain.track_gain == song["replayGain"]["trackGain"]


@responses.activate
Expand Down
Loading

0 comments on commit 227a604

Please sign in to comment.