diff --git a/app/api/__init__.py b/app/api/__init__.py index e7e75b8..ada87fc 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -21,6 +21,7 @@ from app.api import ( lyrics, plugins, logger, + home, ) @@ -56,4 +57,7 @@ def create_api(): # Logger app.register_blueprint(logger.api_bp) + # Home + app.register_blueprint(home.api_bp) + return app diff --git a/app/api/home/__init__.py b/app/api/home/__init__.py new file mode 100644 index 0000000..dd4f4a0 --- /dev/null +++ b/app/api/home/__init__.py @@ -0,0 +1,10 @@ +from flask import Blueprint +from flask_restful import Api + +from .recents import RecentlyAdded + +api_bp = Blueprint("home", __name__, url_prefix="/home") +api = Api(api_bp) + + +api.add_resource(RecentlyAdded, "/recents/added") diff --git a/app/api/home/recents.py b/app/api/home/recents.py new file mode 100644 index 0000000..edf8857 --- /dev/null +++ b/app/api/home/recents.py @@ -0,0 +1,18 @@ +from flask_restful import Resource, reqparse + +from app.lib.home.recents import get_recent_items + +parser = reqparse.RequestParser() + +parser.add_argument("limit", type=int, required=False, default=7, location="args") + + +class RecentlyAdded(Resource): + def get(self): + cutoff = 14 + + args = parser.parse_args() + limit = args["limit"] + print(limit) + + return {"items": get_recent_items(cutoff)[:limit], "cutoff": cutoff} diff --git a/app/api/logger/__init__.py b/app/api/logger/__init__.py index 65381d7..033bfd3 100644 --- a/app/api/logger/__init__.py +++ b/app/api/logger/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint, request +from flask import Blueprint from flask_restful import Api from app.api.logger.tracks import LogTrack diff --git a/app/lib/home/recents.py b/app/lib/home/recents.py new file mode 100644 index 0000000..3f4507d --- /dev/null +++ b/app/lib/home/recents.py @@ -0,0 +1,150 @@ +import os + +from flask import g +from app.models.track import Track +from app.store.tracks import TrackStore +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore + +from app.serializers.track import serialize_track +from app.serializers.album import album_serializer +from app.serializers.artist import serialize_for_card + +from itertools import groupby +from datetime import datetime, timedelta + + +def timestamp_from_days_ago(days_ago): + current_datetime = datetime.now() + delta = timedelta(days=days_ago) + past_timestamp = current_datetime - delta + + past_timestamp = int(past_timestamp.timestamp()) + + return past_timestamp + + +group_type = tuple[str, list[Track]] + + +def calc_based_on_percent(items: list[str], total: int): + """ + Checks if items is more than 85% of total items. Returns a boolean and the most common item. + """ + most_common = max(items, key=items.count) + most_common_count = items.count(most_common) + + return most_common_count / total >= 0.85, most_common + + +def check_is_album_folder(group: group_type): + key, group_ = group + albumhashes = [t.albumhash for t in group_] + return calc_based_on_percent(albumhashes, len(group_)) + + +def check_is_artist_folder(group: group_type): + key, group_ = group + artisthashes = "-".join(t.artist_hashes for t in group_).split("-") + return calc_based_on_percent(artisthashes, len(group_)) + + +def check_is_track_folder(group: group_type): + key, group_ = group + + # is more of a playlist + if len(group_) >= 3: + return False + + return [ + { + "type": "track", + "item": serialize_track(t, to_remove={"created_date"}), + } + for t in group_ + ] + + +def check_folder_type(group_: group_type) -> str: + # check if all tracks in group have the same albumhash + # if so, return "album" + key, tracks = group_ + + if len(tracks) == 1: + return { + "type": "track", + "item": serialize_track(tracks[0], to_remove={"created_date"}), + } + + is_album, albumhash = check_is_album_folder(group_) + if is_album: + album = AlbumStore.get_album_by_hash(albumhash) + return { + "type": "album", + "item": album_serializer( + album, + to_remove={ + "genres", + "og_title", + "date", + "duration", + "count", + "albumartists_hashes", + "base_title", + }, + ), + } + + is_artist, artisthash = check_is_artist_folder(group_) + if is_artist: + artist = ArtistStore.get_artist_by_hash(artisthash) + artist = serialize_for_card(artist) + artist["trackcount"] = len(tracks) + + return { + "type": "artist", + "item": artist, + } + + is_track_folder = check_is_track_folder(group_) + return ( + is_track_folder + if is_track_folder + else { + "type": "folder", + "item": { + "path": key, + "count": len(tracks), + }, + } + ) + + +def group_track_by_folders(tracks: Track) -> (str, list[Track]): + tracks = sorted(tracks, key=lambda t: t.folder) + groups = groupby(tracks, lambda t: t.folder) + groups = ((k, list(g)) for k, g in groups) + + # sort groups by last modified date + return sorted(groups, key=lambda g: os.path.getctime(g[0]), reverse=True) + + +def get_recent_items(cutoff_days: int): + timestamp = timestamp_from_days_ago(cutoff_days) + + tracks = (t for t in TrackStore.tracks if t.created_date > timestamp) + tracks = sorted(tracks, key=lambda t: t.created_date) + + groups = group_track_by_folders(tracks) + + recent_items = [] + + for group in groups: + item = check_folder_type(group) + + if item not in recent_items: + recent_items.append(item) if type(item) == dict else recent_items.extend( + item + ) + + return recent_items diff --git a/app/models/track.py b/app/models/track.py index 91ad2b4..0e6811f 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from pathlib import Path from app.settings import SessionVarKeys, get_flag from app.utils.hashing import create_hash @@ -47,6 +48,10 @@ class Track: og_title: str = "" og_album: str = "" + created_date: float = 0.0 + + def set_created_date(self): + self.created_date = Path(self.filepath).stat().st_ctime def __post_init__(self): self.og_title = self.title @@ -117,6 +122,7 @@ class Track: self.genre = [g.strip() for g in self.genre] self.recreate_hash() + self.set_created_date() def recreate_hash(self): """ diff --git a/app/serializers/album.py b/app/serializers/album.py index 64eb160..ed3aebb 100644 --- a/app/serializers/album.py +++ b/app/serializers/album.py @@ -3,7 +3,10 @@ from app.models import Album def album_serializer(album: Album, to_remove: set[str]) -> dict: - album_dict = asdict(album) + try: + album_dict = asdict(album) + except TypeError: + return {} to_remove.update(key for key in album_dict.keys() if key.startswith("is_")) for key in to_remove: diff --git a/app/serializers/artist.py b/app/serializers/artist.py index 0eed7d3..8280258 100644 --- a/app/serializers/artist.py +++ b/app/serializers/artist.py @@ -4,7 +4,10 @@ from app.models.artist import Artist def serialize_for_card(artist: Artist): - artist_dict = asdict(artist) + try: + artist_dict = asdict(artist) + except TypeError: + return {} props_to_remove = { "is_favorite", diff --git a/app/utils/parsers.py b/app/utils/parsers.py index 92b3530..129b783 100644 --- a/app/utils/parsers.py +++ b/app/utils/parsers.py @@ -8,7 +8,7 @@ def split_artists(src: str): """ Splits a string of artists into a list of artists. """ - separators: set = get_flag(SessionVarKeys.ARTIST_SEPARATORS) + separators: set = get_flag(SessionVarKeys.ARTIST_SEPARATORS) for sep in separators: src = src.replace(sep, ",")