From 54a1b85d8b2ebf3fb19c8eb668560a87e9a8472e Mon Sep 17 00:00:00 2001 From: cwilvx Date: Mon, 24 Jun 2024 22:08:05 +0300 Subject: [PATCH] port: artist page --- app/api/artist.py | 213 +++++++++++++++++++++++++------------------ app/db/__init__.py | 43 +++++++++ app/lib/tagger.py | 5 +- app/lib/taglib.py | 3 + app/models/album.py | 1 + app/models/artist.py | 38 +++----- app/models/track.py | 3 + 7 files changed, 187 insertions(+), 119 deletions(-) diff --git a/app/api/artist.py b/app/api/artist.py index 2d69e542..03a01874 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -2,6 +2,7 @@ Contains all the artist(s) routes. """ +from itertools import groupby import math import random from datetime import datetime @@ -9,17 +10,26 @@ from datetime import datetime from flask_jwt_extended import current_user from flask_openapi3 import APIBlueprint, Tag from pydantic import Field -from app.api.apischemas import AlbumLimitSchema, ArtistHashSchema, ArtistLimitSchema, TrackLimitSchema +from app.api.apischemas import ( + AlbumLimitSchema, + ArtistHashSchema, + ArtistLimitSchema, + TrackLimitSchema, +) +from app.config import UserConfig +from app.db import AlbumTable, ArtistTable, TrackTable from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as fmdb from app.models import Album, FavType from app.serializers.album import serialize_for_card_many from app.serializers.track import serialize_tracks + from app.store.albums import AlbumStore from app.store.artists import ArtistStore from app.store.tracks import TrackStore + bp_tag = Tag(name="Artist", description="Single artist") api = APIBlueprint("artist", __name__, url_prefix="/artist", abp_tags=[bp_tag]) @@ -34,35 +44,22 @@ def get_artist(path: ArtistHashSchema, query: TrackLimitSchema): artisthash = path.artisthash limit = query.limit - artist = ArtistStore.get_artist_by_hash(artisthash) + artist = ArtistTable.get_artist_by_hash(artisthash) + print(artist) if artist is None: return {"error": "Artist not found"}, 404 - tracks = TrackStore.get_tracks_by_artisthash(artisthash) + tracks = TrackTable.get_tracks_by_artisthash(artisthash) tcount = len(tracks) - acount = AlbumStore.count_albums_by_artisthash(artisthash) - if acount == 0 and tcount < 10: + if artist.albumcount == 0 and tcount < 10: limit = tcount - artist.set_trackcount(tcount) - artist.set_albumcount(acount) - artist.set_duration(sum(t.duration for t in tracks)) - - artist.is_favorite = favdb.check_is_favorite(artisthash, FavType.artist) - - genres = set() - - for t in tracks: - if t.genre is not None: - genres = genres.union(t.genre) - - genres = list(genres) + # artist.is_favorite = favdb.check_is_favorite(artisthash, FavType.artist) try: - min_stamp = min(t.date for t in tracks) - year = datetime.fromtimestamp(min_stamp).year + year = datetime.fromtimestamp(artist.date).year except ValueError: year = 0 @@ -73,12 +70,11 @@ def get_artist(path: ArtistHashSchema, query: TrackLimitSchema): decade = str(decade)[2:] + "s" if decade: - genres.insert(0, decade) + artist.genres.insert(0, {"name": decade, "genrehash": decade}) return { "artist": artist, "tracks": serialize_tracks(tracks[:limit]), - "genres": genres, } @@ -98,83 +94,118 @@ def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery): limit = query.limit - all_albums = AlbumStore.get_albums_by_artisthash(artisthash) - # start: check for missing albums. ie. compilations and features - all_tracks = TrackStore.get_tracks_by_artisthash(artisthash) - - track_albums = set(t.albumhash for t in all_tracks) - missing_album_hashes = track_albums.difference(set(a.albumhash for a in all_albums)) - - if len(missing_album_hashes) > 0: - missing_albums = AlbumStore.get_albums_by_hashes(list(missing_album_hashes)) - all_albums.extend(missing_albums) - - # end check - - def get_album_tracks(albumhash: str): - tracks = [t for t in all_tracks if t.albumhash == albumhash] - - if len(tracks) > 0: - return tracks - - return TrackStore.get_tracks_by_albumhash(albumhash) - - for a in all_albums: - a.check_type() - - album_tracks = get_album_tracks(a.albumhash) - - if len(album_tracks) == 0: - continue - - a.get_date_from_tracks(album_tracks) - - if a.date == 0: - AlbumStore.remove_album_by_hash(a.albumhash) - continue - - a.is_single(album_tracks) - - all_albums = sorted(all_albums, key=lambda a: str(a.date), reverse=True) - - singles = [a for a in all_albums if a.is_single] - eps = [a for a in all_albums if a.is_EP] - - def remove_EPs_and_singles(albums_: list[Album]): - albums_ = [a for a in albums_ if not a.is_single] - albums_ = [a for a in albums_ if not a.is_EP] - return albums_ - - albums = filter(lambda a: artisthash in a.albumartists_hashes, all_albums) - albums = list(albums) - albums = remove_EPs_and_singles(albums) - - compilations = [a for a in albums if a.is_compilation] - for c in compilations: - albums.remove(c) - - appearances = filter(lambda a: artisthash not in a.albumartists_hashes, all_albums) - appearances = list(appearances) - - appearances = remove_EPs_and_singles(appearances) - - artist = ArtistStore.get_artist_by_hash(artisthash) + artist = ArtistTable.get_artist_by_hash(artisthash) if artist is None: return {"error": "Artist not found"}, 404 + albums = AlbumTable.get_albums_by_artisthash(artisthash) + tracks = TrackTable.get_tracks_by_artisthash(artisthash) + + missing_albumhashes = { + t.albumhash for t in tracks if t.albumhash not in {a.albumhash for a in albums} + } + + albums.extend(AlbumTable.get_albums_by_hash(missing_albumhashes)) + albumdict = {a.albumhash: a for a in albums} + + config = UserConfig() + albumgroups = groupby(tracks, key=lambda t: t.albumhash) + for albumhash, tracks in albumgroups: + album = albumdict.get(albumhash) + + if album: + album.check_type(list(tracks), config.showAlbumsAsSingles) + + # all_albums = AlbumStore.get_albums_by_artisthash(artisthash) + # start: check for missing albums. ie. compilations and features + # all_tracks = TrackStore.get_tracks_by_artisthash(artisthash) + + # track_albums = set(t.albumhash for t in all_tracks) + # missing_album_hashes = track_albums.difference(set(a.albumhash for a in all_albums)) + + # if len(missing_album_hashes) > 0: + # missing_albums = AlbumStore.get_albums_by_hashes(list(missing_album_hashes)) + # all_albums.extend(missing_albums) + + # end check + + # def get_album_tracks(albumhash: str): + # tracks = [t for t in all_tracks if t.albumhash == albumhash] + + # if len(tracks) > 0: + # return tracks + + # return TrackStore.get_tracks_by_albumhash(albumhash) + + # for a in all_albums: + # a.check_type() + + # album_tracks = get_album_tracks(a.albumhash) + + # if len(album_tracks) == 0: + # continue + + # a.get_date_from_tracks(album_tracks) + + # if a.date == 0: + # AlbumStore.remove_album_by_hash(a.albumhash) + # continue + + # a.is_single(album_tracks) + + albums = [a for a in albumdict.values()] + all_albums = sorted(albums, key=lambda a: str(a.date), reverse=True) + + res = { + "albums": [], + "appearances": [], + "compilations": [], + "singles_and_eps": [], + } + + for album in all_albums: + if album.type == "single" or album.type == "ep": + res["singles_and_eps"].append(album) + elif album.type == "compilation": + res["compilations"].append(album) + elif album.albumhash in missing_albumhashes: + res["appearances"].append(album) + else: + res["albums"].append(album) + + # def remove_EPs_and_singles(albums_: list[Album]): + # albums_ = [a for a in albums_ if not a.type == "single"] + # albums_ = [a for a in albums_ if not a.type == "ep"] + # return albums_ + + # albums = filter(lambda a: artisthash in missing_albumhashes, all_albums) + # albums = list(albums) + # albums = remove_EPs_and_singles(albums) + + # compilations = [a for a in albums if a.is_compilation] + # for c in compilations: + # albums.remove(c) + + # appearances = filter(lambda a: artisthash not in a.albumartists_hashes, all_albums) + # appearances = list(appearances) + + # appearances = remove_EPs_and_singles(appearances) + + # artist = ArtistStore.get_artist_by_hash(artisthash) + + # if artist is None: + # return {"error": "Artist not found"}, 404 + if return_all: limit = len(all_albums) - singles_and_eps = singles + eps + # loop through the res dict and serialize the albums + for key, value in res.items(): + res[key] = serialize_for_card_many(value[:limit]) - return { - "artistname": artist.name, - "albums": serialize_for_card_many(albums[:limit]), - "singles_and_eps": serialize_for_card_many(singles_and_eps[:limit]), - "appearances": serialize_for_card_many(appearances[:limit]), - "compilations": serialize_for_card_many(compilations[:limit]), - } + res["artistname"] = artist.name + return res @api.get("//tracks") diff --git a/app/db/__init__.py b/app/db/__init__.py index 24adab64..df2132b6 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -24,6 +24,7 @@ from sqlalchemy.orm import ( from app.models import Track as TrackModel from app.models import Album as AlbumModel +from app.models import Artist as ArtistModel from app.utils.remove_duplicates import remove_duplicates @@ -100,12 +101,21 @@ class ArtistTable(Base): result = conn.execute(select(cls).offset(start).limit(limit)) return albums_to_dataclasses(result.fetchall()) + @classmethod + def get_artist_by_hash(cls, artisthash: str): + with DbManager() as conn: + result = conn.execute( + select(ArtistTable).where(ArtistTable.artisthash == artisthash) + ) + return artist_to_dataclass(result.fetchone()) + class AlbumTable(Base): __tablename__ = "album" id: Mapped[int] = mapped_column(primary_key=True) albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True) + artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True) albumhash: Mapped[str] = mapped_column(String(), unique=True, index=True) base_title: Mapped[str] = mapped_column(String()) color: Mapped[Optional[str]] = mapped_column(String()) @@ -128,6 +138,14 @@ class AlbumTable(Base): if album: return album_to_dataclass(album) + @classmethod + def get_albums_by_hash(cls, hashes: set[str]): + with DbManager() as conn: + result = conn.execute( + select(AlbumTable).where(AlbumTable.albumhash.in_(hashes)) + ) + return albums_to_dataclasses(result.fetchall()) + @classmethod def get_all(cls, start: int, limit: int): with DbManager() as conn: @@ -157,6 +175,14 @@ class AlbumTable(Base): ) return albums_to_dataclasses(result.fetchall()) + @classmethod + def get_albums_by_artisthash(cls, artisthash: str): + with DbManager() as conn: + result = conn.execute( + select(AlbumTable).where(AlbumTable.artisthashes.contains(artisthash)) + ) + return albums_to_dataclasses(result.all()) + class TrackTable(Base): __tablename__ = "track" @@ -165,6 +191,7 @@ class TrackTable(Base): album: Mapped[str] = mapped_column(String()) albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON()) albumhash: Mapped[str] = mapped_column(String(), index=True) + artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True) artists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True) bitrate: Mapped[int] = mapped_column(Integer()) copyright: Mapped[Optional[str]] = mapped_column(String()) @@ -237,10 +264,26 @@ class TrackTable(Base): if track: return track_to_dataclass(track) + @classmethod + def get_tracks_by_artisthash(cls, artisthash: str): + with DbManager() as conn: + result = conn.execute( + select(TrackTable).where(TrackTable.artists.contains(artisthash)) + ) + return tracks_to_dataclasses(result.fetchall()) + # SECTION: HELPER FUNCTIONS +def artist_to_dataclass(artist: Any): + return ArtistModel(**artist._asdict()) + + +def artists_to_dataclasses(artists: Any): + return [artist_to_dataclass(artist) for artist in artists] + + def album_to_dataclass(album: Any): return AlbumModel(**album._asdict()) diff --git a/app/lib/tagger.py b/app/lib/tagger.py index 95e1a510..9d783b62 100644 --- a/app/lib/tagger.py +++ b/app/lib/tagger.py @@ -41,6 +41,7 @@ class IndexAlbums: if track.albumhash not in albums: albums[track.albumhash] = { "albumartists": track.albumartists, + "artisthashes": [a['artisthash'] for a in track.albumartists], "albumhash": track.albumhash, "base_title": None, "color": None, @@ -107,7 +108,7 @@ class IndexArtists: "dates": [track.date], "date": None, "duration": track.duration, - "genres": [*track.genre] if track.genre else [], + "genres": track.genre if track.genre else [], "name": artist["name"], "trackcount": None, "tracks": {track.trackhash}, @@ -121,7 +122,7 @@ class IndexArtists: artist["created_dates"].append(track.last_mod) if track.genre: - artist["genres"].append(track.genre) + artist["genres"].extend(track.genre) for artist in artists.values(): diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 6c1bc9c5..03cafc1c 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -262,6 +262,9 @@ def get_tags(filepath: str): for a in split_albumartists ] + tags.artisthashes = list({a["artisthash"] for a in tags.artists + tags.albumartists}) + + # remove prod by if config.removeProdBy: new_title = remove_prod(new_title) diff --git a/app/models/album.py b/app/models/album.py index 63949b17..337f24cf 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -20,6 +20,7 @@ class Album: id: int albumartists: list[dict[str, str]] albumhash: str + artisthashes: list[str] base_title: str color: str created_date: int diff --git a/app/models/artist.py b/app/models/artist.py index 1e0446a7..0c50a60d 100644 --- a/app/models/artist.py +++ b/app/models/artist.py @@ -31,33 +31,19 @@ class ArtistMinimal: @dataclass(slots=True) -class Artist(ArtistMinimal): +class Artist: """ Artist class """ - name: str = "" - trackcount: int = 0 - albumcount: int = 0 - duration: int = 0 - colors: list[str] = dataclasses.field(default_factory=list) - is_favorite: bool = False - created_date: float = 0.0 - - def __post_init__(self): - super(Artist, self).__init__(self.name) - - def set_trackcount(self, count: int): - self.trackcount = count - - def set_albumcount(self, count: int): - self.albumcount = count - - def set_duration(self, duration: int): - self.duration = duration - - def set_colors(self, colors: list[str]): - self.colors = colors - - def set_created_date(self, created_date: float): - self.created_date = created_date + id: str + name: str + albumcount: int + artisthash: str + created_date: int + date: int + duration: int + genres: list[dict[str, str]] + name: str + trackcount: int + is_favorite: bool diff --git a/app/models/track.py b/app/models/track.py index a2261190..0a853b3e 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -27,6 +27,7 @@ class Track: album: str albumartists: list[dict[str, str]] albumhash: str + artisthashes: list[str] artists: str bitrate: int copyright: str @@ -44,9 +45,11 @@ class Track: trackhash: str extra: dict + is_favorite: bool = False _pos: int = 0 _ati: str = "" + # album: str # albumartists: str | list[ArtistMinimal] # albumhash: str