From ddfa7f1b03d3d65b0d8d341325611d4535f2f2bb Mon Sep 17 00:00:00 2001 From: mungai-njoroge Date: Sun, 3 Dec 2023 20:35:13 +0300 Subject: [PATCH] add methods to get recently played items --- app/api/home/__init__.py | 3 +- app/api/home/recents.py | 11 +- app/api/playlist.py | 87 +++---------- app/db/sqlite/logger/tracks.py | 14 ++ app/lib/home/{recents.py => recentlyadded.py} | 0 app/lib/home/recentlyplayed.py | 122 ++++++++++++++++++ app/lib/playlistlib.py | 93 +++++++++---- app/models/logger.py | 31 ++++- app/models/playlist.py | 4 +- app/serializers/playlist.py | 13 ++ 10 files changed, 277 insertions(+), 101 deletions(-) rename app/lib/home/{recents.py => recentlyadded.py} (100%) create mode 100644 app/lib/home/recentlyplayed.py create mode 100644 app/serializers/playlist.py diff --git a/app/api/home/__init__.py b/app/api/home/__init__.py index dd4f4a0..3ba70ac 100644 --- a/app/api/home/__init__.py +++ b/app/api/home/__init__.py @@ -1,10 +1,11 @@ from flask import Blueprint from flask_restful import Api -from .recents import RecentlyAdded +from .recents import RecentlyAdded, RecentlyPlayed api_bp = Blueprint("home", __name__, url_prefix="/home") api = Api(api_bp) api.add_resource(RecentlyAdded, "/recents/added") +api.add_resource(RecentlyPlayed, "/recents/played") diff --git a/app/api/home/recents.py b/app/api/home/recents.py index c0c19e1..53e0b2c 100644 --- a/app/api/home/recents.py +++ b/app/api/home/recents.py @@ -1,6 +1,7 @@ from flask_restful import Resource, reqparse -from app.lib.home.recents import get_recent_items +from app.lib.home.recentlyadded import get_recent_items +from app.lib.home.recentlyplayed import get_recently_played parser = reqparse.RequestParser() @@ -15,3 +16,11 @@ class RecentlyAdded(Resource): limit = args["limit"] return {"items": get_recent_items(cutoff, limit), "cutoff": cutoff} + + +class RecentlyPlayed(Resource): + def get(self): + args = parser.parse_args() + limit = args["limit"] + + return {"items": get_recently_played(limit)} diff --git a/app/api/playlist.py b/app/api/playlist.py index 5d70653..f4ec7c4 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -12,9 +12,7 @@ from app import models from app.db.sqlite.playlists import SQLitePlaylistMethods from app.lib import playlistlib from app.lib.albumslib import sort_by_track_no -from app.lib.home.recents import get_recent_tracks -from app.models.track import Track -from app.store.albums import AlbumStore +from app.serializers.playlist import serialize_for_card from app.store.tracks import TrackStore from app.utils.dates import create_new_date, date_string_to_time_passed from app.utils.remove_duplicates import remove_duplicates @@ -25,47 +23,6 @@ api = Blueprint("playlist", __name__, url_prefix="/") PL = SQLitePlaylistMethods -def duplicate_images(images: list): - if len(images) == 1: - images *= 4 - elif len(images) == 2: - images += list(reversed(images)) - elif len(images) == 3: - images = images + images[:1] - - return images - - -def get_first_4_images( - tracks: list[Track] = [], trackhashes: list[str] = [] -) -> list[dict["str", str]]: - if len(trackhashes) > 0: - tracks = TrackStore.get_tracks_by_trackhashes(trackhashes) - - albums = [] - - for track in tracks: - if track.albumhash not in albums: - albums.append(track.albumhash) - - if len(albums) == 4: - break - - albums = AlbumStore.get_albums_by_hashes(albums) - images = [ - { - "image": album.image, - "color": "".join(album.colors), - } - for album in albums - ] - - if len(images) == 4: - return images - - return duplicate_images(images) - - @api.route("/playlists", methods=["GET"]) def send_all_playlists(): """ @@ -78,7 +35,9 @@ def send_all_playlists(): for playlist in playlists: if not no_images: - playlist.images = get_first_4_images(trackhashes=playlist.trackhashes) + playlist.images = playlistlib.get_first_4_images( + trackhashes=playlist.trackhashes + ) playlist.images = [img["image"] for img in playlist.images] playlist.clear_lists() @@ -204,32 +163,28 @@ def get_playlist(playlistid: str): """ Gets a playlist by id, and if it exists, it gets all the tracks in the playlist and returns them. """ - no_tracks = request.args.get("no_tracks", False) + no_tracks = request.args.get("no_tracks", "false") no_tracks = no_tracks == "true" is_recently_added = playlistid == "recentlyadded" - if not is_recently_added: - playlist = PL.get_playlist_by_id(int(playlistid)) - else: - playlist = models.Playlist( - id="recentlyadded", - name="Recently Added", - image=None, - last_updated="Now", - settings={}, - trackhashes=[], - ) + if is_recently_added: + playlist, tracks = playlistlib.get_recently_added_playlist() + + tracks = remove_duplicates(tracks) + duration = sum(t.duration for t in tracks) + + playlist.set_duration(duration) + playlist = serialize_for_card(playlist) + + return {"info": playlist, "tracks": tracks} + + playlist = PL.get_playlist_by_id(int(playlistid)) if playlist is None: return {"msg": "Playlist not found"}, 404 - if is_recently_added: - tracks = get_recent_tracks(cutoff_days=14) - date = datetime.fromtimestamp(tracks[0].created_date) - playlist.last_updated = create_new_date(date) - else: - tracks = TrackStore.get_tracks_by_trackhashes(list(playlist.trackhashes)) + tracks = TrackStore.get_tracks_by_trackhashes(list(playlist.trackhashes)) tracks = remove_duplicates(tracks) duration = sum(t.duration for t in tracks) @@ -239,7 +194,7 @@ def get_playlist(playlistid: str): playlist.set_count(len(tracks)) if not playlist.has_image: - playlist.images = get_first_4_images(tracks) + playlist.images = playlistlib.get_first_4_images(tracks) playlist.clear_lists() @@ -342,7 +297,7 @@ def remove_playlist_image(playlistid: str): playlist.settings["has_gif"] = False playlist.has_image = False - playlist.images = get_first_4_images(trackhashes=playlist.trackhashes) + playlist.images = playlistlib.get_first_4_images(trackhashes=playlist.trackhashes) playlist.last_updated = date_string_to_time_passed(playlist.last_updated) return {"playlist": playlist}, 200 @@ -463,7 +418,7 @@ def save_item_as_playlist(): PL.add_tracks_to_playlist(playlist.id, trackhashes) playlist.set_count(len(trackhashes)) - images = get_first_4_images(trackhashes=trackhashes) + images = playlistlib.get_first_4_images(trackhashes=trackhashes) playlist.images = [img["image"] for img in images] return {"playlist": playlist}, 201 diff --git a/app/db/sqlite/logger/tracks.py b/app/db/sqlite/logger/tracks.py index fa31695..5462281 100644 --- a/app/db/sqlite/logger/tracks.py +++ b/app/db/sqlite/logger/tracks.py @@ -24,3 +24,17 @@ class SQLiteTrackLogger: lastrowid = cur.lastrowid return lastrowid + + @classmethod + def get_all(cls): + """ + Returns all tracks from the database + """ + + with SQLiteManager(userdata_db=True) as cur: + sql = """SELECT * FROM track_logger ORDER BY timestamp DESC""" + + cur.execute(sql) + rows = cur.fetchall() + + return rows diff --git a/app/lib/home/recents.py b/app/lib/home/recentlyadded.py similarity index 100% rename from app/lib/home/recents.py rename to app/lib/home/recentlyadded.py diff --git a/app/lib/home/recentlyplayed.py b/app/lib/home/recentlyplayed.py new file mode 100644 index 0000000..812607e --- /dev/null +++ b/app/lib/home/recentlyplayed.py @@ -0,0 +1,122 @@ +from dataclasses import asdict + +from tomlkit import item + +from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db +from app.db.sqlite.playlists import SQLitePlaylistMethods as pdb +from app.lib.playlistlib import get_first_4_images, get_recently_added_playlist +from app.models.logger import Track as TrackLog +from app.serializers.album import album_serializer +from app.serializers.artist import serialize_for_card +from app.serializers.playlist import serialize_for_card as serialize_playlist +from app.serializers.track import serialize_track +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore +from app.store.tracks import TrackStore + + +def get_recently_played(limit=7): + entries = db.get_all() + items = [] + added = set() + + for entry in entries: + if len(items) >= limit: + break + + entry = TrackLog(*entry) + + if entry.source in added: + continue + + added.add(entry.source) + + if entry.type == "album": + album = AlbumStore.get_album_by_hash(entry.type_src) + + if album is None: + continue + + album = album_serializer( + album, + { + "genres", + "date", + "count", + "duration", + "albumartists_hashes", + "og_title", + }, + ) + + items.append({"type": "album", "item": album}) + continue + + if entry.type == "artist": + artist = ArtistStore.get_artist_by_hash(entry.type_src) + + if artist is None: + continue + + artist = serialize_for_card(artist) + + items.append({"type": "artist", "item": artist}) + + continue + + if entry.type == "track": + try: + track = TrackStore.get_tracks_by_trackhashes([entry.trackhash])[0] + except IndexError: + continue + + track = serialize_track(track) + + items.append({"type": "track", "item": track}) + + continue + + if entry.type == "folder": + count = len([t for t in TrackStore.tracks if t.folder == entry.type_src]) + items.append( + { + "type": "folder", + "item": { + "path": entry.type_src, + "count": count, + }, + } + ) + continue + + if entry.type == "playlist": + is_recently_added = entry.type_src == "recentlyadded" + + if is_recently_added: + playlist, _ = get_recently_added_playlist() + playlist.images = [i["image"] for i in playlist.images] + items.append( + { + "type": "playlist", + "item": serialize_playlist( + playlist, to_remove={"settings", "duration"} + ), + } + ) + continue + + playlist = pdb.get_playlist_by_id(entry.type_src) + if playlist is None: + continue + + tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes) + playlist.clear_lists() + + if not playlist.has_image: + images = get_first_4_images(tracks) + images = [i["image"] for i in images] + playlist.images = images + + items.append({"type": "playlist", "item": serialize_playlist(playlist)}) + + return items diff --git a/app/lib/playlistlib.py b/app/lib/playlistlib.py index 393adc5..a3d15e2 100644 --- a/app/lib/playlistlib.py +++ b/app/lib/playlistlib.py @@ -4,11 +4,18 @@ This library contains all the functions related to playlists. import os import random import string +from datetime import datetime from typing import Any from PIL import Image, ImageSequence from app import settings +from app.lib.home.recentlyadded import get_recent_tracks +from app.models.playlist import Playlist +from app.models.track import Track +from app.store.albums import AlbumStore +from app.store.tracks import TrackStore +from app.utils.dates import create_new_date def create_thumbnail(image: Any, img_path: str) -> str: @@ -86,33 +93,63 @@ def save_p_image( return filename -# -# class ValidatePlaylistThumbs: -# """ -# Removes all unused images in the images/playlists folder. -# """ -# -# def __init__(self) -> None: -# images = [] -# playlists = Get.get_all_playlists() -# -# log.info("Validating playlist thumbnails") -# for playlist in playlists: -# if playlist.image: -# img_path = playlist.image.split("/")[-1] -# thumb_path = playlist.thumb.split("/")[-1] -# -# images.append(img_path) -# images.append(thumb_path) -# -# p_path = os.path.join(settings.APP_DIR, "images", "playlists") -# -# for image in os.listdir(p_path): -# if image not in images: -# os.remove(os.path.join(p_path, image)) -# -# log.info("Validating playlist thumbnails ... ✅") -# +def duplicate_images(images: list): + if len(images) == 1: + images *= 4 + elif len(images) == 2: + images += list(reversed(images)) + elif len(images) == 3: + images = images + images[:1] + + return images -# TODO: Fix ValidatePlaylistThumbs +def get_first_4_images( + tracks: list[Track] = [], trackhashes: list[str] = [] +) -> list[dict["str", str]]: + if len(trackhashes) > 0: + tracks = TrackStore.get_tracks_by_trackhashes(trackhashes) + + albums = [] + + for track in tracks: + if track.albumhash not in albums: + albums.append(track.albumhash) + + if len(albums) == 4: + break + + albums = AlbumStore.get_albums_by_hashes(albums) + images = [ + { + "image": album.image, + "color": "".join(album.colors), + } + for album in albums + ] + + if len(images) == 4: + return images + + return duplicate_images(images) + + +def get_recently_added_playlist(cutoff: int = 14): + playlist = Playlist( + id="recentlyplayed", + name="Recently Added", + image=None, + last_updated="Now", + settings={}, + trackhashes=[], + ) + + tracks = get_recent_tracks(cutoff) + date = datetime.fromtimestamp(tracks[0].created_date) + playlist.last_updated = create_new_date(date) + + images = get_first_4_images(tracks=tracks) + playlist.images = images + playlist.set_count(len(tracks)) + + return playlist, tracks diff --git a/app/models/logger.py b/app/models/logger.py index 7d3810e..ead2aa3 100644 --- a/app/models/logger.py +++ b/app/models/logger.py @@ -1,11 +1,36 @@ -from attr import dataclass +from dataclasses import dataclass +from typing import Literal -@dataclass +@dataclass class Track: """ Track play logger model """ + + id: int trackhash: str duration: int - timestamp: int \ No newline at end of file + timestamp: int + source: str + userid: int + + type = "track" + type_src = None + + def __post_init__(self): + prefix_map = { + "al:": "album", + "ar:": "artist", + "pl:": "playlist", + "fo:": "folder", + } + + for prefix, srctype in prefix_map.items(): + if self.source.startswith(prefix): + try: + self.type_src = self.source.split(":", 1)[1] + except IndexError: + pass + self.type = srctype + break diff --git a/app/models/playlist.py b/app/models/playlist.py index 29a2451..a916bb3 100644 --- a/app/models/playlist.py +++ b/app/models/playlist.py @@ -11,13 +11,13 @@ class Playlist: """Creates playlist objects""" id: int - image: str + image: str | None last_updated: str name: str settings: str | dict trackhashes: str | list[str] - thumb: str = "" + thumb: str | None = "" count: int = 0 duration: int = 0 has_image: bool = False diff --git a/app/serializers/playlist.py b/app/serializers/playlist.py new file mode 100644 index 0000000..95178ba --- /dev/null +++ b/app/serializers/playlist.py @@ -0,0 +1,13 @@ +from dataclasses import asdict +from app.models.playlist import Playlist + + +def serialize_for_card(playlist: Playlist, to_remove=set()): + p_dict = asdict(playlist) + + props = {"trackhashes"}.union(to_remove) + + for key in props: + p_dict.pop(key, None) + + return p_dict