diff --git a/app/api/__init__.py b/app/api/__init__.py index ada87fc..609c213 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -22,6 +22,7 @@ from app.api import ( plugins, logger, home, + getall, ) @@ -60,4 +61,7 @@ def create_api(): # Home app.register_blueprint(home.api_bp) + # Flask Restful + app.register_blueprint(getall.api_bp) + return app diff --git a/app/api/album.py b/app/api/album.py index c9f1815..5f2111d 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -67,11 +67,7 @@ def get_album_tracks_and_info(): album.count = len(tracks) album.get_date_from_tracks(tracks) - - try: - album.duration = sum(t.duration for t in tracks) - except AttributeError: - album.duration = 0 + album.duration = sum(t.duration for t in tracks) album.check_is_single(tracks) diff --git a/app/api/getall/__init__.py b/app/api/getall/__init__.py new file mode 100644 index 0000000..e111d98 --- /dev/null +++ b/app/api/getall/__init__.py @@ -0,0 +1,10 @@ +from flask import Blueprint +from flask_restful import Api + +from .resources import Albums + +api_bp = Blueprint("getall", __name__, url_prefix="/getall") +api = Api(api_bp) + + +api.add_resource(Albums, "/") diff --git a/app/api/getall/resources.py b/app/api/getall/resources.py new file mode 100644 index 0000000..c419967 --- /dev/null +++ b/app/api/getall/resources.py @@ -0,0 +1,93 @@ +from flask_restful import Resource, reqparse +from datetime import datetime +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore + +from app.serializers.album import serialize_for_card as serialize_album +from app.serializers.artist import serialize_for_card as serialize_artist +from app.utils import format_number +from app.utils.dates import ( + create_new_date, + date_string_to_time_passed, + seconds_to_time_string, +) + +parser = reqparse.RequestParser() + +parser.add_argument("start", type=int, default=0, location="args") +parser.add_argument("limit", type=int, default=20, location="args") +parser.add_argument("sortby", type=str, default="created_date", location="args") +parser.add_argument("reverse", type=str, default="1", location="args") + + +class Albums(Resource): + def get(self, itemtype: str): + is_albums = itemtype == "albums" + is_artists = itemtype == "artists" + + items = AlbumStore.albums + + if is_artists: + items = ArtistStore.artists + + args = parser.parse_args() + + start = args["start"] + limit = args["limit"] + sort = args["sortby"] + reverse = args["reverse"] == "1" + + if sort == "": + sort = "created_date" + + sort_is_count = sort == "count" + sort_is_duration = sort == "duration" + + sort_is_date = is_albums and sort == "date" + sort_is_create_date = is_albums and sort == "created_date" + sort_is_artist = is_albums and sort == "albumartists" + + sort_is_artist_trackcount = is_artists and sort == "trackcount" + sort_is_artist_albumcount = is_artists and sort == "albumcount" + + lambda_sort = lambda x: getattr(x, sort) + if sort_is_artist: + lambda_sort = lambda x: getattr(x, sort)[0].name + + sorted_items = sorted(items, key=lambda_sort, reverse=reverse) + items = sorted_items[start : start + limit] + + album_list = [] + + for item in items: + item_dict = serialize_album(item) if is_albums else serialize_artist(item) + + if sort_is_date: + item_dict["help_text"] = item.date + + if sort_is_create_date: + date = create_new_date(datetime.fromtimestamp(item.created_date)) + timeago = date_string_to_time_passed(date) + item_dict["help_text"] = timeago + + if sort_is_count: + item_dict[ + "help_text" + ] = f"{format_number(item.count)} track{'' if item.count == 1 else 's'}" + + if sort_is_duration: + item_dict["help_text"] = seconds_to_time_string(item.duration) + + if sort_is_artist_trackcount: + item_dict[ + "help_text" + ] = f"{format_number(item.trackcount)} track{'' if item.trackcount == 1 else 's'}" + + if sort_is_artist_albumcount: + item_dict[ + "help_text" + ] = f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}" + + album_list.append(item_dict) + + return {"items": album_list, "total": len(sorted_items)} diff --git a/app/lib/albumslib.py b/app/lib/albumslib.py index e3dc6f9..c8f9802 100644 --- a/app/lib/albumslib.py +++ b/app/lib/albumslib.py @@ -4,14 +4,41 @@ Contains methods relating to albums. from dataclasses import asdict from typing import Any +from itertools import groupby -from app.logger import log from app.models.track import Track from app.store.albums import AlbumStore from app.store.tracks import TrackStore +def create_albums(): + """ + Creates albums from the tracks in the store. + """ + + # group all tracks by albumhash + tracks = TrackStore.tracks + tracks = sorted(tracks, key=lambda t: t.albumhash) + grouped = groupby(tracks, lambda t: t.albumhash) + + # create albums from the groups + albums: list[Track] = [] + for albumhash, tracks in grouped: + count = len(list(tracks)) + duration = sum(t.duration for t in tracks) + created_date = min(t.created_date for t in tracks) + + album = AlbumStore.create_album(list(tracks)[0]) + album.set_count(count) + album.set_duration(duration) + album.set_created_date(created_date) + + albums.append(album) + + return albums + + def validate_albums(): """ Removes albums that have no tracks. diff --git a/app/lib/artistlib.py b/app/lib/artistlib.py index 4cf01e9..f49fcf0 100644 --- a/app/lib/artistlib.py +++ b/app/lib/artistlib.py @@ -1,3 +1,5 @@ +from collections import namedtuple +from itertools import groupby import os import urllib from concurrent.futures import ThreadPoolExecutor @@ -12,6 +14,7 @@ from requests.exceptions import ReadTimeout from app import settings from app.models import Album, Artist, Track from app.store import artists as artist_store +from app.store.tracks import TrackStore from app.utils.hashing import create_hash from app.utils.progressbar import tqdm @@ -190,21 +193,63 @@ def get_albumartists(albums: list[Album]) -> set[str]: def get_all_artists(tracks: list[Track], albums: list[Album]) -> list[Artist]: - artists_from_tracks = get_artists_from_tracks(tracks=tracks) - artist_from_albums = get_albumartists(albums=albums) + TrackInfo = namedtuple( + "TrackInfo", + [ + "artisthash", + "albumhash", + "trackhash", + "duration", + "artistname", + "created_date", + ], + ) + src_tracks = TrackStore.tracks + all_tracks: set[TrackInfo] = set() - artists = list(artists_from_tracks.union(artist_from_albums)) - artists.sort() + for track in src_tracks: + artist_hashes = {(a.name, a.artisthash) for a in track.artists}.union( + (a.name, a.artisthash) for a in track.albumartists + ) - # Remove duplicates - artists_dup_free = set() - artist_hashes = set() + for artist in artist_hashes: + track_info = TrackInfo( + artistname=artist[0], + artisthash=artist[1], + albumhash=track.albumhash, + trackhash=track.trackhash, + duration=track.duration, + created_date=track.created_date, + # work on created date + ) - for artist in artists: - artist_hash = create_hash(artist, decode=True) + all_tracks.add(track_info) - if artist_hash not in artist_hashes: - artists_dup_free.add(artist) - artist_hashes.add(artist_hash) + all_tracks = sorted(all_tracks, key=lambda x: x.artisthash) + all_tracks = groupby(all_tracks, key=lambda x: x.artisthash) - return [Artist(a) for a in artists_dup_free] + artists = [] + + for artisthash, tracks in all_tracks: + tracks: list[TrackInfo] = list(tracks) + + artistname = ( + sorted({t.artistname for t in tracks})[0] + if len(tracks) > 1 + else tracks[0].artistname + ) + + albumcount = len({t.albumhash for t in tracks}) + duration = sum(t.duration for t in tracks) + created_date = min(t.created_date for t in tracks) + + artist = Artist(name=artistname) + + artist.set_trackcount(len(tracks)) + artist.set_albumcount(albumcount) + artist.set_duration(duration) + artist.set_created_date(created_date) + + artists.append(artist) + + return artists diff --git a/app/models/album.py b/app/models/album.py index 52f54ab..ef26bd3 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -27,6 +27,7 @@ class Album: colors: list[str] = dataclasses.field(default_factory=list) date: str = "" + created_date: int = 0 og_title: str = "" base_title: str = "" is_soundtrack: bool = False @@ -40,6 +41,7 @@ class Album: versions: list[str] = dataclasses.field(default_factory=list) def __post_init__(self): + self.title = self.title.strip() self.og_title = self.title self.image = self.albumhash + ".webp" @@ -202,3 +204,12 @@ class Album: dates = (int(t.date) for t in tracks if t.date) self.date = datetime.datetime.fromtimestamp(min(dates)).year + + def set_count(self, count: int): + self.count = count + + def set_duration(self, duration: int): + self.duration = duration + + def set_created_date(self, created_date: int): + self.created_date = created_date diff --git a/app/models/artist.py b/app/models/artist.py index 3c684c0..c8b6eb1 100644 --- a/app/models/artist.py +++ b/app/models/artist.py @@ -36,6 +36,7 @@ class Artist(ArtistMinimal): 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) @@ -51,3 +52,6 @@ class Artist(ArtistMinimal): def set_colors(self, colors: list[str]): self.colors = colors + + def set_created_date(self, created_date: float): + self.created_date = created_date diff --git a/app/store/albums.py b/app/store/albums.py index 14cd793..693a6de 100644 --- a/app/store/albums.py +++ b/app/store/albums.py @@ -1,9 +1,10 @@ +from itertools import groupby import json import random - from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb from app.models import Album, Track +from app.utils.remove_duplicates import remove_duplicates from ..utils.hashing import create_hash from .tracks import TrackStore @@ -36,16 +37,29 @@ class AlbumStore: cls.albums = [] - albumhashes = set(t.albumhash for t in TrackStore.tracks) + tracks = remove_duplicates(TrackStore.tracks) + tracks = sorted(tracks, key=lambda t: t.albumhash) + grouped = groupby(tracks, lambda t: t.albumhash) - for albumhash in tqdm(albumhashes, desc=f"Loading albums"): - if instance_key != ALBUM_LOAD_KEY: - return + for albumhash, tracks in grouped: + tracks = list(tracks) + sample = tracks[0] - for track in TrackStore.tracks: - if track.albumhash == albumhash: - cls.albums.append(cls.create_album(track)) - break + if sample is None: + continue + + count = len(list(tracks)) + duration = sum(t.duration for t in tracks) + created_date = min(t.created_date for t in tracks) + + album = AlbumStore.create_album(sample) + + album.get_date_from_tracks(tracks) + album.set_count(count) + album.set_duration(duration) + album.set_created_date(created_date) + + cls.albums.append(album) db_albums: list[tuple] = aldb.get_all_albums() diff --git a/app/store/artists.py b/app/store/artists.py index f93991a..85a82cc 100644 --- a/app/store/artists.py +++ b/app/store/artists.py @@ -23,8 +23,9 @@ class ArtistStore: global ARTIST_LOAD_KEY ARTIST_LOAD_KEY = instance_key + print("Loading artists... ", end=" ") cls.artists = get_all_artists(TrackStore.tracks, AlbumStore.albums) - + print("Done!") for artist in ardb.get_all_artists(): if instance_key != ARTIST_LOAD_KEY: return diff --git a/app/utils/__init__.py b/app/utils/__init__.py index e69de29..84cb6dd 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -0,0 +1,11 @@ +import locale + +# Set to user's default locale: +locale.setlocale(locale.LC_ALL, "") + +# Or set to a specific locale: +# locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') + + +def format_number(number: float) -> str: + return locale.format_string("%d", number, grouping=True) diff --git a/app/utils/dates.py b/app/utils/dates.py index 6366cce..27dfc20 100644 --- a/app/utils/dates.py +++ b/app/utils/dates.py @@ -25,3 +25,23 @@ def date_string_to_time_passed(prev_date: str) -> str: diff = now - then now = pendulum.now() return now.subtract(seconds=diff).diff_for_humans() + + +def seconds_to_time_string(seconds): + """ + Converts seconds to a time string. e.g. 1 hour 2 minutes, 1 hour 2 seconds, 1 hour, 1 minute 2 seconds, etc. + """ + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + remaining_seconds = seconds % 60 + + if hours > 0: + if minutes > 0: + return f"{hours} hr{'s' if hours > 1 else ''}, {minutes} minute{'s' if minutes > 1 else ''}" + + return f"{hours} hr{'s' if hours > 1 else ''}" + + if minutes > 0: + return f"{minutes} minute{'s' if minutes > 1 else ''}" + + return f"{remaining_seconds} second{'s' if remaining_seconds > 1 else ''}"