add routes to get all albums and artists with sort

+ rewrite load all albums + artist logic with itertools.groupby
+ add a function to convert seconds to string
This commit is contained in:
mungai-njoroge 2023-12-08 09:20:51 +03:00
parent 7f87cde96c
commit 336360d509
12 changed files with 265 additions and 29 deletions

View File

@ -22,6 +22,7 @@ from app.api import (
plugins, plugins,
logger, logger,
home, home,
getall,
) )
@ -60,4 +61,7 @@ def create_api():
# Home # Home
app.register_blueprint(home.api_bp) app.register_blueprint(home.api_bp)
# Flask Restful
app.register_blueprint(getall.api_bp)
return app return app

View File

@ -67,11 +67,7 @@ def get_album_tracks_and_info():
album.count = len(tracks) album.count = len(tracks)
album.get_date_from_tracks(tracks) album.get_date_from_tracks(tracks)
album.duration = sum(t.duration for t in tracks)
try:
album.duration = sum(t.duration for t in tracks)
except AttributeError:
album.duration = 0
album.check_is_single(tracks) album.check_is_single(tracks)

View File

@ -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, "/<itemtype>")

View File

@ -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)}

View File

@ -4,14 +4,41 @@ Contains methods relating to albums.
from dataclasses import asdict from dataclasses import asdict
from typing import Any from typing import Any
from itertools import groupby
from app.logger import log
from app.models.track import Track from app.models.track import Track
from app.store.albums import AlbumStore from app.store.albums import AlbumStore
from app.store.tracks import TrackStore 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(): def validate_albums():
""" """
Removes albums that have no tracks. Removes albums that have no tracks.

View File

@ -1,3 +1,5 @@
from collections import namedtuple
from itertools import groupby
import os import os
import urllib import urllib
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -12,6 +14,7 @@ from requests.exceptions import ReadTimeout
from app import settings from app import settings
from app.models import Album, Artist, Track from app.models import Album, Artist, Track
from app.store import artists as artist_store from app.store import artists as artist_store
from app.store.tracks import TrackStore
from app.utils.hashing import create_hash from app.utils.hashing import create_hash
from app.utils.progressbar import tqdm 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]: def get_all_artists(tracks: list[Track], albums: list[Album]) -> list[Artist]:
artists_from_tracks = get_artists_from_tracks(tracks=tracks) TrackInfo = namedtuple(
artist_from_albums = get_albumartists(albums=albums) "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)) for track in src_tracks:
artists.sort() artist_hashes = {(a.name, a.artisthash) for a in track.artists}.union(
(a.name, a.artisthash) for a in track.albumartists
)
# Remove duplicates for artist in artist_hashes:
artists_dup_free = set() track_info = TrackInfo(
artist_hashes = set() 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: all_tracks.add(track_info)
artist_hash = create_hash(artist, decode=True)
if artist_hash not in artist_hashes: all_tracks = sorted(all_tracks, key=lambda x: x.artisthash)
artists_dup_free.add(artist) all_tracks = groupby(all_tracks, key=lambda x: x.artisthash)
artist_hashes.add(artist_hash)
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

View File

@ -27,6 +27,7 @@ class Album:
colors: list[str] = dataclasses.field(default_factory=list) colors: list[str] = dataclasses.field(default_factory=list)
date: str = "" date: str = ""
created_date: int = 0
og_title: str = "" og_title: str = ""
base_title: str = "" base_title: str = ""
is_soundtrack: bool = False is_soundtrack: bool = False
@ -40,6 +41,7 @@ class Album:
versions: list[str] = dataclasses.field(default_factory=list) versions: list[str] = dataclasses.field(default_factory=list)
def __post_init__(self): def __post_init__(self):
self.title = self.title.strip()
self.og_title = self.title self.og_title = self.title
self.image = self.albumhash + ".webp" self.image = self.albumhash + ".webp"
@ -202,3 +204,12 @@ class Album:
dates = (int(t.date) for t in tracks if t.date) dates = (int(t.date) for t in tracks if t.date)
self.date = datetime.datetime.fromtimestamp(min(dates)).year 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

View File

@ -36,6 +36,7 @@ class Artist(ArtistMinimal):
duration: int = 0 duration: int = 0
colors: list[str] = dataclasses.field(default_factory=list) colors: list[str] = dataclasses.field(default_factory=list)
is_favorite: bool = False is_favorite: bool = False
created_date: float = 0.0
def __post_init__(self): def __post_init__(self):
super(Artist, self).__init__(self.name) super(Artist, self).__init__(self.name)
@ -51,3 +52,6 @@ class Artist(ArtistMinimal):
def set_colors(self, colors: list[str]): def set_colors(self, colors: list[str]):
self.colors = colors self.colors = colors
def set_created_date(self, created_date: float):
self.created_date = created_date

View File

@ -1,9 +1,10 @@
from itertools import groupby
import json import json
import random import random
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb
from app.models import Album, Track from app.models import Album, Track
from app.utils.remove_duplicates import remove_duplicates
from ..utils.hashing import create_hash from ..utils.hashing import create_hash
from .tracks import TrackStore from .tracks import TrackStore
@ -36,16 +37,29 @@ class AlbumStore:
cls.albums = [] 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"): for albumhash, tracks in grouped:
if instance_key != ALBUM_LOAD_KEY: tracks = list(tracks)
return sample = tracks[0]
for track in TrackStore.tracks: if sample is None:
if track.albumhash == albumhash: continue
cls.albums.append(cls.create_album(track))
break 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() db_albums: list[tuple] = aldb.get_all_albums()

View File

@ -23,8 +23,9 @@ class ArtistStore:
global ARTIST_LOAD_KEY global ARTIST_LOAD_KEY
ARTIST_LOAD_KEY = instance_key ARTIST_LOAD_KEY = instance_key
print("Loading artists... ", end=" ")
cls.artists = get_all_artists(TrackStore.tracks, AlbumStore.albums) cls.artists = get_all_artists(TrackStore.tracks, AlbumStore.albums)
print("Done!")
for artist in ardb.get_all_artists(): for artist in ardb.get_all_artists():
if instance_key != ARTIST_LOAD_KEY: if instance_key != ARTIST_LOAD_KEY:
return return

View File

@ -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)

View File

@ -25,3 +25,23 @@ def date_string_to_time_passed(prev_date: str) -> str:
diff = now - then diff = now - then
now = pendulum.now() now = pendulum.now()
return now.subtract(seconds=diff).diff_for_humans() 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 ''}"