def create( title: str, filepath: Path, sha256_initial: bytes, release_id: int, artists: list[dict], duration: int, track_number: str, disc_number: str, conn: Connection, sha256: Optional[bytes] = None, ) -> T: """ Create a track with the provided parameters. If a track already exists with the same SHA256, the filepath of that track will be set to the passed-in filepath and nothing else will be done. :param title: The title of the track. :param filepath: The filepath of the track. :param sha256_initial: The SHA256 of the first 1KB of the track file. :param release_id: The ID of the release that this track belongs to. :param artists: The artists that contributed to this track. A list of ``{"artist_id": int, "role": ArtistRole}`` mappings. :param duration: The duration of this track, in seconds. :param track_number: The track number. :param disc_number: The disc number. :param sha256: The full SHA256 of the track file. This should generally not be passed in--calculating a SHA256 requires a filesystem read of several MB, and we want to do that lazily. But we allow it to be passed in for testing and cases where efficiency doesn't matter. :return: The newly created track. :raises NotFound: If no release has the given release ID or no artist corresponds with any of the given artist IDs. :raises Duplicate: If a track with the same filepath already exists. The duplicate track is passed as the ``entity`` argument. """ if not librelease.exists(release_id, conn): logger.debug(f"Release {release_id} does not exist.") raise NotFound(f"Release {release_id} does not exist.") if bad_ids := [ d["artist_id"] for d in artists if not artist.exists(d["artist_id"], conn) ]: logger.debug( f"Artist(s) {', '.join(str(i) for i in bad_ids)} do not exist.") raise NotFound( f"Artist(s) {', '.join(str(i) for i in bad_ids)} do not exist.")
def resolve_star_artist(_, info: GraphQLResolveInfo, id: int) -> artist.T: art = artist.from_id(id, info.context.db) if not art: raise NotFound(f"Artist {id} does not exist.") artist.star(art, info.context.user.id, info.context.db) return art
def resolve_star_playlist(_, info: GraphQLResolveInfo, id: int) -> playlist.T: ply = playlist.from_id(id, info.context.db) if not ply: raise NotFound(f"Playlist {id} does not exist.") playlist.star(ply, info.context.user.id, info.context.db) return ply
def resolve_unstar_collection(_, info: GraphQLResolveInfo, id: int) -> collection.T: col = collection.from_id(id, info.context.db) if not col: raise NotFound(f"Collection {id} does not exist.") collection.unstar(col, info.context.user.id, info.context.db) return col
def delete_playlist_entries( obj: Any, info: GraphQLResolveInfo, playlistId: int, trackId: int, ) -> dict: for ety in pentry.from_playlist_and_track(playlistId, trackId, info.context.db): pentry.delete(ety, info.context.db) ply = playlist.from_id(playlistId, info.context.db) if not ply: raise NotFound(f"Playlist {playlistId} does not exist.") trk = track.from_id(trackId, info.context.db) if not trk: raise NotFound(f"Track {trackId} does not exist.") return {"playlist": ply, "track": trk}
def add_release(col: T, release_id: int, conn: Connection) -> T: """ Add the provided release to the provided collection. :param col: The collection to add the release to. :param release_id: The ID of the release to add. :param conn: A connection to the database. :return: The collection with the number of tracks (if present) updated. :raises NotFound: If no release has the given release ID. :raises AlreadyExists: If the release is already in the collection. """ if not release.exists(release_id, conn): logger.debug(f"Release {release_id} does not exist.") raise NotFound(f"Release {release_id} does not exist.") cursor = conn.execute( """ SELECT 1 FROM music__collections_releases WHERE collection_id = ? AND release_id = ? """, (col.id, release_id), ) if cursor.fetchone(): logger.debug(f"Release {release_id} already in collection {col.id}.") raise AlreadyExists("Release is already in collection.") conn.execute( """ INSERT INTO music__collections_releases (collection_id, release_id) VALUES (?, ?) """, (col.id, release_id), ) now = datetime.now() conn.execute( """ UPDATE music__collections SET last_updated_on = ? WHERE id = ? """, ( col.id, now, ), ) logger.info(f"Added release {release_id} to collection {col.id}.") return update_dataclass( col, num_releases=(col.num_releases + 1 if col.num_releases is not None else col.num_releases), last_updated_on=now, )
def del_release(col: T, release_id: int, conn: Connection) -> T: """ Remove the provided release from the provided collection. :param col: The collection to remove the release from. :param release_id: The release to remove. :param conn: A connection to the database. :return: The collection with the number of tracks (if present) updated. :raises NotFound: If no release has the given release ID. :raises DoesNotExist: If the release is not in the collection. """ if not release.exists(release_id, conn): logger.debug(f"Release {release_id} does not exist.") raise NotFound(f"Release {release_id} does not exist.") cursor = conn.execute( """ SELECT 1 FROM music__collections_releases WHERE collection_id = ? AND release_id = ? """, (col.id, release_id), ) if not cursor.fetchone(): logger.debug(f"Release {release_id} not in collection {col.id}.") raise DoesNotExist("Release is not in collection.") conn.execute( """ DELETE FROM music__collections_releases WHERE collection_id = ? AND release_id = ? """, (col.id, release_id), ) now = datetime.utcnow() conn.execute( """ UPDATE music__collections SET last_updated_on = ? WHERE id = ? """, ( now, col.id, ), ) logger.info(f"Deleted release {release_id} from collection {col.id}.") return update_dataclass( col, num_releases=(col.num_releases - 1 if col.num_releases is not None else col.num_releases), last_updated_on=now, )
def create(playlist_id: int, track_id: int, conn: Connection) -> T: """ Add the provided track to the provided playlist. :param playlist_id: The ID of the playlist to add the entry to. :param track_id: The ID of the track to insert. :param conn: A connection to the database. :return: The new playlist entry. :raises NotFound: If no track has the given track ID. """ if not libtrack.exists(track_id, conn): logger.debug(f"Track {track_id} does not exist.") raise NotFound(f"Track {track_id} does not exist.") if not libplaylist.exists(playlist_id, conn): logger.debug(f"Playlist {playlist_id} does not exist.") raise NotFound(f"Playlist {playlist_id} does not exist.") cursor = conn.execute( """ INSERT INTO music__playlists_tracks (playlist_id, track_id, position) VALUES (?, ?, ?) """, (playlist_id, track_id, _highest_position(playlist_id, conn) + 1), ) conn.execute( """ UPDATE music__playlists SET last_updated_on = CURRENT_TIMESTAMP WHERE id = ? """, (playlist_id,), ) logger.info( f"Created entry {cursor.lastrowid} with " f"track {track_id} and playlist {playlist_id}." ) pety = from_id(cursor.lastrowid, conn) assert pety is not None return pety
def resolve_update_track( _, info: GraphQLResolveInfo, id: int, **changes, ) -> track.T: trk = track.from_id(id, info.context.db) if not trk: raise NotFound(f"Track {id} does not exist.") return track.update(trk, info.context.db, **convert_keys_case(changes))
def resolve_update_artist( _, info: GraphQLResolveInfo, id: int, **changes, ) -> artist.T: art = artist.from_id(id, info.context.db) if not art: raise NotFound(f"Artist {id} does not exist.") return artist.update(art, info.context.db, **convert_keys_case(changes))
def resolve_update_playlist( _, info: GraphQLResolveInfo, id: int, **changes, ) -> playlist.T: ply = playlist.from_id(id, info.context.db) if not ply: raise NotFound(f"Playlist {id} does not exist.") return playlist.update(ply, info.context.db, **convert_keys_case(changes))
def resolve_update_collection( _, info: GraphQLResolveInfo, id: int, **changes, ) -> collection.T: col = collection.from_id(id, info.context.db) if not col: raise NotFound(f"Collection {id} does not exist.") return collection.update(col, info.context.db, **convert_keys_case(changes))
def resolve_del_release_from_collection( _, info: GraphQLResolveInfo, collectionId: int, releaseId: int, ) -> dict: col = collection.from_id(collectionId, info.context.db) if not col: raise NotFound(f"Collection {collectionId} does not exist.") col = collection.del_release(col, releaseId, info.context.db) rls = release.from_id(releaseId, info.context.db) return {"collection": col, "release": rls}
def resolve_del_artist_from_track( _, info: GraphQLResolveInfo, trackId: int, artistId: int, role: ArtistRole, ) -> dict: trk = track.from_id(trackId, info.context.db) if not trk: raise NotFound("Track does not exist.") trk = track.del_artist(trk, artistId, role, info.context.db) art = artist.from_id(artistId, info.context.db) return {"track": trk, "track_artist": {"role": role, "artist": art}}
def resolve_del_artist_from_release( _, info: GraphQLResolveInfo, releaseId: int, artistId: int, role: ArtistRole, ) -> dict: rls = release.from_id(releaseId, info.context.db) if not rls: raise NotFound(f"Release {releaseId} does not exist.") rls = release.del_artist(rls, artistId, role, info.context.db) art = artist.from_id(artistId, info.context.db) return {"release": rls, "artist": art}
def update(trk: T, conn: Connection, **changes) -> T: """ Update a track and persist changes to the database. To update a value, pass it in as a keyword argument. To keep the original value, do not pass in a keyword argument. :param trk: The track to update. :param conn: A connection to the database. :param title: New track title. :type title: :py:obj:`str` :param release_id: ID of the new release. :type release_id: :py:obj:`int` :param track_number: New track number. :type track_number: :py:obj:`str` :param disc_number: New disc number. :type disc_number: :py:obj:`str` :return: The updated track. :raise NotFound: If the new release ID does not exist. """ if "release_id" in changes and not librelease.exists( changes["release_id"], conn): logger.debug(f"Release {changes['release_id']} does not exist.") raise NotFound(f"Release {changes['release_id']} does not exist.") conn.execute( """ UPDATE music__tracks SET title = ?, release_id = ?, track_number = ?, disc_number = ? WHERE id = ? """, ( changes.get("title", trk.title), changes.get("release_id", trk.release_id), changes.get("track_number", trk.track_number), changes.get("disc_number", trk.disc_number), trk.id, ), ) logger.info(f"Updated track {trk.id} with {changes}.") return update_dataclass(trk, **changes)
def add_artist(trk: T, artist_id: int, role: ArtistRole, conn: Connection) -> T: """ Add the provided artist/role combo to the provided track. :param trk: The track to add the artist to. :param artist_id: The ID of the artist to add. :param role: The role to add the artist with. :param conn: A connection to the database. :return: The track that was passed in. :raises NotFound: If no artist has the given artist ID. :raises AlreadyExists: If the artist/role combo is already on the track. """ if not artist.exists(artist_id, conn): logger.debug(f"Artist {artist_id} does not exist.") raise NotFound(f"Artist {artist_id} does not exist.") cursor = conn.execute( """ SELECT 1 FROM music__tracks_artists WHERE track_id = ? AND artist_id = ? AND role = ? """, (trk.id, artist_id, role.value), ) if cursor.fetchone(): logger.debug( f"Artist {artist_id} is already on track {trk.id} with role {role}." ) raise AlreadyExists("Artist already on track with this role.") conn.execute( """ INSERT INTO music__tracks_artists (track_id, artist_id, role) VALUES (?, ?, ?) """, (trk.id, artist_id, role.value), ) logger.info( f"Added artist {artist_id} to track {trk.id} with role {role}.") return trk
def resolve_update_release( _, info: GraphQLResolveInfo, id: int, **changes, ) -> release.T: rls = release.from_id(id, info.context.db) if not rls: raise NotFound(f"Release {id} does not exist.") # Convert the "releaseDate" update from a string to a `date` object. If it is not in # the changes dict, do nothing. try: changes["releaseDate"] = date.fromisoformat(changes["releaseDate"]) except ValueError: raise ParseError("Invalid release date.") except KeyError: pass return release.update(rls, info.context.db, **convert_keys_case(changes))
def del_artist(trk: T, artist_id: int, role: ArtistRole, conn: Connection) -> T: """ Delete the provided artist/role combo to the provided track. :param trk: The track to delete the artist from. :param artist_id: The ID of the artist to delete. :param role: The role of the artist on the track. :param conn: A connection to the database. :return: The track that was passed in. :raises NotFound: If no artist has the given artist ID. :raises DoesNotExist: If the artist is not on the track. """ if not artist.exists(artist_id, conn): logger.debug(f"Artist {artist_id} does not exist.") raise NotFound(f"Artist {artist_id} does not exist.") cursor = conn.execute( """ SELECT 1 FROM music__tracks_artists WHERE track_id = ? AND artist_id = ? AND role = ? """, (trk.id, artist_id, role.value), ) if not cursor.fetchone(): logger.debug( f"Artist {artist_id} is not on track {trk.id} with role {role}.") raise DoesNotExist("No artist on track with this role.") conn.execute( """ DELETE FROM music__tracks_artists WHERE track_id = ? AND artist_id = ? AND role = ? """, (trk.id, artist_id, role.value), ) logger.info( f"Deleted artist {artist_id} from track {trk.id} with role {role}.") return trk
def create( title: str, artists: list[dict], release_type: ReleaseType, release_year: Optional[int], conn: Connection, release_date: Optional[date] = None, rating: Optional[int] = None, image_id: Optional[int] = None, allow_duplicate: bool = True, ) -> T: """ Create a release with the provided parameters. :param title: The title of the release. :param artists: The artists that contributed to this release. A list of ``{"artist_id": int, "role": ArtistRole}`` mappings. :param release_type: The type of the release. :param release_year: The year the release came out. :param conn: A connection to the database. :param release_date: The date the release came out. :param rating: A rating for the release. :param image_id: An ID of an image to serve as cover art. :param allow_duplicate: Whether to allow creation of a duplicate release or not. If this is ``False``, then ``Duplicate`` will never be raised. All releases will be created. :return: The newly created release. :raises NotFound: If the list of artists contains an invalid ID. :raises Duplicate: If a release with the same name and artists already exists. The duplicate release is passed as the ``entity`` argument. """ if bad_ids := [ d["artist_id"] for d in artists if not artist.exists(d["artist_id"], conn) ]: logger.debug( f"Artist(s) {', '.join(str(i) for i in bad_ids)} do not exist.") raise NotFound( f"Artist(s) {', '.join(str(i) for i in bad_ids)} do not exist.")
from src.graphql.util import commit from src.library import collection, release from src.library import user as libuser from src.util import convert_keys_case, del_pagination_keys gql_collection = ObjectType("Collection") gql_collections = ObjectType("Collections") @query.field("collection") def resolve_collection(obj: Any, info: GraphQLResolveInfo, id: int) -> collection.T: if col := collection.from_id(id, info.context.db): return col raise NotFound(f"Collection {id} not found.") @query.field("collectionFromNameTypeUser") def resolve_collection_from_name_type_user( obj: Any, info: GraphQLResolveInfo, name: str, type: CollectionType, user: Optional[int] = None, ) -> collection.T: if col := collection.from_name_type_user(name, type, info.context.db, user_id=user): return col
from src.library import playlist from src.library import playlist_entry as pentry from src.library import user as libuser from src.util import convert_keys_case, del_pagination_keys gql_playlist = ObjectType("Playlist") gql_playlists = ObjectType("Playlists") @query.field("playlist") def resolve_playlist(obj: Any, info: GraphQLResolveInfo, id: int) -> playlist.T: if ply := playlist.from_id(id, info.context.db): return ply raise NotFound(f"Playlist {id} not found.") @query.field("playlistFromNameTypeUser") def resolve_playlist_from_name_type_user( obj: Any, info: GraphQLResolveInfo, name: str, type: PlaylistType, user: Optional[int] = None, ) -> playlist.T: if ply := playlist.from_name_type_user(name, type, info.context.db, user_id=user): return ply
from src.graphql.mutation import mutation from src.graphql.query import query from src.graphql.util import commit from src.library import invite, user from src.util import convert_keys_case, del_pagination_keys gql_invite = ObjectType("Invite") gql_invites = ObjectType("Invites") @query.field("invite") def resolve_invite(_: Any, info: GraphQLResolveInfo, id: int) -> invite.T: if inv := invite.from_id(id, info.context.db): return inv raise NotFound(f"Invite {id} does not exist.") @query.field("invites") def resolve_invites(_: Any, info: GraphQLResolveInfo, **kwargs) -> dict: kwargs = convert_keys_case(kwargs) return { "results": invite.search(info.context.db, **kwargs), "total": invite.count(info.context.db, **del_pagination_keys(kwargs)), } @gql_invite.field("code") def resolve_releases(obj: invite.T, _: GraphQLResolveInfo) -> str: return obj.code.hex()
from src.graphql.mutation import mutation from src.graphql.query import query from src.graphql.util import commit from src.library import artist, release from src.util import convert_keys_case, del_pagination_keys gql_artist = ObjectType("Artist") gql_artists = ObjectType("Artists") @query.field("artist") def resolve_artist(obj: Any, info: GraphQLResolveInfo, id: int) -> artist.T: if art := artist.from_id(id, info.context.db): return art raise NotFound(f"Artist {id} does not exist.") @query.field("artistFromName") def resolve_artist_from_name(obj: Any, info: GraphQLResolveInfo, name: str) -> artist.T: if art := artist.from_name(name, info.context.db): return art raise NotFound(f'Artist "{name}" does not exist.') @query.field("artists") def resolve_artists(obj: Any, info: GraphQLResolveInfo, **kwargs) -> dict: kwargs = convert_keys_case(kwargs) return { "results": artist.search(info.context.db, **kwargs),
from src.errors import NotFound from src.graphql.mutation import mutation from src.graphql.query import query from src.graphql.util import commit from src.library import artist, release, track from src.util import convert_keys_case, del_pagination_keys gql_track = ObjectType("Track") @query.field("track") def resolve_track(obj: Any, info: GraphQLResolveInfo, id: int) -> track.T: if trk := track.from_id(id, info.context.db): return trk raise NotFound(f"Track {id} not found.") @query.field("tracks") def resolve_tracks(obj: Any, info: GraphQLResolveInfo, **kwargs) -> dict: kwargs = convert_keys_case(kwargs) return { "results": track.search(info.context.db, **kwargs), "total": track.count(info.context.db, **del_pagination_keys(kwargs)), } @gql_track.field("inFavorites") def resolve_in_favorites(obj: track.T, info: GraphQLResolveInfo) -> bool: return track.in_favorites(obj, info.context.user.id, info.context.db)
from src.graphql.query import query from src.graphql.util import commit from src.library import artist, collection, release, track from src.util import convert_keys_case, del_pagination_keys gql_release = ObjectType("Release") gql_releases = ObjectType("Releases") @query.field("release") def resolve_release(_: Any, info: GraphQLResolveInfo, id: int) -> release.T: if rls := release.from_id(id, info.context.db): return rls raise NotFound(f"Release {id} not found.") @query.field("releases") def resolve_releases(_: Any, info: GraphQLResolveInfo, **kwargs) -> dict: kwargs = convert_keys_case(kwargs) return { "results": release.search(info.context.db, **kwargs), "total": release.count(info.context.db, **del_pagination_keys(kwargs)), } @gql_release.field("inInbox") def resolve_in_inbox(obj: release.T, info: GraphQLResolveInfo) -> bool: return release.in_inbox(obj, info.context.user.id, info.context.db)
@mutation.field("delPlaylistEntry") @commit def delete_playlist_entry( obj: Any, info: GraphQLResolveInfo, id: int, ) -> dict: if ety := pentry.from_id(id, info.context.db): pentry.delete(ety, info.context.db) return { "playlist": playlist.from_id(ety.playlist_id, info.context.db), "track": track.from_id(ety.track_id, info.context.db), } raise NotFound(f"Playlist entry {id} does not exist.") @mutation.field("delPlaylistEntries") @commit def delete_playlist_entries( obj: Any, info: GraphQLResolveInfo, playlistId: int, trackId: int, ) -> dict: for ety in pentry.from_playlist_and_track(playlistId, trackId, info.context.db): pentry.delete(ety, info.context.db) ply = playlist.from_id(playlistId, info.context.db) if not ply: