diff --git a/server/app/api/__init__.py b/server/app/api/__init__.py index 38416d8a..5a128083 100644 --- a/server/app/api/__init__.py +++ b/server/app/api/__init__.py @@ -3,7 +3,6 @@ This module contains all the Flask Blueprints and API routes. It also contains a that are used through-out the app. It handles the initialization of the watchdog, checking and creating config dirs and starting the re-indexing process using a background thread. """ - from app import functions from app import helpers from app import prep diff --git a/server/app/api/album.py b/server/app/api/album.py index 64ed1da3..2f22f702 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -6,12 +6,12 @@ from typing import List from app import api from app import helpers +from app import instances from app import models +from app.functions import FetchAlbumBio from app.lib import albumslib from flask import Blueprint from flask import request -from app.functions import FetchAlbumBio -from app import instances album_bp = Blueprint("album", __name__, url_prefix="") @@ -41,27 +41,31 @@ def get_album(): """Returns all the tracks in the given album.""" data = request.get_json() albumhash = data["hash"] + error_msg = {"error": "Album not created yet."} tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) + + if len(tracks) == 0: + return error_msg, 204 + tracks = [models.Track(t) for t in tracks] tracks = helpers.RemoveDuplicates(tracks)() album = instances.album_instance.find_album_by_hash(albumhash) if not album: - return {"error": "Album not created yet."}, 204 + return error_msg, 204 album = models.Album(album) album.count = len(tracks) - album.duration = albumslib.get_album_duration(tracks) + try: + album.duration = sum([t.length for t in tracks]) + except AttributeError: + album.duration = 0 - if ( - album.count == 1 - and tracks[0].title == album.title - and tracks[0].tracknumber == 1 - and tracks[0].disknumber == 1 - ): + if (album.count == 1 and tracks[0].title == album.title + and tracks[0].tracknumber == 1 and tracks[0].disknumber == 1): album.is_single = True return {"tracks": tracks, "info": album} @@ -72,12 +76,17 @@ def get_album_bio(): """Returns the album bio for the given album.""" data = request.get_json() album_hash = data["hash"] + err_msg = {"bio": "No bio found"} album = instances.album_instance.find_album_by_hash(album_hash) + + if album is None: + return err_msg, 404 + bio = FetchAlbumBio(album["title"], album["artist"])() if bio is None: - return {"bio": "No bio found."}, 404 + return err_msg, 404 return {"bio": bio} diff --git a/server/app/api/artist.py b/server/app/api/artist.py index e82d02c3..08f60aba 100644 --- a/server/app/api/artist.py +++ b/server/app/api/artist.py @@ -10,7 +10,6 @@ from flask import Blueprint artist_bp = Blueprint("artist", __name__, url_prefix="/") - # @artist_bp.route("/artist/") # @cache.cached() # def get_artist_data(artist: str): diff --git a/server/app/api/playlist.py b/server/app/api/playlist.py index b956e808..c26eb0df 100644 --- a/server/app/api/playlist.py +++ b/server/app/api/playlist.py @@ -8,12 +8,13 @@ from app import exceptions from app import instances from app import models from app import serializer +from app.helpers import create_new_date +from app.helpers import Get +from app.helpers import UseBisection from app.lib import playlistlib from flask import Blueprint from flask import request -from app.helpers import Get, UseBisection, create_new_date - playlist_bp = Blueprint("playlist", __name__, url_prefix="/") PlaylistExists = exceptions.PlaylistExists @@ -27,7 +28,8 @@ def get_all_playlists(): dbplaylists = [models.Playlist(p) for p in dbplaylists] playlists = [ - serializer.Playlist(p, construct_last_updated=False) for p in dbplaylists + serializer.Playlist(p, construct_last_updated=False) + for p in dbplaylists ] playlists.sort( key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"), @@ -84,7 +86,12 @@ def get_playlist(playlistid: str): playlist = models.Playlist(p) tracks = playlistlib.create_playlist_tracks(playlist.pretracks) - return {"info": serializer.Playlist(playlist), "tracks": tracks} + + duration = sum([t.length for t in tracks]) + playlist = serializer.Playlist(playlist) + playlist.duration = duration + + return {"info": playlist, "tracks": tracks} @playlist_bp.route("/playlist//update", methods=["PUT"]) diff --git a/server/app/api/search.py b/server/app/api/search.py index b2bd82b4..b745a166 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -3,14 +3,14 @@ Contains all the search routes. """ from pprint import pprint from typing import List + from app import helpers +from app import models +from app import serializer from app.lib import searchlib from flask import Blueprint from flask import request -from app import models -from app import serializer - search_bp = Blueprint("search", __name__, url_prefix="/") SEARCH_RESULTS = { @@ -197,20 +197,20 @@ def search_load_more(): if type == "tracks": t = SearchResults.tracks return { - "tracks": t[index : index + 5], + "tracks": t[index:index + 5], "more": len(t) > index + 5, } elif type == "albums": a = SearchResults.albums return { - "albums": a[index : index + 6], + "albums": a[index:index + 6], "more": len(a) > index + 6, } elif type == "artists": a = SearchResults.artists return { - "artists": a[index : index + 6], + "artists": a[index:index + 6], "more": len(a) > index + 6, } diff --git a/server/app/api/track.py b/server/app/api/track.py index 9a3eb587..924f6aa1 100644 --- a/server/app/api/track.py +++ b/server/app/api/track.py @@ -3,11 +3,10 @@ Contains all the track routes. """ from app import api from app import instances +from app import models from flask import Blueprint from flask import send_file -from app import models - track_bp = Blueprint("track", __name__, url_prefix="/") @@ -17,14 +16,18 @@ def send_track_file(trackid): Returns an audio file that matches the passed id to the client. """ track = instances.tracks_instance.get_track_by_id(trackid) + msg = {"msg": "File Not Found"} if track is None: - return "File not found", 404 + return msg, 404 track = models.Track(track) type = track.filepath.split(".")[-1] - return send_file(track.filepath, mimetype=f"audio/{type}") + try: + return send_file(track.filepath, mimetype=f"audio/{type}") + except FileNotFoundError: + return msg, 404 @track_bp.route("/sample") @@ -33,6 +36,5 @@ def get_sample_track(): Returns a sample track object. """ - return instances.tracks_instance.get_song_by_album( - "Legends Never Die", "Juice WRLD" - ) + return instances.tracks_instance.get_song_by_album("Legends Never Die", + "Juice WRLD") diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 030c900c..7e220760 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -2,6 +2,8 @@ This file contains the Album class for interacting with album documents in MongoDB. """ +from typing import List + from app.db.mongodb import convert_many from app.db.mongodb import convert_one from app.db.mongodb import MongoAlbums @@ -20,8 +22,13 @@ class Albums(MongoAlbums): """ album = album.__dict__ return self.collection.update_one( - {"album": album["title"], "artist": album["artist"]}, - {"$set": album}, + { + "album": album["title"], + "artist": album["artist"] + }, + { + "$set": album + }, upsert=True, ).upserted_id @@ -59,3 +66,14 @@ class Albums(MongoAlbums): """ album = self.collection.find_one({"hash": hash}) return convert_one(album) + + def set_album_colors(self, colors: List[str], hash: str) -> None: + """ + Sets the colors for an album. + """ + self.collection.update_one( + {"hash": hash}, + {"$set": { + "colors": colors + }}, + ) diff --git a/server/app/db/mongodb/playlists.py b/server/app/db/mongodb/playlists.py index 9b730c48..78941f2c 100644 --- a/server/app/db/mongodb/playlists.py +++ b/server/app/db/mongodb/playlists.py @@ -1,10 +1,10 @@ """ This file contains the Playlists class for interacting with the playlist documents in MongoDB. """ +from app import helpers from app.db.mongodb import convert_many from app.db.mongodb import convert_one from app.db.mongodb import MongoPlaylists -from app import helpers from bson import ObjectId @@ -18,8 +18,12 @@ class Playlists(MongoPlaylists): Inserts a new playlist object into the database. """ return self.collection.update_one( - {"name": playlist["name"]}, - {"$set": playlist}, + { + "name": playlist["name"] + }, + { + "$set": playlist + }, upsert=True, ).upserted_id @@ -45,7 +49,9 @@ class Playlists(MongoPlaylists): return self.collection.update_one( {"_id": ObjectId(playlistid)}, - {"$set": {"lastUpdated": date}}, + {"$set": { + "lastUpdated": date + }}, ) def add_track_to_playlist(self, playlistid: str, track: dict) -> None: @@ -56,7 +62,9 @@ class Playlists(MongoPlaylists): { "_id": ObjectId(playlistid), }, - {"$push": {"pre_tracks": track}}, + {"$push": { + "pre_tracks": track + }}, ) self.set_last_updated(playlistid) diff --git a/server/app/db/mongodb/tracks.py b/server/app/db/mongodb/tracks.py index a7c3b1be..a3596d0e 100644 --- a/server/app/db/mongodb/tracks.py +++ b/server/app/db/mongodb/tracks.py @@ -2,7 +2,9 @@ This file contains the AllSongs class for interacting with track documents in MongoDB. """ import pymongo -from app.db.mongodb import MongoTracks, convert_many, convert_one +from app.db.mongodb import convert_many +from app.db.mongodb import convert_one +from app.db.mongodb import MongoTracks from bson import ObjectId @@ -18,9 +20,12 @@ class Tracks(MongoTracks): """ Inserts a new track object into the database. """ - return self.collection.update_one( - {"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True - ).upserted_id + return self.collection.update_one({ + "filepath": song_obj["filepath"] + }, { + "$set": song_obj + }, + upsert=True).upserted_id def insert_many(self, songs: list): """ @@ -52,21 +57,33 @@ class Tracks(MongoTracks): """ Returns all the songs matching the albums in the query params (using regex). """ - songs = self.collection.find({"album": {"$regex": query, "$options": "i"}}) + songs = self.collection.find( + {"album": { + "$regex": query, + "$options": "i" + }}) return convert_many(songs) def search_songs_by_artist(self, query: str) -> list: """ Returns all the songs matching the artists in the query params. """ - songs = self.collection.find({"artists": {"$regex": query, "$options": "i"}}) + songs = self.collection.find( + {"artists": { + "$regex": query, + "$options": "i" + }}) return convert_many(songs) def find_song_by_title(self, query: str) -> list: """ Finds all the tracks matching the title in the query params. """ - song = self.collection.find({"title": {"$regex": query, "$options": "i"}}) + song = self.collection.find( + {"title": { + "$regex": query, + "$options": "i" + }}) return convert_many(song) def find_songs_by_album(self, name: str, artist: str) -> list: @@ -80,7 +97,9 @@ class Tracks(MongoTracks): """ Returns a sorted list of all the tracks exactly matching the folder in the query params """ - songs = self.collection.find({"folder": query}).sort("title", pymongo.ASCENDING) + songs = self.collection.find({ + "folder": query + }).sort("title", pymongo.ASCENDING) return convert_many(songs) def find_songs_by_filenames(self, filenames: list) -> list: @@ -102,8 +121,10 @@ class Tracks(MongoTracks): Returns a list of all the tracks matching the path in the query params. """ return self.collection.count_documents( - {"filepath": {"$regex": f"^{path}", "$options": "i"}} - ) + {"filepath": { + "$regex": f"^{path}", + "$options": "i" + }}) def find_songs_by_artist(self, query: str) -> list: """ @@ -117,8 +138,10 @@ class Tracks(MongoTracks): Returns a list of all the tracks containing the albumartist in the query params. """ songs = self.collection.find( - {"albumartist": {"$regex": query, "$options": "i"}} - ) + {"albumartist": { + "$regex": query, + "$options": "i" + }}) return convert_many(songs) def get_song_by_path(self, path: str) -> dict: @@ -155,13 +178,14 @@ class Tracks(MongoTracks): songs = self.collection.find({"albumhash": hash}) return convert_many(songs) - def find_track_by_title_artists_album( - self, title: str, artist: str, album: str - ) -> dict: + def find_track_by_title_artists_album(self, title: str, artist: str, + album: str) -> dict: """ Returns a single track matching the title, artist, and album in the query params. """ - song = self.collection.find_one( - {"title": title, "artists": artist, "album": album} - ) + song = self.collection.find_one({ + "title": title, + "artists": artist, + "album": album + }) return convert_one(song) diff --git a/server/app/functions.py b/server/app/functions.py index e62128a9..b9293de3 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -7,14 +7,19 @@ from concurrent.futures import ThreadPoolExecutor from io import BytesIO import requests -from PIL import Image - -from app import helpers, settings +from app import helpers +from app import settings +from app.lib import trackslib from app.lib import watchdoge from app.lib.albumslib import ValidateAlbumThumbs -from app.lib import trackslib -from app.lib.populate import CreateAlbums, Populate +from app.lib.colorlib import ProcessAlbumColors from app.lib.playlistlib import ValidatePlaylistThumbs +from app.lib.populate import CreateAlbums +from app.lib.populate import Populate +from app.logger import get_logger +from PIL import Image + +log = get_logger() @helpers.background @@ -22,18 +27,25 @@ def run_checks(): """ Checks for new songs every 5 minutes. """ - - # while True: - trackslib.validate_tracks() - - Populate() - CreateAlbums() - - if helpers.Ping()(): - CheckArtistImages()() - ValidateAlbumThumbs() - ValidatePlaylistThumbs() + + while True: + trackslib.validate_tracks() + + Populate() + CreateAlbums() + + if helpers.Ping()(): + CheckArtistImages()() + + @helpers.background + def process_album_colors(): + ProcessAlbumColors() + + ValidatePlaylistThumbs() + process_album_colors() + + time.sleep(300) @helpers.background @@ -67,6 +79,7 @@ class getArtistImage: class useImageDownloader: + def __init__(self, url: str, dest: str) -> None: self.url = url self.dest = dest @@ -76,14 +89,18 @@ class useImageDownloader: img = Image.open(BytesIO(requests.get(self.url).content)) img.save(self.dest, format="webp") img.close() + return "fetched image" except requests.exceptions.ConnectionError: time.sleep(5) + return "connection error" class CheckArtistImages: + def __init__(self): self.artists: list[str] = [] print("Checking for artist images") + log.info("Checking artist images") @staticmethod def check_if_exists(img_path: str): @@ -104,29 +121,25 @@ class CheckArtistImages: :param artistname: The artist name """ - img_path = ( - settings.APP_DIR - + "/images/artists/" - + helpers.create_safe_name(artistname) - + ".webp" - ) + img_path = (settings.APP_DIR + "/images/artists/" + + helpers.create_safe_name(artistname) + ".webp") if cls.check_if_exists(img_path): - return + return "exists" url = getArtistImage(artistname)() if url is None: - return - useImageDownloader(url, img_path)() + return "url is none" + + return useImageDownloader(url, img_path)() def __call__(self): self.artists = helpers.Get.get_all_artists() with ThreadPoolExecutor() as pool: iter = pool.map(self.download_image, self.artists) - for i in iter: - pass + [i for i in iter] print("Done fetching images") @@ -136,8 +149,7 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None: Returns the album bio for a given album. """ last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format( - settings.LAST_FM_API_KEY, albumartist, title - ) + settings.LAST_FM_API_KEY, albumartist, title) try: response = requests.get(last_fm_url) @@ -146,7 +158,8 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None: return None try: - bio = data["album"]["wiki"]["summary"].split(' Dict[List[str], List[str]]: return subfolders, files - - class RemoveDuplicates: + def __init__(self, tracklist: List[models.Track]) -> None: self.tracklist = tracklist @@ -77,15 +73,12 @@ def is_valid_file(filename: str) -> bool: return False -ill_chars = '/\\:*?"<>|#&' - - def create_album_hash(title: str, artist: str) -> str: """ Creates a simple hash for an album """ lower = (title + artist).replace(" ", "").lower() - hash = "".join([i for i in lower if i not in ill_chars]) + hash = "".join([i for i in lower if i.isalnum()]) return hash @@ -99,7 +92,7 @@ def create_safe_name(name: str) -> str: """ Creates a url-safe name from a name. """ - return "".join([i for i in name if i not in ill_chars]) + return "".join([i for i in name if i.isalnum()]) class UseBisection: @@ -110,7 +103,8 @@ class UseBisection: items. """ - def __init__(self, source: List, search_from: str, queries: List[str]) -> None: + def __init__(self, source: List, search_from: str, + queries: List[str]) -> None: self.source_list = source self.queries_list = queries self.attr = search_from @@ -140,6 +134,7 @@ class UseBisection: class Get: + @staticmethod def get_all_tracks() -> List[models.Track]: """ diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index f8c8b925..eea60e4f 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -6,7 +6,9 @@ import random from dataclasses import dataclass from typing import List -from app import helpers, instances, models +from app import helpers +from app import instances +from app import models from app.lib import taglib from app.logger import logg from app.settings import THUMBS_PATH @@ -35,6 +37,7 @@ class RipAlbumImage: class ValidateAlbumThumbs: + @staticmethod def remove_obsolete(): """ @@ -58,7 +61,9 @@ class ValidateAlbumThumbs: Re-rip lost album thumbnails """ entries = os.scandir(THUMBS_PATH) - entries = [Thumbnail(entry.name) for entry in entries if entry.is_file()] + entries = [ + Thumbnail(entry.name) for entry in entries if entry.is_file() + ] albums = helpers.Get.get_all_albums() thumbs = [(album.hash + ".webp") for album in albums] @@ -85,19 +90,6 @@ class ValidateAlbumThumbs: self.find_lost_thumbnails() -def get_album_duration(album: List[models.Track]) -> int: - """ - Gets the duration of an album. - """ - - album_duration = 0 - - for track in album: - album_duration += track.length - - return album_duration - - def use_defaults() -> str: """ Returns a path to a random image in the defaults directory. diff --git a/server/app/lib/colorlib.py b/server/app/lib/colorlib.py index 5882461f..5936759b 100644 --- a/server/app/lib/colorlib.py +++ b/server/app/lib/colorlib.py @@ -1,17 +1,17 @@ -from io import BytesIO - import colorgram -from app import api from app import instances -from app.lib.taglib import return_album_art -from PIL import Image -from progress.bar import Bar +from app import settings +from app.helpers import Get +from app.logger import get_logger +from app.models import Album + +log = get_logger() -def get_image_colors(image) -> list: +def get_image_colors(image: str) -> list: """Extracts 2 of the most dominant colors from an image.""" try: - colors = sorted(colorgram.extract(image, 2), key=lambda c: c.hsl.h) + colors = sorted(colorgram.extract(image, 4), key=lambda c: c.hsl.h) except OSError: return [] @@ -24,30 +24,26 @@ def get_image_colors(image) -> list: return formatted_colors -def save_track_colors(img, filepath) -> None: - """Saves the track colors to the database""" +class ProcessAlbumColors: - track_colors = get_image_colors(img) + def __init__(self) -> None: + log.info("Processing album colors") + all_albums = Get.get_all_albums() - tc_dict = { - "filepath": filepath, - "colors": track_colors, - } + all_albums = [a for a in all_albums if len(a.colors) == 0] - instances.track_color_instance.insert_track_color(tc_dict) + for a in all_albums: + self.process_color(a) + log.info("Processing album colors ... ✅") -def save_t_colors(): - _bar = Bar("Processing image colors", max=len(api.DB_TRACKS)) + @staticmethod + def process_color(album: Album): + img = settings.THUMBS_PATH + "/" + album.image - for track in api.DB_TRACKS: - filepath = track["filepath"] - album_art = return_album_art(filepath) + colors = get_image_colors(img) - if album_art is not None: - img = Image.open(BytesIO(album_art)) - save_track_colors(img, filepath) + if len(colors) > 0: + instances.album_instance.set_album_colors(colors, album.hash) - _bar.next() - - _bar.finish() + return colors diff --git a/server/app/lib/folderslib.py b/server/app/lib/folderslib.py index 1632b66f..0cf2d38a 100644 --- a/server/app/lib/folderslib.py +++ b/server/app/lib/folderslib.py @@ -1,13 +1,12 @@ +import time +from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from os import scandir -import time from typing import Tuple -from concurrent.futures import ThreadPoolExecutor - -from app.models import Folder -from app.models import Track from app import instances +from app.models import Folder +from app.models import Track @dataclass @@ -27,10 +26,14 @@ def get_folder_track_count(path: str) -> int: def create_folder(dir: Dir) -> Folder: """Create a single Folder object""" folder = { - "name": dir.path.split("/")[-1], - "path": dir.path, - "is_sym": dir.is_sym, - "trackcount": instances.tracks_instance.find_tracks_inside_path_regex(dir.path), + "name": + dir.path.split("/")[-1], + "path": + dir.path, + "is_sym": + dir.is_sym, + "trackcount": + instances.tracks_instance.find_tracks_inside_path_regex(dir.path), } return Folder(folder) diff --git a/server/app/lib/playlistlib.py b/server/app/lib/playlistlib.py index a1d9f4d8..95fda111 100644 --- a/server/app/lib/playlistlib.py +++ b/server/app/lib/playlistlib.py @@ -11,14 +11,13 @@ from app import exceptions from app import instances from app import models from app import settings +from app.helpers import Get +from app.lib import trackslib +from app.logger import get_logger from PIL import Image from PIL import ImageSequence from werkzeug import datastructures -from app.lib import trackslib -from app.helpers import Get -from app.logger import get_logger - TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist logg = get_logger() @@ -53,7 +52,8 @@ def create_thumbnail(image: any, img_path: str) -> str: Creates a 250 x 250 thumbnail from a playlist image """ thumb_path = "thumb_" + img_path - full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", thumb_path) + full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", + thumb_path) aspect_ratio = image.width / image.height @@ -71,11 +71,13 @@ def save_p_image(file: datastructures.FileStorage, pid: str): """ img = Image.open(file) - random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5)) + random_str = "".join( + random.choices(string.ascii_letters + string.digits, k=5)) img_path = pid + str(random_str) + ".webp" - full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path) + full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", + img_path) if file.content_type == "image/gif": frames = [] diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 417e5e7a..c6ee9400 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -1,20 +1,22 @@ -from dataclasses import dataclass import time from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass from typing import List +from app import instances from app import settings -from app.logger import logg -from app.helpers import Get, UseBisection, create_album_hash +from app.helpers import create_album_hash +from app.helpers import Get from app.helpers import run_fast_scandir +from app.helpers import UseBisection from app.instances import tracks_instance from app.lib.albumslib import create_album from app.lib.taglib import get_tags -from app.models import Album, Track +from app.logger import logg +from app.models import Album +from app.models import Track from tqdm import tqdm -from app import instances - class Populate: """ @@ -76,6 +78,7 @@ class PreAlbum: class CreateAlbums: + def __init__(self) -> None: self.db_tracks = Get.get_all_tracks() self.db_albums = Get.get_all_albums() @@ -119,7 +122,8 @@ class CreateAlbums: return prealbums @staticmethod - def filter_processed(albums: List[Album], prealbums: List[PreAlbum]) -> List[dict]: + def filter_processed(albums: List[Album], + prealbums: List[PreAlbum]) -> List[dict]: to_process = [] for p in tqdm(prealbums, desc="Filtering processed albums"): @@ -144,7 +148,7 @@ class CreateAlbums: album = create_album(track) self.db_tracks.remove(track) else: - album["image"] = hash + album["image"] = hash + ".webp" try: album = Album(album) return album diff --git a/server/app/lib/searchlib.py b/server/app/lib/searchlib.py index e3cb77dc..adf77b1e 100644 --- a/server/app/lib/searchlib.py +++ b/server/app/lib/searchlib.py @@ -37,6 +37,7 @@ class Limit: class SearchTracks: + def __init__(self, tracks: List[models.Track], query: str) -> None: self.query = query self.tracks = tracks @@ -59,6 +60,7 @@ class SearchTracks: class SearchArtists: + def __init__(self, artists: set[str], query: str) -> None: self.query = query self.artists = artists @@ -88,6 +90,7 @@ class SearchArtists: class SearchAlbums: + def __init__(self, albums: List[models.Album], query: str) -> None: self.query = query self.albums = albums @@ -118,6 +121,7 @@ class SearchAlbums: class SearchPlaylists: + def __init__(self, playlists: List[models.Playlist], query: str) -> None: self.playlists = playlists self.query = query diff --git a/server/app/lib/taglib.py b/server/app/lib/taglib.py index 1292feba..4770035d 100644 --- a/server/app/lib/taglib.py +++ b/server/app/lib/taglib.py @@ -33,6 +33,7 @@ def extract_thumb(filepath: str, webp_path: str) -> bool: Extracts the thumbnail from an audio file. Returns the path to the thumbnail. """ img_path = os.path.join(settings.THUMBS_PATH, webp_path) + tsize = settings.THUMB_SIZE if os.path.exists(img_path): return True @@ -43,12 +44,12 @@ def extract_thumb(filepath: str, webp_path: str) -> bool: img = Image.open(BytesIO(album_art)) try: - small_img = img.resize((250, 250), Image.ANTIALIAS) + small_img = img.resize((tsize, tsize), Image.ANTIALIAS) small_img.save(img_path, format="webp") except OSError: try: png = img.convert("RGB") - small_img = png.resize((250, 250), Image.ANTIALIAS) + small_img = png.resize((tsize, tsize), Image.ANTIALIAS) small_img.save(webp_path, format="webp") except: return False diff --git a/server/app/lib/trackslib.py b/server/app/lib/trackslib.py index f48847f2..61e39136 100644 --- a/server/app/lib/trackslib.py +++ b/server/app/lib/trackslib.py @@ -22,5 +22,4 @@ def validate_tracks() -> None: def get_p_track(ptrack): return instances.tracks_instance.find_track_by_title_artists_album( - ptrack["title"], ptrack["artists"], ptrack["album"] - ) + ptrack["title"], ptrack["artists"], ptrack["album"]) diff --git a/server/app/lib/watchdoge.py b/server/app/lib/watchdoge.py index 2838077b..50731a6d 100644 --- a/server/app/lib/watchdoge.py +++ b/server/app/lib/watchdoge.py @@ -4,15 +4,15 @@ This library contains the classes and functions related to the watchdog file wat import os import time -from app.logger import logg from app import instances -from app import models -from app.helpers import Get, create_album_hash -from app.lib.albumslib import create_album +from app.helpers import create_album_hash from app.lib.taglib import get_tags +from app.logger import get_logger from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer +log = get_logger() + class OnMyWatch: """ @@ -31,7 +31,7 @@ class OnMyWatch: try: self.observer.start() except OSError: - logg.error("Could not start watchdog.") + log.error("Could not start watchdog.") return try: @@ -55,18 +55,6 @@ def add_track(filepath: str) -> None: if tags is not None: hash = create_album_hash(tags["album"], tags["albumartist"]) tags["albumhash"] = hash - - album = instances.album_instance.find_album_by_hash(hash) - all_tracks = Get.get_all_tracks() - all_tracks.append(models.Track(tags)) - - if album is None: - album_data = create_album(tags, all_tracks) - album = models.Album(album_data) - - instances.album_instance.insert_album(album) - - tags["image"] = album.image instances.tracks_instance.insert_song(tags) @@ -75,8 +63,6 @@ def remove_track(filepath: str) -> None: Removes a track from the music dict. """ - filepath = filepath + "k" - instances.tracks_instance.remove_song_by_filepath(filepath) @@ -140,7 +126,3 @@ class Handler(PatternMatchingEventHandler): watch = OnMyWatch() - -# TODO -# When removing a track, check if there are other tracks in the same album, -# if it was the last one, remove the album. diff --git a/server/app/logger.py b/server/app/logger.py index 15b91233..cd030110 100644 --- a/server/app/logger.py +++ b/server/app/logger.py @@ -1,7 +1,5 @@ import logging -from app.settings import logger - class CustomFormatter(logging.Formatter): diff --git a/server/app/models.py b/server/app/models.py index 915e225c..974e4b36 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -1,8 +1,9 @@ """ Contains all the models for objects generation and typing. """ -from dataclasses import dataclass, field import random +from dataclasses import dataclass +from dataclasses import field from typing import List from app import helpers @@ -48,16 +49,13 @@ class Track: self.image = tags["albumhash"] + ".webp" self.tracknumber = int(tags["tracknumber"]) - self.uniq_hash = self.create_unique_hash( - "".join(self.artists), self.album, self.title - ) + self.uniq_hash = self.create_unique_hash("".join(self.artists), + self.album, self.title) @staticmethod def create_unique_hash(*args): - ill_chars = '/\\:*?"<>|#&' - string = "".join(str(a) for a in args).replace(" ", "") - return "".join(string).strip(ill_chars).lower() + return "".join([i for i in string if i.isalnum()]).lower() @dataclass(slots=True) @@ -92,6 +90,7 @@ class Album: is_soundtrack: bool = False is_compilation: bool = False is_single: bool = False + colors: List[str] = field(default_factory=list) def __init__(self, tags): self.title = tags["title"] @@ -100,6 +99,11 @@ class Album: self.image = tags["image"] self.hash = tags["hash"] + try: + self.colors = tags["colors"] + except KeyError: + self.colors = [] + @property def is_soundtrack(self) -> bool: keywords = ["motion picture", "soundtrack"] diff --git a/server/app/prep.py b/server/app/prep.py index bb80b71b..2ddde835 100644 --- a/server/app/prep.py +++ b/server/app/prep.py @@ -11,13 +11,11 @@ class CopyFiles: """Copies assets to the app directory.""" def __init__(self) -> None: - files = [ - { - "src": "assets", - "dest": os.path.join(settings.APP_DIR, "assets"), - "is_dir": True, - } - ] + files = [{ + "src": "assets", + "dest": os.path.join(settings.APP_DIR, "assets"), + "is_dir": True, + }] for entry in files: src = os.path.join(os.getcwd(), entry["src"]) @@ -26,9 +24,7 @@ class CopyFiles: shutil.copytree( src, entry["dest"], - ignore=shutil.ignore_patterns( - "*.pyc", - ), + ignore=shutil.ignore_patterns("*.pyc", ), copy_function=shutil.copy2, dirs_exist_ok=True, ) diff --git a/server/app/serializer.py b/server/app/serializer.py index cfae04d5..cde4e22e 100644 --- a/server/app/serializer.py +++ b/server/app/serializer.py @@ -59,6 +59,7 @@ class Playlist: lastUpdated: int description: str count: int = 0 + duration: int = 0 def __init__(self, p: models.Playlist, @@ -72,7 +73,8 @@ class Playlist: self.count = p.count if construct_last_updated: - self.lastUpdated = self.l_updated(p.lastUpdated) + self.lastUpdated = self.get_l_updated(p.lastUpdated) - def l_updated(self, date: str) -> str: + @staticmethod + def get_l_updated(date: str) -> str: return date_string_to_time_passed(date) diff --git a/server/app/settings.py b/server/app/settings.py index ccb2bace..740f2c9a 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -1,9 +1,8 @@ """ Contains default configs """ -import os import multiprocessing - +import os # paths CONFIG_FOLDER = ".alice" @@ -28,6 +27,9 @@ LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" CPU_COUNT = multiprocessing.cpu_count() +THUMB_SIZE: int = 400 +""" +The size of extracted in pixels +""" -class logger: - enable = True +LOGGER_ENABLE: bool = True diff --git a/server/assets/default.webp b/server/assets/default.webp index d9795fa9..8993fcf8 100644 Binary files a/server/assets/default.webp and b/server/assets/default.webp differ diff --git a/src/assets/css/_variables.scss b/src/assets/css/_variables.scss index 52fe2039..caad1304 100644 --- a/src/assets/css/_variables.scss +++ b/src/assets/css/_variables.scss @@ -39,7 +39,7 @@ $teal: rgb(64, 200, 224); $primary: $gray4; -$accent: $darkblue; +$accent: $red; $secondary: $gray5; $cta: $blue; $danger: $red; diff --git a/src/components/AlbumView/AlbumBio.vue b/src/components/AlbumView/AlbumBio.vue index e4f6af77..e4063e29 100644 --- a/src/components/AlbumView/AlbumBio.vue +++ b/src/components/AlbumView/AlbumBio.vue @@ -8,10 +8,10 @@ - \ No newline at end of file + diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 22008810..8b3d0d93 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -1,16 +1,22 @@