From 030ab8a3799c40bfad0a5805fe75d23794eec72e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 13 Jun 2022 14:45:18 +0300 Subject: [PATCH 01/56] refactor most things to use the database directly --- server/app/api/__init__.py | 18 ------- server/app/api/album.py | 42 ++++++++-------- server/app/api/playlist.py | 65 ++++++++++++------------ server/app/api/search.py | 21 +++----- server/app/api/track.py | 20 +++----- server/app/db/__init__.py | 12 +++++ server/app/db/mongodb/albums.py | 21 ++++---- server/app/db/mongodb/playlists.py | 31 ++++++------ server/app/db/mongodb/tracks.py | 43 +++++++++++++--- server/app/functions.py | 18 +++---- server/app/lib/albumslib.py | 21 ++------ server/app/lib/folderslib.py | 78 +++-------------------------- server/app/lib/playlistlib.py | 79 ++++++++++++++---------------- server/app/lib/populate.py | 59 ++++++---------------- server/app/lib/trackslib.py | 23 +++++---- server/app/models.py | 43 +++------------- server/app/settings.py | 4 +- src/composables/album.ts | 4 +- 18 files changed, 237 insertions(+), 365 deletions(-) diff --git a/server/app/api/__init__.py b/server/app/api/__init__.py index b9b916d2..b8cfb9ee 100644 --- a/server/app/api/__init__.py +++ b/server/app/api/__init__.py @@ -3,25 +3,10 @@ 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 typing import List -from typing import Set from app import functions from app import helpers -from app import instances -from app import models from app import prep -from app.lib import albumslib -from app.lib import folderslib -from app.lib import playlistlib - -DB_TRACKS = instances.tracks_instance.get_all_tracks() -VALID_FOLDERS: Set[str] = set() - -ALBUMS: List[models.Album] = [] -TRACKS: List[models.Track] = [] -PLAYLISTS: List[models.Playlist] = [] -FOLDERS: List[models.Folder] = List @helpers.background @@ -31,9 +16,6 @@ def initialize() -> None: """ functions.start_watchdog() prep.create_config_dir() - albumslib.create_everything() - folderslib.run_scandir() - playlistlib.create_all_playlists() functions.reindex_tracks() diff --git a/server/app/api/album.py b/server/app/api/album.py index fbdf73d4..2624966d 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -1,16 +1,17 @@ """ Contains all the album routes. """ +from pprint import pprint from typing import List from app import api from app import helpers from app import models from app.lib import albumslib -from app.lib import trackslib from flask import Blueprint from flask import request from app.functions import FetchAlbumBio +from app import instances album_bp = Blueprint("album", __name__, url_prefix="") @@ -35,31 +36,31 @@ def get_albums(): return {"albums": albums} -@album_bp.route("/album/tracks", methods=["POST"]) +@album_bp.route("/album", methods=["POST"]) def get_album(): """Returns all the tracks in the given album.""" data = request.get_json() - - album = data["album"] - artist = data["artist"] - - songs = trackslib.get_album_tracks(album, artist) + album, artist = data["album"], data["artist"] albumhash = helpers.create_album_hash(album, artist) - index = albumslib.find_album(api.ALBUMS, albumhash) - album: models.Album = api.ALBUMS[index] - album.count = len(songs) - album.duration = albumslib.get_album_duration(songs) + tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) + tracks = [models.Track(t) for t in tracks] + + album = instances.album_instance.find_album_by_hash(albumhash) + album = models.Album(album) + + album.count = len(tracks) + album.duration = albumslib.get_album_duration(tracks) if ( album.count == 1 - and songs[0].title == album.title - and songs[0].tracknumber == 1 - and songs[0].disknumber == 1 + and tracks[0].title == album.title + and tracks[0].tracknumber == 1 + and tracks[0].disknumber == 1 ): album.is_single = True - return {"songs": songs, "info": album} + return {"tracks": tracks, "info": album} @album_bp.route("/album/bio", methods=["POST"]) @@ -80,14 +81,11 @@ def get_albumartists(): """Returns a list of artists featured in a given album.""" data = request.get_json() - album = data["album"] - artist = data["artist"] + album, artist = data["album"], data["artist"] + albumhash = helpers.create_album_hash(album, artist) - tracks: List[models.Track] = [] - - for track in api.TRACKS: - if track.album == album and track.albumartist == artist: - tracks.append(track) + tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) + tracks = [models.Track(t) for t in tracks] artists = [] diff --git a/server/app/api/playlist.py b/server/app/api/playlist.py index d8730a02..5d023c4f 100644 --- a/server/app/api/playlist.py +++ b/server/app/api/playlist.py @@ -22,8 +22,12 @@ TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist @playlist_bp.route("/playlists", methods=["GET"]) def get_all_playlists(): + """Returns all the playlists.""" + dbplaylists = instances.playlist_instance.get_all_playlists() + dbplaylists = [models.Playlist(p) for p in dbplaylists] + playlists = [ - serializer.Playlist(p, construct_last_updated=False) for p in api.PLAYLISTS + 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"), @@ -36,7 +40,7 @@ def get_all_playlists(): def create_playlist(): data = request.get_json() - playlist = { + data = { "name": data["name"], "description": "", "pre_tracks": [], @@ -45,21 +49,16 @@ def create_playlist(): "thumb": "", } - try: - for pl in api.PLAYLISTS: - if pl.name == playlist["name"]: - raise PlaylistExists("Playlist already exists.") + dbp = instances.playlist_instance.get_playlist_by_name(data["name"]) - except PlaylistExists as e: - return {"error": str(e)}, 409 + if dbp is not None: + return {"message": "Playlist already exists."}, 409 - upsert_id = instances.playlist_instance.insert_playlist(playlist) + upsert_id = instances.playlist_instance.insert_playlist(data) p = instances.playlist_instance.get_playlist_by_id(upsert_id) - pp = models.Playlist(p) + playlist = models.Playlist(p) - api.PLAYLISTS.append(pp) - - return {"playlist": pp}, 201 + return {"playlist": playlist}, 201 @playlist_bp.route("/playlist//add", methods=["POST"]) @@ -70,22 +69,22 @@ def add_track_to_playlist(playlist_id: str): try: playlistlib.add_track(playlist_id, trackid) - except TrackExistsInPlaylist as e: + except TrackExistsInPlaylist: return {"error": "Track already exists in playlist"}, 409 return {"msg": "I think It's done"}, 200 @playlist_bp.route("/playlist/") -def get_single_p_info(playlistid: str): - p = UseBisection(api.PLAYLISTS, "playlistid", [playlistid])() - playlist: models.Playlist = p[0] +def get_playlist(playlistid: str): + p = instances.playlist_instance.get_playlist_by_id(playlistid) + if p is None: + return {"info": {}, "tracks": []} - if playlist is not None: - tracks = playlist.get_tracks() - return {"info": serializer.Playlist(playlist), "tracks": tracks} + playlist = models.Playlist(p) - return {"info": {}, "tracks": []} + tracks = playlistlib.create_playlist_tracks(playlist.pretracks) + return {"info": serializer.Playlist(playlist), "tracks": tracks} @playlist_bp.route("/playlist//update", methods=["PUT"]) @@ -109,21 +108,21 @@ def update_playlist(playlistid: str): p: models.Playlist = p[0] if playlist is not None: - if image: - image_, thumb_ = playlistlib.save_p_image(image, playlistid) - playlist["image"] = image_ - playlist["thumb"] = thumb_ + if image: + image_, thumb_ = playlistlib.save_p_image(image, playlistid) + playlist["image"] = image_ + playlist["thumb"] = thumb_ - else: - playlist["image"] = p.image.split("/")[-1] - playlist["thumb"] = p.thumb.split("/")[-1] + else: + playlist["image"] = p.image.split("/")[-1] + playlist["thumb"] = p.thumb.split("/")[-1] - p.update_playlist(playlist) - instances.playlist_instance.update_playlist(playlistid, playlist) + p.update_playlist(playlist) + instances.playlist_instance.update_playlist(playlistid, playlist) - return { - "data": serializer.Playlist(p), - } + return { + "data": serializer.Playlist(p), + } return {"msg": "Something shady happened"}, 500 diff --git a/server/app/api/search.py b/server/app/api/search.py index 52a54ca6..f5aac908 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -95,18 +95,9 @@ def search(): return { "data": [ - { - "tracks": tracks[:5], - "more": len(tracks) > 5 - }, - { - "albums": albums[:6], - "more": len(albums) > 6 - }, - { - "artists": artists_dicts[:6], - "more": len(artists_dicts) > 6 - }, + {"tracks": tracks[:5], "more": len(tracks) > 5}, + {"albums": albums[:6], "more": len(albums) > 6}, + {"artists": artists_dicts[:6], "more": len(artists_dicts) > 6}, ] } @@ -121,18 +112,18 @@ def search_load_more(): if type == "tracks": return { - "tracks": SEARCH_RESULTS["tracks"][index:index + 5], + "tracks": SEARCH_RESULTS["tracks"][index : index + 5], "more": len(SEARCH_RESULTS["tracks"]) > index + 5, } elif type == "albums": return { - "albums": SEARCH_RESULTS["albums"][index:index + 6], + "albums": SEARCH_RESULTS["albums"][index : index + 6], "more": len(SEARCH_RESULTS["albums"]) > index + 6, } elif type == "artists": return { - "artists": SEARCH_RESULTS["artists"][index:index + 6], + "artists": SEARCH_RESULTS["artists"][index : index + 6], "more": len(SEARCH_RESULTS["artists"]) > index + 6, } diff --git a/server/app/api/track.py b/server/app/api/track.py index 64bf7453..9a3eb587 100644 --- a/server/app/api/track.py +++ b/server/app/api/track.py @@ -6,6 +6,8 @@ from app import instances from flask import Blueprint from flask import send_file +from app import models + track_bp = Blueprint("track", __name__, url_prefix="/") @@ -14,21 +16,15 @@ def send_track_file(trackid): """ Returns an audio file that matches the passed id to the client. """ - try: - files = [] - for f in api.DB_TRACKS: - try: - if f["_id"]["$oid"] == trackid: - files.append(f["filepath"]) - except KeyError: - # Bug: some albums are not found although they exist in `api.ALBUMS`. It has something to do with the bisection method used or sorting. Not sure yet. - pass + track = instances.tracks_instance.get_track_by_id(trackid) - filepath = files[0] - except IndexError: + if track is None: return "File not found", 404 - return send_file(filepath, mimetype="audio/mp3") + track = models.Track(track) + type = track.filepath.split(".")[-1] + + return send_file(track.filepath, mimetype=f"audio/{type}") @track_bp.route("/sample") diff --git a/server/app/db/__init__.py b/server/app/db/__init__.py index e7157d74..fd406360 100644 --- a/server/app/db/__init__.py +++ b/server/app/db/__init__.py @@ -200,3 +200,15 @@ class TrackMethods: Removes a track from the database. Returns a boolean indicating success or failure of the operation. """ pass + + def find_tracks_by_hash(): + """ + Returns all the tracks matching the passed hash. + """ + pass + + def find_tracks_inside_path_regex(): + """ + Returns a list of all the tracks matching the path in the query params. + """ + pass diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 27b7feb5..b9e124be 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -21,16 +21,17 @@ 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 + def insert_many(self, albums: list): + """ + Inserts multiple albums into the database. + """ + return self.collection.insert_many(albums) + def get_all_albums(self) -> list: """ Returns all the albums in the database. @@ -52,9 +53,9 @@ class Albums(MongoAlbums): album = self.collection.find_one({"album": name, "artist": artist}) return convert_one(album) - def get_album_by_artist(self, name: str) -> dict: + def find_album_by_hash(self, hash: str) -> dict: """ - Returns a single album matching the artist in the query params. + Returns a single album matching the hash in the query params. """ - album = self.collection.find_one({"albumartist": name}) + album = self.collection.find_one({"hash": hash}) return convert_one(album) diff --git a/server/app/db/mongodb/playlists.py b/server/app/db/mongodb/playlists.py index d6df804b..7c588169 100644 --- a/server/app/db/mongodb/playlists.py +++ b/server/app/db/mongodb/playlists.py @@ -18,12 +18,8 @@ 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 @@ -41,25 +37,28 @@ class Playlists(MongoPlaylists): playlist = self.collection.find_one({"_id": ObjectId(id)}) return convert_one(playlist) - def add_track_to_playlist(self, playlistid: str, track: dict) -> None: + def set_last_updated(self, playlistid: str) -> None: """ - Adds a track to a playlist. + Sets the lastUpdated field to the current date. """ date = create_new_date() return self.collection.update_one( + {"_id": ObjectId(playlistid)}, + {"$set": {"lastUpdated": date}}, + ) + + def add_track_to_playlist(self, playlistid: str, track: dict) -> None: + """ + Adds a track to a playlist. + """ + self.collection.update_one( { "_id": ObjectId(playlistid), }, - { - "$push": { - "pre_tracks": track - }, - "$set": { - "lastUpdated": date - } - }, + {"$push": {"pre_tracks": track}}, ) + self.set_last_updated(playlistid) def get_playlist_by_name(self, name: str) -> dict: """ diff --git a/server/app/db/mongodb/tracks.py b/server/app/db/mongodb/tracks.py index 6fa72c95..9bd3d0d1 100644 --- a/server/app/db/mongodb/tracks.py +++ b/server/app/db/mongodb/tracks.py @@ -2,9 +2,7 @@ This file contains the AllSongs class for interacting with track documents in MongoDB. """ import pymongo -from app.db.mongodb import convert_many -from app.db.mongodb import convert_one -from app.db.mongodb import MongoTracks +from app.db.mongodb import MongoTracks, convert_many, convert_one from bson import ObjectId @@ -24,17 +22,23 @@ class Tracks(MongoTracks): {"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True ).upserted_id + def insert_many(self, songs: list): + """ + Inserts multiple songs into the database. + """ + return self.collection.insert_many(songs) + def get_all_tracks(self) -> list: """ Returns all tracks in the database. """ return convert_many(self.collection.find()) - def get_song_by_id(self, file_id: str) -> dict: + def get_track_by_id(self, id: str) -> dict: """ Returns a track object by its mongodb id. """ - song = self.collection.find_one({"_id": ObjectId(file_id)}) + song = self.collection.find_one({"_id": ObjectId(id)}) return convert_one(song) def get_song_by_album(self, name: str, artist: str) -> dict: @@ -81,11 +85,20 @@ class Tracks(MongoTracks): def find_songs_by_folder_og(self, query: str) -> list: """ - Returns an unsorted list of all the tracks exactly matching the folder in the query params + Returns an unsorted list of all the track matching the folder in the query params """ songs = self.collection.find({"folder": query}) return convert_many(songs) + def find_tracks_inside_path_regex(self, path: str) -> list: + """ + Returns a list of all the tracks matching the path in the query params. + """ + songs = self.collection.find( + {"filepath": {"$regex": f"^{path}", "$options": "i"}} + ) + return convert_many(songs) + def find_songs_by_artist(self, query: str) -> list: """ Returns a list of all the tracks exactly matching the artists in the query params. @@ -128,3 +141,21 @@ class Tracks(MongoTracks): return True except: return False + + def find_tracks_by_hash(self, hash: str) -> list: + """ + Returns a list of all the tracks matching the hash in the query params. + """ + 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: + """ + 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} + ) + return convert_one(song) diff --git a/server/app/functions.py b/server/app/functions.py index 31345332..f91f5ff4 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -14,7 +14,9 @@ from app.lib.populate import Populate from PIL import Image from concurrent.futures import ThreadPoolExecutor -from app.lib.trackslib import create_all_tracks +from app.lib.trackslib import validate_tracks +from app.lib import trackslib +from server.app import instances, models @helpers.background @@ -24,6 +26,8 @@ def reindex_tracks(): """ while True: + trackslib.validate_tracks() + populate() CheckArtistImages()() @@ -42,10 +46,6 @@ def populate(): pop = Populate() pop.run() - tracks = create_all_tracks() - api.TRACKS.clear() - api.TRACKS.extend(tracks) - class getArtistImage: """ @@ -104,11 +104,11 @@ class CheckArtistImages: """ Loops through all the tracks and gathers all the artists. """ + ar = instances.tracks_instance.get_all_tracks() + tracks = [models.Track(t) for t in ar] - for song in api.DB_TRACKS: - this_artists: list = song["artists"].split(", ") - - for artist in this_artists: + for t in tracks: + for artist in t.artists: if artist not in self.artists: self.artists.append(artist) diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index 2bf6d929..8a4e6baf 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -4,12 +4,8 @@ This library contains all the functions related to albums. import random from typing import List -from app import api -from app import helpers -from app import instances -from app import models -from app.lib import taglib -from app.lib import trackslib +from app import api, helpers, instances, models +from app.lib import taglib, trackslib from tqdm import tqdm @@ -30,21 +26,12 @@ def get_all_albums() -> List[models.Album]: return albums -def create_everything() -> List[models.Track]: +def validate() -> None: """ Creates album objects for all albums and returns a list of track objects """ - albums: list[models.Album] = get_all_albums() - api.ALBUMS = albums - api.ALBUMS.sort(key=lambda x: x.hash) - - tracks = trackslib.create_all_tracks() - - api.TRACKS.clear() - api.TRACKS.extend(tracks) - api.TRACKS.sort(key=lambda x: x.title) def find_album(albums: List[models.Album], hash: str) -> int | None: @@ -142,8 +129,6 @@ class GetAlbumTracks: self.tracks.remove(track) index = trackslib.find_track(self.tracks, self.hash) - # self.tracks.extend(tracks) - # self.tracks.sort(key=lambda x: x["albumhash"]) return tracks diff --git a/server/app/lib/folderslib.py b/server/app/lib/folderslib.py index bc70f830..81217298 100644 --- a/server/app/lib/folderslib.py +++ b/server/app/lib/folderslib.py @@ -1,15 +1,11 @@ from dataclasses import dataclass from os import scandir -from time import time -from typing import List -from typing import Set from typing import Tuple -from app import api -from app import helpers from app.models import Folder from app.models import Track -from tqdm import tqdm + +from app import instances @dataclass @@ -18,20 +14,12 @@ class Dir: is_sym: bool -def get_valid_folders() -> None: - for track in api.TRACKS: - api.VALID_FOLDERS.add(track.folder) - - def get_folder_track_count(foldername: str) -> int: """ Returns the number of files associated with a folder. """ - count = 0 - for track in api.TRACKS: - if foldername in track.folder: - count += 1 - return count + tracks = instances.tracks_instance.find_tracks_inside_path_regex(foldername) + return len(tracks) def create_folder(dir: Dir) -> Folder: @@ -46,51 +34,6 @@ def create_folder(dir: Dir) -> Folder: return Folder(folder) -def create_all_folders() -> Set[Folder]: - folders: List[Folder] = [] - - for foldername in tqdm(api.VALID_FOLDERS, desc="Creating folders"): - folder = create_folder(foldername) - folders.append(folder) - - return folders - - -def get_subdirs(foldername: str) -> List[Folder]: - """ - Finds and Creates Folder objects for each sub-directory string in the foldername passed. - """ - subdirs = set() - - for folder in api.VALID_FOLDERS: - if foldername in folder: - str0 = folder.replace(foldername, "") - - try: - str1 = str0.split("/")[1] - except IndexError: - str1 = None - - if str1 is not None: - subdirs.add(foldername + "/" + str1) - return [create_folder(dir) for dir in subdirs] - - -@helpers.background -def run_scandir(): - """ - Initiates the creation of all folder objects for each folder with a track in it. - - Runs in a background thread after every 5 minutes. - It calls the - """ - get_valid_folders() - # folders_ = create_all_folders() - """Create all the folder objects before clearing api.FOLDERS""" - - # api.FOLDERS = folders_ - - class getFnF: """ Get files and folders from a directory. @@ -99,15 +42,6 @@ class getFnF: def __init__(self, path: str) -> None: self.path = path - @classmethod - def get_tracks(cls, files: List[str]) -> List[Track]: - """ - Returns a list of Track objects for each file in the given list. - """ - tracks = helpers.UseBisection(api.TRACKS, "filepath", files)() - tracks = filter(lambda t: t is not None, tracks) - return list(tracks) - def __call__(self) -> Tuple[Track, Folder]: try: all = scandir(self.path) @@ -125,9 +59,11 @@ class getFnF: dirs.append(Dir(**dir)) elif entry.is_file() and entry.name.endswith((".mp3", ".flac")): files.append(entry.path) - tracks = self.get_tracks(files) + tracks = instances.tracks_instance.find_songs_by_folder(self.path) + tracks = [Track(track) for track in tracks] folders = [create_folder(dir) for dir in dirs] + folders = filter(lambda f: f.trackcount > 0, folders) return tracks, folders diff --git a/server/app/lib/playlistlib.py b/server/app/lib/playlistlib.py index aabc4cf0..88cdcad5 100644 --- a/server/app/lib/playlistlib.py +++ b/server/app/lib/playlistlib.py @@ -5,6 +5,7 @@ import os import random import string from datetime import datetime +from typing import List from tqdm import tqdm @@ -13,55 +14,37 @@ from app import exceptions from app import instances from app import models from app import settings -from app.lib import trackslib from PIL import Image from PIL import ImageSequence -from progress.bar import Bar from werkzeug import datastructures +from app.lib import trackslib + TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist def add_track(playlistid: str, trackid: str): """ - Adds a track to a playlist in the api.PLAYLISTS dict and to the database. + Adds a track to a playlist to the database. """ - for playlist in api.PLAYLISTS: - if playlist.playlistid == playlistid: - tt = trackslib.get_track_by_id(trackid) + tt = instances.tracks_instance.get_track_by_id(trackid) - track = { - "title": tt.title, - "artists": tt.artists, - "album": tt.album, - } + if tt is None: + return - try: - playlist.add_track(track) - instances.playlist_instance.add_track_to_playlist( - playlistid, track) - return - except TrackExistsInPlaylist as error: - raise error + track = models.Track(tt) + playlist = instances.playlist_instance.get_playlist_by_id(playlistid) -def get_playlist_tracks(pid: str): - for p in api.PLAYLISTS: - if p.playlistid == pid: - return p.tracks + track = { + "title": track.title, + "artists": tt["artists"], + "album": track.album, + } + if track in playlist["pre_tracks"]: + raise TrackExistsInPlaylist - -def create_all_playlists(): - """ - Gets all playlists from the database. - """ - playlists = instances.playlist_instance.get_all_playlists() - - - for playlist in tqdm(playlists, desc="Creating playlists"): - api.PLAYLISTS.append(models.Playlist(playlist)) - - validate_images() + instances.playlist_instance.add_track_to_playlist(playlistid, track) def create_thumbnail(image: any, img_path: str) -> str: @@ -69,8 +52,7 @@ 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 @@ -88,13 +70,11 @@ 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 = [] @@ -118,8 +98,10 @@ def validate_images(): Removes all unused images in the images/playlists folder. """ images = [] + p = instances.playlist_instance.get_all_playlists() + playlists = [models.Playlist(p) for p in p] - for playlist in api.PLAYLISTS: + for playlist in playlists: if playlist.image: img_path = playlist.image.split("/")[-1] thumb_path = playlist.thumb.split("/")[-1] @@ -136,3 +118,18 @@ def validate_images(): def create_new_date(): return datetime.now() + + +def create_playlist_tracks(playlist_tracks: List) -> List[models.Track]: + """ + Creates a list of model.Track objects from a list of playlist track dicts. + """ + tracks: List[models.Track] = [] + + for t in playlist_tracks: + track = trackslib.get_p_track(t) + + if track is not None: + tracks.append(models.Track(track)) + + return tracks diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index f2f7102f..f5500197 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -1,4 +1,5 @@ import os +from pprint import pprint import time from concurrent.futures import ThreadPoolExecutor from copy import deepcopy @@ -12,7 +13,6 @@ from app.helpers import create_album_hash from app.helpers import run_fast_scandir from app.instances import album_instance from app.instances import tracks_instance -from app.lib import folderslib from app.lib.albumslib import create_album from app.lib.albumslib import find_album from app.lib.taglib import get_tags @@ -44,6 +44,7 @@ class Populate: self.db_tracks = tracks_instance.get_all_tracks() self.tag_count = 0 self.exist_count = 0 + self.tracks = [] def run(self): self.check_untagged() @@ -53,17 +54,14 @@ class Populate: return self.tagged_tracks.sort(key=lambda x: x["albumhash"]) - self.tracks = deepcopy(self.tagged_tracks) self.pre_albums = self.create_pre_albums(self.tagged_tracks) self.create_albums(self.pre_albums) self.albums.sort(key=lambda x: x.hash) - api.ALBUMS.sort(key=lambda x: x.hash) - - self.save_albums() self.create_tracks() - # self.create_folders() + + self.save_all() def check_untagged(self): """ @@ -84,7 +82,6 @@ class Populate: t["albumhash"] = create_album_hash(t["album"], t["albumartist"]) self.tagged_tracks.append(t) - api.DB_TRACKS.append(t) self.folders.add(t["folder"]) @@ -95,8 +92,7 @@ class Populate: folder = tags["folder"] self.folders.add(folder) - tags["albumhash"] = create_album_hash(tags["album"], - tags["albumartist"]) + tags["albumhash"] = create_album_hash(tags["album"], tags["albumartist"]) self.tagged_tracks.append(tags) api.DB_TRACKS.append(tags) @@ -110,13 +106,6 @@ class Populate: with ThreadPoolExecutor() as executor: executor.map(self.get_tags, self.files) - # with Pool(maxtasksperchild=10, processes=10) as p: - # tags = p.map(get_tags, tqdm(self.files)) - # self.process_tags(tags) - - # for t in tqdm(self.files): - # self.get_tags(t) - d = time.time() - s Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds") @@ -147,7 +136,6 @@ class Populate: self.exist_count += 1 return - self.albums.sort(key=lambda x: x.hash) index = find_track(self.tagged_tracks, albumhash) if index is None: @@ -163,7 +151,6 @@ class Populate: album = Album(album) - api.ALBUMS.append(album) self.albums.append(album) def create_albums(self, albums: List[dict]): @@ -173,8 +160,7 @@ class Populate: for album in tqdm(albums, desc="Building albums"): self.create_album(album) - Log(f"{self.exist_count} of {len(albums)} albums were already in the database" - ) + Log(f"{self.exist_count} of {len(albums)} albums were already in the database") def create_track(self, track: dict): """ @@ -196,38 +182,25 @@ class Populate: pass track["image"] = album.image - - upsert_id = tracks_instance.insert_song(track) - track["_id"] = {"$oid": str(upsert_id)} - - api.TRACKS.append(Track(track)) + return track def create_tracks(self): """ Loops through all the tagged tracks creating complete track objects using the `models.Track` model. """ with ThreadPoolExecutor() as executor: - executor.map(self.create_track, self.tagged_tracks) + iterable = executor.map(self.create_track, self.tagged_tracks) - Log(f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums" - ) + self.tracks = [t for t in iterable if t is not None] - def save_albums(self): + Log( + f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums" + ) + + def save_all(self): """ Saves the albums to the database. """ - with ThreadPoolExecutor() as executor: - executor.map(album_instance.insert_album, self.albums) - - # def create_folders(self): - # """ - # Creates the folder objects for all the tracks. - # """ - # for folder in tqdm(self.folders, desc="Creating folders"): - # api.VALID_FOLDERS.add(folder) - - # fff = folderslib.create_folder(folder) - # api.FOLDERS.append(fff) - - # Log(f"Created {len(self.folders)} new folders") + album_instance.insert_many([a.__dict__ for a in self.albums]) + tracks_instance.insert_many(self.tracks) diff --git a/server/app/lib/trackslib.py b/server/app/lib/trackslib.py index f580e6cf..3c73af39 100644 --- a/server/app/lib/trackslib.py +++ b/server/app/lib/trackslib.py @@ -2,31 +2,25 @@ This library contains all the functions related to tracks. """ import os +from pprint import pprint from typing import List -from app import api -from app import instances -from app import models +from app import api, instances, models from app.helpers import remove_duplicates from tqdm import tqdm -def create_all_tracks() -> List[models.Track]: +def validate_tracks() -> None: """ Gets all songs under the ~/ directory. """ - tracks: list[models.Track] = [] + entries = instances.tracks_instance.get_all_tracks() - for track in tqdm(api.DB_TRACKS, desc="Creating tracks"): + for track in tqdm(entries, desc="Validating tracks"): try: os.chmod(track["filepath"], 0o755) except FileNotFoundError: instances.tracks_instance.remove_song_by_id(track["_id"]["$oid"]) - api.DB_TRACKS.remove(track) - - tracks.append(models.Track(track)) - - return tracks def get_album_tracks(albumname, artist): @@ -48,7 +42,6 @@ def get_track_by_id(trackid: str) -> models.Track: return track except AttributeError: print("AttributeError") - print(track) def find_track(tracks: list, hash: str) -> int or None: @@ -73,3 +66,9 @@ def find_track(tracks: list, hash: str) -> int or None: right = mid - 1 return None + + +def get_p_track(ptrack): + return instances.tracks_instance.find_track_by_title_artists_album( + ptrack["title"], ptrack["artists"], ptrack["album"] + ) diff --git a/server/app/models.py b/server/app/models.py index f02e100d..3456a707 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -4,7 +4,7 @@ Contains all the models for objects generation and typing. from dataclasses import dataclass, field from typing import List -from app import api, helpers +from app import helpers from app.exceptions import TrackExistsInPlaylist @@ -117,30 +117,6 @@ class Album: return self.artist.lower() == "various artists" -def get_p_track(ptrack): - for track in api.TRACKS: - if ( - track.title == ptrack["title"] - and track.artists == ptrack["artists"] - and ptrack["album"] == track.album - ): - return track - - -def create_playlist_tracks(playlist_tracks: List) -> List[Track]: - """ - Creates a list of model.Track objects from a list of playlist track dicts. - """ - tracks: List[Track] = [] - - for t in playlist_tracks: - track = get_p_track(t) - if track is not None: - tracks.append(track) - - return tracks - - @dataclass class Playlist: """Creates playlist objects""" @@ -148,7 +124,7 @@ class Playlist: playlistid: str name: str tracks: List[Track] - _pre_tracks: list = field(init=False, repr=False) + pretracks: list = field(init=False, repr=False) lastUpdated: int image: str thumb: str @@ -162,16 +138,11 @@ class Playlist: self.description = data["description"] self.image = self.create_img_link(data["image"]) self.thumb = self.create_img_link(data["thumb"]) - self._pre_tracks = data["pre_tracks"] + self.pretracks = data["pre_tracks"] self.tracks = [] self.lastUpdated = data["lastUpdated"] - self.count = len(self._pre_tracks) + self.count = len(self.pretracks) - def get_tracks(self) -> List[Track]: - """ - Generates and returns Track objects from pre_tracks - """ - return create_playlist_tracks(self._pre_tracks) def create_img_link(self, image: str): if image: @@ -180,11 +151,11 @@ class Playlist: return "default.webp" def update_count(self): - self.count = len(self._pre_tracks) + self.count = len(self.pretracks) def add_track(self, track): - if track not in self._pre_tracks: - self._pre_tracks.append(track) + if track not in self.pretracks: + self.pretracks.append(track) self.update_count() self.lastUpdated = helpers.create_new_date() else: diff --git a/server/app/settings.py b/server/app/settings.py index 29029ae7..fb840a26 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -11,7 +11,8 @@ CONFIG_FOLDER = ".alice" HOME_DIR = os.path.expanduser("~") APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER) THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails") -TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill" +TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio" +HOME_DIR = TEST_DIR # URL IMG_BASE_URI = "http://127.0.0.1:8900/images/" IMG_ARTIST_URI = IMG_BASE_URI + "artists/" @@ -38,6 +39,7 @@ P_COLORS = [ CPU_COUNT = multiprocessing.cpu_count() + class logger: enable = True diff --git a/src/composables/album.ts b/src/composables/album.ts index 44518b2a..8b38e007 100644 --- a/src/composables/album.ts +++ b/src/composables/album.ts @@ -9,13 +9,13 @@ const getAlbumTracks = async (album: string, artist: string) => { }; await axios - .post(state.settings.uri + "/album/tracks", { + .post(state.settings.uri + "/album", { album: album, artist: artist, }) .then((res) => { data.info = res.data.info; - data.tracks = res.data.songs; + data.tracks = res.data.tracks; }) .catch((err: AxiosError) => { console.error(err); From b6c5c57186df135ed9b30e316ec41d51a3c7608b Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 13 Jun 2022 17:11:58 +0300 Subject: [PATCH 02/56] create methods to fetch data from the database - start using a class to hold search query results --- server/app/api/search.py | 49 +++++++++++++++++++++++++++++++++++++ server/app/helpers.py | 40 ++++++++++++++++++++++++++++-- server/app/lib/searchlib.py | 35 ++++++-------------------- server/app/models.py | 2 +- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/server/app/api/search.py b/server/app/api/search.py index f5aac908..4ba3834a 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -1,11 +1,14 @@ """ Contains all the search routes. """ +from typing import List from app import helpers from app.lib import searchlib from flask import Blueprint from flask import request +from server.app import instances, models + search_bp = Blueprint("search", __name__, url_prefix="/") SEARCH_RESULTS = { @@ -15,6 +18,52 @@ SEARCH_RESULTS = { } +class SearchResults: + """ + Holds all the search results. + """ + + query: str = "" + + class Tracks: + """ + Holds all the tracks search results. + """ + + results: List[models.Track] + + class Albums: + """ + Holds all the albums search results. + """ + + results: List[models.Album] + + class Artists: + """ + Holds all the artists search results. + """ + + results: List[models.Artist] + + +class DoSearch: + def __init__(self, query: str) -> None: + self.query = query + self.tracks = helpers.Get.get_all_tracks() + self.albums = helpers.Get.get_all_albums() + self.artists = helpers.Get.get_all_artists() + self.playlists = helpers.Get.get_all_playlists() + + def search_tracks(self): + results = searchlib.SearchTracks(self.tracks, self.query) + + def search_artists(self): + SearchResults.Artists.results = searchlib.SearchArtists( + self.artists, self.query + ) + + @search_bp.route("/search/tracks", methods=["GET"]) def search_tracks(): """ diff --git a/server/app/helpers.py b/server/app/helpers.py index e57b2404..2db60988 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -10,6 +10,7 @@ from typing import List from app import models from app import settings +from app import instances app_dir = settings.APP_DIR @@ -136,9 +137,9 @@ def create_safe_name(name: str) -> str: class UseBisection: """ - Uses bisection to find a list of items in another list. + Uses bisection to find a list of items in another list. - returns a list of found items with `None` items being not found + returns a list of found items with `None` items being not found items. """ @@ -166,3 +167,38 @@ class UseBisection: def __call__(self) -> List: return [self.find(query) for query in self.queries] + + +class Get: + @staticmethod + def get_all_tracks() -> List[models.Track]: + """ + Returns all tracks + """ + t = instances.tracks_instance.get_all_tracks() + return [models.Track(t) for t in t] + + def get_all_albums() -> List[models.Album]: + """ + Returns all albums + """ + a = instances.album_instance.get_all_albums() + return [models.Album(a) for a in a] + + def get_all_artists(self) -> set[str]: + tracks = self.get_all_tracks() + artists: set[str] = set() + + for track in tracks: + for artist in track.artists: + artists.add(artist.lower()) + + return artists + + @staticmethod + def get_all_playlists() -> List[models.Playlist]: + """ + Returns all playlists + """ + p = instances.playlist_instance.get_all_playlists() + return [models.Playlist(p) for p in p] diff --git a/server/app/lib/searchlib.py b/server/app/lib/searchlib.py index 6b8ff223..6d0016b8 100644 --- a/server/app/lib/searchlib.py +++ b/server/app/lib/searchlib.py @@ -19,9 +19,9 @@ class Cutoff: Holds all the default cutoff values. """ - tracks: int = 70 - albums: int = 70 - artists: int = 70 + tracks: int = 80 + albums: int = 80 + artists: int = 80 class Limit: @@ -35,7 +35,6 @@ class Limit: class SearchTracks: - def __init__(self, query) -> None: self.query = query @@ -57,35 +56,18 @@ class SearchTracks: class SearchArtists: - - def __init__(self, query) -> None: + def __init__(self, artists: set[str], query) -> None: self.query = query - - @staticmethod - def get_all_artist_names() -> List[str]: - """ - Gets all artist names. - """ - - artists = [track.artists for track in api.TRACKS] - - f_artists = set() - - for artist in artists: - for a in artist: - f_artists.add(a) - - return f_artists + self.artists = artists def __call__(self) -> list: """ Gets all artists with a given name. """ - artists = self.get_all_artist_names() results = process.extract( self.query, - artists, + self.artists, scorer=fuzz.WRatio, score_cutoff=Cutoff.artists, limit=Limit.artists, @@ -103,7 +85,6 @@ class SearchArtists: class SearchAlbums: - def __init__(self, query) -> None: self.query = query @@ -141,7 +122,6 @@ class SearchAlbums: class GetTopArtistTracks: - def __init__(self, artist: str) -> None: self.artist = artist @@ -165,6 +145,5 @@ def get_artists(artist: str) -> List[models.Track]: Gets all songs with a given artist. """ return [ - track for track in api.TRACKS - if artist.lower() in str(track.artists).lower() + track for track in api.TRACKS if artist.lower() in str(track.artists).lower() ] diff --git a/server/app/models.py b/server/app/models.py index 3456a707..b8bbfe88 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -16,7 +16,7 @@ class Track: trackid: str title: str - artists: list + artists: list[str] albumartist: str album: str folder: str From 88247e0553b45a901235b4c6e2917cc1e45b5ea2 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 13 Jun 2022 20:40:23 +0300 Subject: [PATCH 03/56] blind write search methods --- server/app/api/search.py | 144 +++++++++++++++++++++++++----------- server/app/helpers.py | 6 +- server/app/lib/albumslib.py | 18 ----- server/app/lib/searchlib.py | 65 +++++++--------- 4 files changed, 130 insertions(+), 103 deletions(-) diff --git a/server/app/api/search.py b/server/app/api/search.py index 4ba3834a..f4d2fc42 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -24,62 +24,91 @@ class SearchResults: """ query: str = "" - - class Tracks: - """ - Holds all the tracks search results. - """ - - results: List[models.Track] - - class Albums: - """ - Holds all the albums search results. - """ - - results: List[models.Album] - - class Artists: - """ - Holds all the artists search results. - """ - - results: List[models.Artist] + tracks: list[models.Track] + albums: list[models.Album] + playlists: list[models.Playlist] + artists: list[models.Artist] class DoSearch: + """Class containing the methods that perform searching.""" + def __init__(self, query: str) -> None: + """ + :param :str:`query`: the search query. + """ self.query = query - self.tracks = helpers.Get.get_all_tracks() - self.albums = helpers.Get.get_all_albums() - self.artists = helpers.Get.get_all_artists() - self.playlists = helpers.Get.get_all_playlists() + SearchResults.query = query def search_tracks(self): - results = searchlib.SearchTracks(self.tracks, self.query) + """Calls :class:`SearchTracks` which returns the tracks that fuzzily match + the search terms. Then adds them to the `SearchResults` store. + """ + self.tracks = helpers.Get.get_all_tracks() + tracks = searchlib.SearchTracks(self.tracks, self.query)() + SearchResults.tracks = tracks + + return tracks def search_artists(self): - SearchResults.Artists.results = searchlib.SearchArtists( - self.artists, self.query - ) + """Calls :class:`SearchArtists` which returns the artists that fuzzily match + the search term. Then adds them to the `SearchResults` store. + """ + self.artists = helpers.Get.get_all_artists() + artists = searchlib.SearchArtists(self.artists, self.query)() + SearchResults.artists = artists + + return artists + + def search_albums(self): + """Calls :class:`SearchAlbums` which returns the albums that fuzzily match + the search term. Then adds them to the `SearchResults` store. + """ + self.albums = helpers.Get.get_all_albums() + albums = searchlib.SearchAlbums(self.albums, self.query)() + SearchResults.albums = albums + + return albums + + def search_playlists(self): + """Calls :class:`SearchPlaylists` which returns the playlists that fuzzily match + the search term. Then adds them to the `SearchResults` store. + """ + self.playlists = helpers.Get.get_all_playlists() + playlists = searchlib.SearchPlaylists(self.playlists, self.query) + SearchResults.playlists = playlists + + return playlists + + def search_all(self): + """Calls all the search methods.""" + self.search_tracks() + self.search_albums() + self.search_artists() + self.search_playlists() @search_bp.route("/search/tracks", methods=["GET"]) def search_tracks(): """ - Searches for tracks. + Searches for tracks that match the search query. """ query = request.args.get("q") if not query: return {"error": "No query provided"}, 400 - results = searchlib.SearchTracks(query)() - SEARCH_RESULTS["tracks"] = results + if SearchResults.query == query and len(SearchResults.tracks) > 0: + return { + "tracks": SearchResults.tracks[:5], + "more": len(SearchResults.tracks) > 5, + }, 200 + + tracks = DoSearch(query).search_tracks() return { - "tracks": results[:5], - "more": len(results) > 5, + "tracks": tracks[:5], + "more": len(tracks) > 5, }, 200 @@ -93,12 +122,17 @@ def search_albums(): if not query: return {"error": "No query provided"}, 400 - results = searchlib.SearchAlbums(query)() - SEARCH_RESULTS["albums"] = results + if SearchResults.query == query and len(SearchResults.albums) > 0: + return { + "albums": SearchResults.albums[:6], + "more": len(SearchResults.albums) > 6, + }, 200 + + tracks = DoSearch(query).search_albums() return { - "albums": results[:6], - "more": len(results) > 6, + "albums": tracks[:6], + "more": len(tracks) > 6, }, 200 @@ -112,15 +146,41 @@ def search_artists(): if not query: return {"error": "No query provided"}, 400 - results = searchlib.SearchArtists(query)() - SEARCH_RESULTS["artists"] = results + if SearchResults.query == query and len(SearchResults.artists) > 0: + return { + "artists": SearchResults.artists[:6], + "more": len(SearchResults.artists) > 6, + }, 200 + + artists = DoSearch(query).search_artists() return { - "artists": results[:6], - "more": len(results) > 6, + "artists": artists[:6], + "more": len(artists) > 6, }, 200 +@search_bp.route("/search/top", methods=["GET"]) +def get_top_results(): + """ + Returns the top results for the search query. + """ + + query = request.args.get("q") + if not query: + return {"error": "No query provided"}, 400 + + DoSearch(query).search_all() + + max = 2 + return { + "tracks": SearchResults.tracks[:max], + "albums": SearchResults.albums[:max], + "artists": SearchResults.artists[:max], + "playlists": SearchResults.playlists[:max], + } + + @search_bp.route("/search") def search(): """ diff --git a/server/app/helpers.py b/server/app/helpers.py index 2db60988..dbe0981c 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -5,7 +5,7 @@ import os import random import threading from datetime import datetime -from typing import Dict +from typing import Dict, Set from typing import List from app import models @@ -185,9 +185,9 @@ class Get: a = instances.album_instance.get_all_albums() return [models.Album(a) for a in a] - def get_all_artists(self) -> set[str]: + def get_all_artists(self) -> Set[str]: tracks = self.get_all_tracks() - artists: set[str] = set() + artists: Set[str] = set() for track in tracks: for artist in track.artists: diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index 8a4e6baf..edaec8c6 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -157,21 +157,3 @@ def create_album(track: dict, tracklist: list) -> dict: # album["image"] = "".join(x for x in albumhash if x not in "\/:*?<>|") return album - - -def search_albums_by_name(query: str) -> List[models.Album]: - """ - Searches albums by album name. - """ - title_albums: List[models.Album] = [] - artist_albums: List[models.Album] = [] - - for album in api.ALBUMS: - if query.lower() in album.title.lower(): - title_albums.append(album) - - for album in api.ALBUMS: - if query.lower() in album.artist.lower(): - artist_albums.append(album) - - return [*title_albums, *artist_albums] diff --git a/server/app/lib/searchlib.py b/server/app/lib/searchlib.py index 6d0016b8..30744dc8 100644 --- a/server/app/lib/searchlib.py +++ b/server/app/lib/searchlib.py @@ -22,6 +22,7 @@ class Cutoff: tracks: int = 80 albums: int = 80 artists: int = 80 + playlists: int = 80 class Limit: @@ -32,18 +33,20 @@ class Limit: tracks: int = 50 albums: int = 50 artists: int = 50 + playlists: int = 50 class SearchTracks: - def __init__(self, query) -> None: + def __init__(self, tracks: List[models.Track], query: str) -> None: self.query = query + self.tracks = tracks def __call__(self) -> List[models.Track]: """ Gets all songs with a given title. """ - tracks = [track.title for track in api.TRACKS] + tracks = [track.title for track in self.tracks] results = process.extract( self.query, tracks, @@ -52,11 +55,11 @@ class SearchTracks: limit=Limit.tracks, ) - return [api.TRACKS[i[2]] for i in results] + return [self.tracks[i[2]] for i in results] class SearchArtists: - def __init__(self, artists: set[str], query) -> None: + def __init__(self, artists: set[str], query: str) -> None: self.query = query self.artists = artists @@ -85,24 +88,16 @@ class SearchArtists: class SearchAlbums: - def __init__(self, query) -> None: + def __init__(self, albums: List[models.Album], query: str) -> None: self.query = query - - def get_albums_by_name(self) -> List[models.Album]: - """ - Gets all albums with a given title. - """ - - albums = [album.title for album in api.ALBUMS] - results = process.extract(self.query, albums) - return [api.ALBUMS[i[2]] for i in results] + self.albums = albums def __call__(self) -> List[models.Album]: """ Gets all albums with a given title. """ - albums = [a.title for a in api.ALBUMS] + albums = [a.title for a in self.albums] results = process.extract( self.query, albums, @@ -111,7 +106,7 @@ class SearchAlbums: limit=Limit.albums, ) - return [api.ALBUMS[i[2]] for i in results] + return [self.albums[i[2]] for i in results] # get all artists that matched the query # for get all albums from the artists @@ -121,29 +116,19 @@ class SearchAlbums: # recheck next and previous artist on play next or add to playlist -class GetTopArtistTracks: - def __init__(self, artist: str) -> None: - self.artist = artist +class SearchPlaylists: + def __init__(self, playlists: List[models.Playlist], query: str) -> None: + self.playlists = playlists + self.query = query - def __call__(self) -> List[models.Track]: - """ - Gets all tracks from a given artist. - """ + def __call__(self) -> List[models.Playlist]: + playlists = [p.name for p in self.playlists] + results = process.extract( + self.query, + playlists, + scorer=fuzz.WRatio, + score_cutoff=Cutoff.playlists, + limit=Limit.playlists, + ) - return [track for track in api.TRACKS if self.artist in track.artists] - - -def get_search_albums(query: str) -> List[models.Album]: - """ - Gets all songs with a given album. - """ - return albumslib.search_albums_by_name(query) - - -def get_artists(artist: str) -> List[models.Track]: - """ - Gets all songs with a given artist. - """ - return [ - track for track in api.TRACKS if artist.lower() in str(track.artists).lower() - ] + return [self.playlists[i[2]] for i in results] From 79a71618279cc9ae208e0088f7290f3d1e09302a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 16 Jun 2022 09:36:42 +0300 Subject: [PATCH 04/56] fix search to read from database --- server/app/api/album.py | 3 +- server/app/api/search.py | 77 ++++++++++--------- server/app/db/mongodb/playlists.py | 4 +- server/app/functions.py | 19 +---- server/app/helpers.py | 5 +- server/app/lib/searchlib.py | 8 +- .../RightSideBar/Search/AlbumGrid.vue | 2 +- .../RightSideBar/Search/ArtistGrid.vue | 2 +- .../RightSideBar/Search/TracksGrid.vue | 2 +- src/composables/loadmore.js | 0 src/composables/searchMusic.ts | 1 + src/stores/search.ts | 8 +- 12 files changed, 67 insertions(+), 64 deletions(-) delete mode 100644 src/composables/loadmore.js diff --git a/server/app/api/album.py b/server/app/api/album.py index 2624966d..1ae004af 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -91,6 +91,7 @@ def get_albumartists(): for track in tracks: for artist in track.artists: + artist = artist.lower() if artist not in artists: artists.append(artist) @@ -98,7 +99,7 @@ def get_albumartists(): for artist in artists: artist_obj = { "name": artist, - "image": helpers.check_artist_image(artist), + "image": helpers.check_artist_image(helpers.create_safe_name(artist)), } final_artists.append(artist_obj) diff --git a/server/app/api/search.py b/server/app/api/search.py index f4d2fc42..15ab35ca 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -1,13 +1,15 @@ """ Contains all the search routes. """ +from pprint import pprint from typing import List from app import helpers from app.lib import searchlib from flask import Blueprint from flask import request -from server.app import instances, models +from app import models +from app import serializer search_bp = Blueprint("search", __name__, url_prefix="/") @@ -24,10 +26,10 @@ class SearchResults: """ query: str = "" - tracks: list[models.Track] - albums: list[models.Album] - playlists: list[models.Playlist] - artists: list[models.Artist] + tracks: list[models.Track] = [] + albums: list[models.Album] = [] + playlists: list[models.Playlist] = [] + artists: list[models.Artist] = [] class DoSearch: @@ -64,8 +66,8 @@ class DoSearch: """Calls :class:`SearchAlbums` which returns the albums that fuzzily match the search term. Then adds them to the `SearchResults` store. """ - self.albums = helpers.Get.get_all_albums() - albums = searchlib.SearchAlbums(self.albums, self.query)() + albums = helpers.Get.get_all_albums() + albums = searchlib.SearchAlbums(albums, self.query)() SearchResults.albums = albums return albums @@ -74,8 +76,10 @@ class DoSearch: """Calls :class:`SearchPlaylists` which returns the playlists that fuzzily match the search term. Then adds them to the `SearchResults` store. """ - self.playlists = helpers.Get.get_all_playlists() - playlists = searchlib.SearchPlaylists(self.playlists, self.query) + playlists = helpers.Get.get_all_playlists() + playlists = [serializer.Playlist(playlist) for playlist in playlists] + + playlists = searchlib.SearchPlaylists(playlists, self.query)() SearchResults.playlists = playlists return playlists @@ -98,17 +102,11 @@ def search_tracks(): if not query: return {"error": "No query provided"}, 400 - if SearchResults.query == query and len(SearchResults.tracks) > 0: - return { - "tracks": SearchResults.tracks[:5], - "more": len(SearchResults.tracks) > 5, - }, 200 - tracks = DoSearch(query).search_tracks() return { - "tracks": tracks[:5], - "more": len(tracks) > 5, + "tracks": tracks[:6], + "more": len(tracks) > 6, }, 200 @@ -122,12 +120,6 @@ def search_albums(): if not query: return {"error": "No query provided"}, 400 - if SearchResults.query == query and len(SearchResults.albums) > 0: - return { - "albums": SearchResults.albums[:6], - "more": len(SearchResults.albums) > 6, - }, 200 - tracks = DoSearch(query).search_albums() return { @@ -146,12 +138,6 @@ def search_artists(): if not query: return {"error": "No query provided"}, 400 - if SearchResults.query == query and len(SearchResults.artists) > 0: - return { - "artists": SearchResults.artists[:6], - "more": len(SearchResults.artists) > 6, - }, 200 - artists = DoSearch(query).search_artists() return { @@ -160,6 +146,24 @@ def search_artists(): }, 200 +@search_bp.route("/search/playlists", methods=["GET"]) +def search_playlists(): + """ + Searches for playlists. + """ + + query = request.args.get("q") + if not query: + return {"error": "No query provided"}, 400 + + playlists = DoSearch(query).search_playlists() + + return { + "playlists": playlists[:6], + "more": len(playlists) > 6, + }, 200 + + @search_bp.route("/search/top", methods=["GET"]) def get_top_results(): """ @@ -220,19 +224,22 @@ def search_load_more(): index = int(request.args.get("index")) if type == "tracks": + t = SearchResults.tracks return { - "tracks": SEARCH_RESULTS["tracks"][index : index + 5], - "more": len(SEARCH_RESULTS["tracks"]) > index + 5, + "tracks": t[index : index + 5], + "more": len(t) > index + 5, } elif type == "albums": + a = SearchResults.albums return { - "albums": SEARCH_RESULTS["albums"][index : index + 6], - "more": len(SEARCH_RESULTS["albums"]) > index + 6, + "albums": a[index : index + 6], + "more": len(a) > index + 6, } elif type == "artists": + a = SearchResults.artists return { - "artists": SEARCH_RESULTS["artists"][index : index + 6], - "more": len(SEARCH_RESULTS["artists"]) > index + 6, + "artists": a[index : index + 6], + "more": len(a) > index + 6, } diff --git a/server/app/db/mongodb/playlists.py b/server/app/db/mongodb/playlists.py index 7c588169..9b730c48 100644 --- a/server/app/db/mongodb/playlists.py +++ b/server/app/db/mongodb/playlists.py @@ -4,7 +4,7 @@ This file contains the Playlists class for interacting with the playlist documen from app.db.mongodb import convert_many from app.db.mongodb import convert_one from app.db.mongodb import MongoPlaylists -from app.helpers import create_new_date +from app import helpers from bson import ObjectId @@ -41,7 +41,7 @@ class Playlists(MongoPlaylists): """ Sets the lastUpdated field to the current date. """ - date = create_new_date() + date = helpers.create_new_date() return self.collection.update_one( {"_id": ObjectId(playlistid)}, diff --git a/server/app/functions.py b/server/app/functions.py index f91f5ff4..9c972192 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -14,9 +14,8 @@ from app.lib.populate import Populate from PIL import Image from concurrent.futures import ThreadPoolExecutor -from app.lib.trackslib import validate_tracks from app.lib import trackslib -from server.app import instances, models +from app import instances, models @helpers.background @@ -100,18 +99,6 @@ class CheckArtistImages: else: return False - def gather_artists(self): - """ - Loops through all the tracks and gathers all the artists. - """ - ar = instances.tracks_instance.get_all_tracks() - tracks = [models.Track(t) for t in ar] - - for t in tracks: - for artist in t.artists: - if artist not in self.artists: - self.artists.append(artist) - @classmethod def download_image(cls, artistname: str): """ @@ -123,7 +110,7 @@ class CheckArtistImages: img_path = ( helpers.app_dir + "/images/artists/" - + artistname.replace("/", "::") + + helpers.create_safe_name(artistname) + ".webp" ) @@ -138,7 +125,7 @@ class CheckArtistImages: useImageDownloader(url, img_path)() def __call__(self): - self.gather_artists() + self.artists = helpers.Get.get_all_artists() with ThreadPoolExecutor() as pool: pool.map(self.download_image, self.artists) diff --git a/server/app/helpers.py b/server/app/helpers.py index dbe0981c..e6196ecc 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -185,8 +185,9 @@ class Get: a = instances.album_instance.get_all_albums() return [models.Album(a) for a in a] - def get_all_artists(self) -> Set[str]: - tracks = self.get_all_tracks() + @classmethod + def get_all_artists(cls) -> Set[str]: + tracks = cls.get_all_tracks() artists: Set[str] = set() for track in tracks: diff --git a/server/app/lib/searchlib.py b/server/app/lib/searchlib.py index 30744dc8..4f543b15 100644 --- a/server/app/lib/searchlib.py +++ b/server/app/lib/searchlib.py @@ -97,7 +97,13 @@ class SearchAlbums: Gets all albums with a given title. """ - albums = [a.title for a in self.albums] + albums = [] + + for album in self.albums: + title = album.title.lower() + if title not in albums: + albums.append(title) + results = process.extract( self.query, albums, diff --git a/src/components/RightSideBar/Search/AlbumGrid.vue b/src/components/RightSideBar/Search/AlbumGrid.vue index 910fe279..7c590cad 100644 --- a/src/components/RightSideBar/Search/AlbumGrid.vue +++ b/src/components/RightSideBar/Search/AlbumGrid.vue @@ -19,7 +19,7 @@ import useSearchStore from "../../../stores/search"; const search = useSearchStore(); function loadMore() { - search.updateLoadCounter("albums", 6); + search.updateLoadCounter("albums"); search.loadAlbums(search.loadCounter.albums); } diff --git a/src/components/RightSideBar/Search/ArtistGrid.vue b/src/components/RightSideBar/Search/ArtistGrid.vue index 26f6819c..76ecb489 100644 --- a/src/components/RightSideBar/Search/ArtistGrid.vue +++ b/src/components/RightSideBar/Search/ArtistGrid.vue @@ -19,7 +19,7 @@ import useSearchStore from "../../../stores/search"; const search = useSearchStore(); function loadMore() { - search.updateLoadCounter("artists", 6); + search.updateLoadCounter("artists"); search.loadArtists(search.loadCounter.artists); } diff --git a/src/components/RightSideBar/Search/TracksGrid.vue b/src/components/RightSideBar/Search/TracksGrid.vue index 696216d3..3ad045cb 100644 --- a/src/components/RightSideBar/Search/TracksGrid.vue +++ b/src/components/RightSideBar/Search/TracksGrid.vue @@ -26,7 +26,7 @@ const queue = useQStore(); const search = useSearchStore(); function loadMore() { - search.updateLoadCounter("tracks", 5); + search.updateLoadCounter("tracks"); search.loadTracks(search.loadCounter.tracks); } diff --git a/src/composables/loadmore.js b/src/composables/loadmore.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/composables/searchMusic.ts b/src/composables/searchMusic.ts index d18d784b..75bdac4f 100644 --- a/src/composables/searchMusic.ts +++ b/src/composables/searchMusic.ts @@ -43,6 +43,7 @@ async function searchTracks(query: string) { } const data = await res.json(); + console.log(data) return data; } diff --git a/src/stores/search.ts b/src/stores/search.ts index 4bdcb1c2..b36dccd8 100644 --- a/src/stores/search.ts +++ b/src/stores/search.ts @@ -123,16 +123,16 @@ export default defineStore("search", () => { .then(() => scrollOnLoad()); } - function updateLoadCounter(type: string, value: number) { + function updateLoadCounter(type: string) { switch (type) { case "tracks": - loadCounter.tracks += value; + loadCounter.tracks += 6; break; case "albums": - loadCounter.albums += value; + loadCounter.albums += 6; break; case "artists": - loadCounter.artists += value; + loadCounter.artists += 6; break; } } From 600b267ce48803437e863a7a3d686483e6ba271b Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 16 Jun 2022 09:50:07 +0300 Subject: [PATCH 05/56] drop in onStartTyping to focus on search bar automatically --- src/App.vue | 5 +++++ src/components/RightSideBar/SearchInput.vue | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index 91ccae81..97012b67 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,6 +36,7 @@ import useQStore from "@/stores/queue"; import useShortcuts from "@/composables/useKeyboard"; import Logo from "@/components/Logo.vue"; import { useRouter } from "vue-router"; +import { onStartTyping } from "@vueuse/core"; const context_store = useContextStore(); const queue = useQStore(); @@ -53,6 +54,10 @@ app_dom.addEventListener("click", (e) => { useRouter().afterEach(() => { document.getElementById("acontent")?.scrollTo(0, 0); }); + +onStartTyping(() => { + document.getElementById("globalsearch").focus(); +}); diff --git a/src/components/playlists/PlaylistCard.vue b/src/components/playlists/PlaylistCard.vue index 4a677096..cd56149b 100644 --- a/src/components/playlists/PlaylistCard.vue +++ b/src/components/playlists/PlaylistCard.vue @@ -4,9 +4,6 @@ :playlist="props.playlist" class="p-card rounded" > -
-
import { Playlist } from "../../interfaces"; -import PlayBtn from "../shared/PlayBtn.vue"; -import Option from "../shared/Option.vue"; import { paths } from "../../config"; -const imguri = paths.images.playlist - +const imguri = paths.images.playlist; const props = defineProps<{ playlist: Playlist; diff --git a/src/composables/searchMusic.ts b/src/composables/searchMusic.ts index 75bdac4f..12d9ea25 100644 --- a/src/composables/searchMusic.ts +++ b/src/composables/searchMusic.ts @@ -43,7 +43,7 @@ async function searchTracks(query: string) { } const data = await res.json(); - console.log(data) + console.log(data); return data; } @@ -105,3 +105,6 @@ export { loadMoreAlbums, loadMoreArtists, }; + +// TODO: +// Rewrite this module using `useAxios` hook \ No newline at end of file diff --git a/src/stores/search.ts b/src/stores/search.ts index b36dccd8..49344b71 100644 --- a/src/stores/search.ts +++ b/src/stores/search.ts @@ -1,6 +1,6 @@ import { ref, reactive } from "@vue/reactivity"; import { defineStore } from "pinia"; -import { AlbumInfo, Artist, Track } from "../interfaces"; +import { AlbumInfo, Artist, Playlist, Track } from "../interfaces"; import { searchTracks, searchAlbums, @@ -33,6 +33,7 @@ export default defineStore("search", () => { tracks: 0, albums: 0, artists: 0, + playlists: 0, }); const tracks = reactive({ @@ -53,6 +54,12 @@ export default defineStore("search", () => { more: false, }); + const playlists = reactive({ + query: "", + value: [], + more: false, + }); + /** * Searches for tracks, albums and artists * @param newquery query to search for @@ -123,7 +130,9 @@ export default defineStore("search", () => { .then(() => scrollOnLoad()); } - function updateLoadCounter(type: string) { + type loadType = "tracks" | "albums" | "artists" | "playlists"; + + function updateLoadCounter(type: loadType) { switch (type) { case "tracks": loadCounter.tracks += 6; @@ -204,6 +213,7 @@ export default defineStore("search", () => { tracks, albums, artists, + playlists, query, currentTab, loadCounter, diff --git a/todo b/todo index f69cb3cb..83035e5b 100644 --- a/todo +++ b/todo @@ -13,4 +13,25 @@ - [ ] Add settings page (or modal) - [ ] Add keyboard shortcuts listing page (or modal) - [ ] Add backspace shortcut to go back. -- [ ] Implement Esc key to cancel modals. \ No newline at end of file +- [ ] Implement Esc key to cancel modals. + + +### Notes + +- Maybe first process tags and store them to the database, then process albums from these tags. + +Like,this: +1. Tag files +2. Insert all into the database +3. Fetch all albums +4. Fetch all tracks +5. Create prealbums +6. Pop all processed albums +7. Use the following procedure to process single album image: + 7.1. Get a single album track, pop it from memory + 7.2. Try ripping image, + (i). if successful: hurray! we won't have to go further. + (ii). if failed, try getting another track from the same album, try ripping image. + (iii). If failed, repeat (ii) until success, or until you run out of tracks. In that case, set album image to fallback. + + From 3cf44759b56e490ea73aefa7a6e9038877319725 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 19 Jun 2022 14:45:25 +0300 Subject: [PATCH 10/56] process albums seperate from tracks - break populate function into 2 --- server/app/api/album.py | 1 - server/app/db/mongodb/albums.py | 4 +- server/app/functions.py | 12 +-- server/app/helpers.py | 13 +-- server/app/lib/albumslib.py | 78 ++++----------- server/app/lib/populate.py | 163 ++++++++++++-------------------- server/app/lib/taglib.py | 8 +- server/app/lib/trackslib.py | 2 +- server/app/models.py | 36 ++----- server/app/settings.py | 2 +- src/interfaces.ts | 1 + 11 files changed, 100 insertions(+), 220 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index 0b34a24a..11b4ccef 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -40,7 +40,6 @@ def get_albums(): def get_album(): """Returns all the tracks in the given album.""" data = request.get_json() - print(data) album, artist = data["album"], data["artist"] albumhash = helpers.create_album_hash(album, artist) diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index b9e124be..030c900c 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -2,7 +2,6 @@ This file contains the Album class for interacting with album documents in MongoDB. """ -from app import db from app.db.mongodb import convert_many from app.db.mongodb import convert_one from app.db.mongodb import MongoAlbums @@ -26,7 +25,8 @@ class Albums(MongoAlbums): upsert=True, ).upserted_id - def insert_many(self, albums: list): + def insert_many(self, albums: Album): + albums = [a.__dict__ for a in albums] """ Inserts multiple albums into the database. """ diff --git a/server/app/functions.py b/server/app/functions.py index 9c972192..7d5509bf 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -6,16 +6,14 @@ import time from io import BytesIO import requests -from app import api from app import helpers from app import settings from app.lib import watchdoge -from app.lib.populate import Populate +from app.lib.populate import Populate, CreateAlbums from PIL import Image from concurrent.futures import ThreadPoolExecutor from app.lib import trackslib -from app import instances, models @helpers.background @@ -27,7 +25,8 @@ def reindex_tracks(): while True: trackslib.validate_tracks() - populate() + Populate() + CreateAlbums() CheckArtistImages()() time.sleep(60) @@ -41,11 +40,6 @@ def start_watchdog(): watchdoge.watch.run() -def populate(): - pop = Populate() - pop.run() - - class getArtistImage: """ Returns an artist image url. diff --git a/server/app/helpers.py b/server/app/helpers.py index f2b38672..ea11a63b 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -73,17 +73,6 @@ def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]: return tracklist - -# def save_image(url: str, path: str) -> None: -# """ -# Saves an image from an url to a path. -# """ - -# response = requests.get(url) -# img = Image.open(BytesIO(response.content)) -# img.save(path, "JPEG") - - def is_valid_file(filename: str) -> bool: """ Checks if a file is valid. Returns True if it is, False if it isn't. @@ -120,7 +109,7 @@ def create_album_hash(title: str, artist: str) -> str: Creates a simple hash for an album """ lower = (title + artist).replace(" ", "").lower() - hash = lower.join([i for i in lower if i not in '/\\:*?"<>|&']) + hash = "".join([i for i in lower if i not in '/\\:*?"<>|&']) return hash diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index 7030cbff..5c389833 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -34,32 +34,6 @@ def validate() -> None: """ -def find_album(albums: List[models.Album], hash: str) -> int | None: - """ - Finds an album by album title and artist. - - :param `albums`: List of album objects. - :param `hash`: Hash of album. - :return: Index of album in list. - """ - - left = 0 - right = len(albums) - 1 - - while left <= right: - mid = (left + right) // 2 - - if albums[mid].hash == hash: - return mid - - if albums[mid].hash < hash: - left = mid + 1 - else: - right = mid - 1 - - return None - - def get_album_duration(album: List[models.Track]) -> int: """ Gets the duration of an album. @@ -81,31 +55,19 @@ def use_defaults() -> str: return path -def gen_random_path() -> str: - """ - Generates a random image file path for an album image. - """ - choices = "abcdefghijklmnopqrstuvwxyz0123456789" - path = "".join(random.choice(choices) for i in range(20)) - path += ".webp" - - return path - - -def get_album_image(album: list) -> str: +def get_album_image(track: models.Track) -> str: """ Gets the image of an album. """ - for track in album: - img_p = gen_random_path() + img_p = track.albumhash + ".webp" - exists = taglib.extract_thumb(track["filepath"], webp_path=img_p) + success = taglib.extract_thumb(track.filepath, webp_path=img_p) - if exists: - return img_p + if success: + return img_p - return use_defaults() + return None class GetAlbumTracks: @@ -122,14 +84,6 @@ class GetAlbumTracks: def __call__(self): tracks = helpers.UseBisection(self.tracks, "albumhash", [self.hash])() - pprint(tracks) - - # while index is not None: - # track = self.tracks[index] - # tracks.append(track) - # self.tracks.remove(track) - # index = helpers.UseBisection(self.tracks, "albumhash", [self.hash])() - return tracks @@ -137,23 +91,23 @@ def get_album_tracks(tracklist: List[models.Track], hash: str) -> List: return GetAlbumTracks(tracklist, hash)() -def create_album(track: dict, tracklist: list[models.Track]) -> dict: +def create_album(track: models.Track) -> dict: """ Generates and returns an album object from a track object. """ album = { - "title": track["album"], - "artist": track["albumartist"], + "title": track.album, + "artist": track.albumartist, + "hash": track.albumhash, } - albumhash = helpers.create_album_hash(album["title"], album["artist"]) - album_tracks = get_album_tracks(tracklist, albumhash) - if len(album_tracks) == 0: - return None + album["date"] = track.date - album["date"] = album_tracks[0]["date"] + img_p = get_album_image(track) - album["image"] = get_album_image(album_tracks) - # album["image"] = "".join(x for x in albumhash if x not in "\/:*?<>|") + if img_p is not None: + album["image"] = img_p + return album + album["image"] = None return album diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index d1bd63e2..5ca7d31c 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -1,18 +1,17 @@ +from dataclasses import dataclass +from pprint import pprint import time from concurrent.futures import ThreadPoolExecutor from typing import List from app import settings -from app.helpers import create_album_hash +from app.helpers import Get, UseBisection, create_album_hash from app.helpers import run_fast_scandir -from app.instances import album_instance from app.instances import tracks_instance from app.lib.albumslib import create_album -from app.lib.albumslib import find_album from app.lib.taglib import get_tags -from app.lib.trackslib import find_track from app.logger import Log -from app.models import Album +from app.models import Album, Track from tqdm import tqdm from app import instances @@ -28,36 +27,15 @@ class Populate: """ def __init__(self) -> None: - self.files = [] self.db_tracks = [] self.tagged_tracks = [] - self.folders = set() - self.pre_albums = [] - self.albums: List[Album] = [] self.files = run_fast_scandir(settings.HOME_DIR, full=True)[1] self.db_tracks = tracks_instance.get_all_tracks() - self.tag_count = 0 - self.exist_count = 0 - self.tracks = [] - def run(self): self.check_untagged() self.tag_untagged() - if len(self.tagged_tracks) == 0: - return - - # self.tagged_tracks.sort(key=lambda x: x["albumhash"]) - - self.pre_albums = self.create_pre_albums(self.tagged_tracks) - self.create_albums(self.pre_albums) - - self.albums.sort(key=lambda x: x.hash) - self.create_tracks() - - self.save_all() - def check_untagged(self): """ Loops through all the tracks in db tracks removing each @@ -88,97 +66,80 @@ class Populate: with ThreadPoolExecutor() as executor: executor.map(self.get_tags, self.files) - tracks_instance.insert_many(self.tagged_tracks) + if len(self.tagged_tracks) > 0: + tracks_instance.insert_many(self.tagged_tracks) + d = time.time() - s Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds") + +@dataclass +class PreAlbum: + title: str + artist: str + hash: str + + +class CreateAlbums: + def __init__(self) -> None: + self.db_tracks = Get.get_all_tracks() + self.db_albums = Get.get_all_albums() + + prealbums = self.create_pre_albums(self.db_tracks) + prealbums = self.filter_processed(self.db_albums, prealbums) + print(f"📌 {len(prealbums)}") + + albums = [] + for album in tqdm(prealbums, desc="Creating albums"): + a = self.create_album(album) + albums.append(a) + + if len(albums) > 0: + instances.album_instance.insert_many(albums) + @staticmethod - def create_pre_albums(tracks: List[dict]): - """ - Creates pre-albums for the all tagged tracks. - """ + def create_pre_albums(tracks: List[Track]) -> List[PreAlbum]: prealbums = [] - for track in tqdm(tracks, desc="Creating pre-albums"): - album = {"title": track["album"], "artist": track["albumartist"]} + for track in tqdm(tracks, desc="Creating prealbums"): + album = { + "title": track.album, + "artist": track.albumartist, + "hash": track.albumhash, + } + + album = PreAlbum(**album) if album not in prealbums: prealbums.append(album) - Log(f"Created {len(prealbums)} pre-albums") return prealbums - def create_album(self, album: dict): - albumhash = create_album_hash(album["title"], album["artist"]) - album = instances.album_instance.find_album_by_hash(albumhash) + @staticmethod + def filter_processed(albums: List[Album], prealbums: List[PreAlbum]) -> List[dict]: + to_process = [] - if album is not None: - self.albums.append(album) - self.exist_count += 1 - return + for p in tqdm(prealbums, desc="Filtering processed albums"): + album = UseBisection(albums, "hash", [p.hash])()[0] - index = find_track(self.tagged_tracks, albumhash) + if album is None: + to_process.append(p) - track = self.tagged_tracks[index] + return to_process - album = create_album(track, self.tagged_tracks) + def create_album(self, album: PreAlbum) -> Album: + hash = album.hash - if album is None: - print("album is none") - return + album = {"image": None} + + while album["image"] is None: + track = UseBisection(self.db_tracks, "albumhash", [hash])()[0] + + if track is not None: + album = create_album(track) + self.db_tracks.remove(track) + else: + album["image"] = hash album = Album(album) - - self.albums.append(album) - - def create_albums(self, albums: List[dict]): - """ - Uses the pre-albums to create new albums and add them to the database. - """ - for album in tqdm(albums, desc="Building albums"): - self.create_album(album) - - Log(f"{self.exist_count} of {len(albums)} albums were already in the database") - - def create_track(self, track: dict): - """ - Creates a single track object. - """ - - albumhash = track["albumhash"] - index = find_album(self.albums, albumhash) - - if index is None: - return - - try: - album: Album = self.albums[index] - except (TypeError): - """ - 😭😭😭 - """ - pass - - track["image"] = album.image - return track - - def create_tracks(self): - """ - Loops through all the tagged tracks creating complete track objects using the `models.Track` model. - """ - with ThreadPoolExecutor() as executor: - iterable = executor.map(self.create_track, self.tagged_tracks) - - self.tracks = [t for t in iterable if t is not None] - - Log( - f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums" - ) - - def save_all(self): - """ - Saves the albums to the database. - """ - - album_instance.insert_many([a.__dict__ for a in self.albums]) - tracks_instance.insert_many(self.tracks) + return album diff --git a/server/app/lib/taglib.py b/server/app/lib/taglib.py index 0106b2c3..1292feba 100644 --- a/server/app/lib/taglib.py +++ b/server/app/lib/taglib.py @@ -135,8 +135,8 @@ def parse_track_number(tags): Parses the track number from an audio file. """ try: - track_number = tags["tracknumber"][0] - except (KeyError, IndexError): + track_number = int(tags["tracknumber"][0]) + except (KeyError, IndexError, ValueError): track_number = 1 return track_number @@ -147,8 +147,8 @@ def parse_disk_number(tags): Parses the disk number from an audio file. """ try: - disk_number = tags["disknumber"][0] - except (KeyError, IndexError): + disk_number = int(tags["disknumber"][0]) + except (KeyError, IndexError, ValueError): disk_number = 1 return disk_number diff --git a/server/app/lib/trackslib.py b/server/app/lib/trackslib.py index 3c73af39..34d037e2 100644 --- a/server/app/lib/trackslib.py +++ b/server/app/lib/trackslib.py @@ -44,7 +44,7 @@ def get_track_by_id(trackid: str) -> models.Track: print("AttributeError") -def find_track(tracks: list, hash: str) -> int or None: +def find_track(tracks: list, hash: str) -> int | None: """ Finds an album by album title and artist. """ diff --git a/server/app/models.py b/server/app/models.py index 2bc11fe6..1c461294 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -6,7 +6,6 @@ import random from typing import List from app import helpers -from app.exceptions import TrackExistsInPlaylist @dataclass(slots=True) @@ -25,20 +24,14 @@ class Track: length: int genre: str bitrate: int - image: str tracknumber: int disknumber: int albumhash: str + date: str + image: str def __init__(self, tags): - try: - self.trackid = tags["_id"]["$oid"] - except KeyError: - self.trackid = "".join( - random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(20) - ) - print("No id") - + self.trackid = tags["_id"]["$oid"] self.title = tags["title"] self.artists = tags["artists"].split(", ") self.albumartist = tags["albumartist"] @@ -50,16 +43,9 @@ class Track: self.length = int(tags["length"]) self.disknumber = int(tags["disknumber"]) self.albumhash = tags["albumhash"] - - try: - self.image = tags["image"] - except KeyError: - print(tags) - - try: - self.tracknumber = int(tags["tracknumber"]) - except ValueError: - self.tracknumber = 1 + self.date = tags["date"] + self.image = tags["albumhash"] + ".webp" + self.tracknumber = int(tags["tracknumber"]) @dataclass(slots=True) @@ -81,14 +67,14 @@ class Artist: @dataclass class Album: """ - Album class + Creates an album object """ title: str artist: str + hash: str date: int image: str - hash: str count: int = 0 duration: int = 0 is_soundtrack: bool = False @@ -100,11 +86,7 @@ class Album: self.artist = tags["artist"] self.date = tags["date"] self.image = tags["image"] - - try: - self.hash = tags["albumhash"] - except KeyError: - self.hash = helpers.create_album_hash(self.title, self.artist) + self.hash = tags["hash"] @property def is_soundtrack(self) -> bool: diff --git a/server/app/settings.py b/server/app/settings.py index fb840a26..9cce056e 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -12,7 +12,7 @@ HOME_DIR = os.path.expanduser("~") APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER) THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails") TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio" -HOME_DIR = TEST_DIR +# HOME_DIR = TEST_DIR # URL IMG_BASE_URI = "http://127.0.0.1:8900/images/" IMG_ARTIST_URI = IMG_BASE_URI + "artists/" diff --git a/src/interfaces.ts b/src/interfaces.ts index 78b99e44..03b96aa5 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -6,6 +6,7 @@ export interface Track { album?: string; artists: string[]; albumartist?: string; + albumhash?: string; folder?: string; filepath?: string; length?: number; From 12c8406a0d459dc6500d1ab5f78834bbf94eedf2 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 20 Jun 2022 09:49:16 +0300 Subject: [PATCH 11/56] add ping check - fix artist downloader function --- server/app/functions.py | 12 +++++++----- server/app/helpers.py | 19 ++++++++++++++++--- server/app/lib/folderslib.py | 4 ++-- server/app/lib/populate.py | 26 ++++++++++++++++++++++---- server/assets/default.webp | Bin 0 -> 4082 bytes server/start.sh | 2 +- 6 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 server/assets/default.webp diff --git a/server/app/functions.py b/server/app/functions.py index 7d5509bf..561760be 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -27,7 +27,9 @@ def reindex_tracks(): Populate() CreateAlbums() - CheckArtistImages()() + + if helpers.Ping()(): + CheckArtistImages()() time.sleep(60) @@ -73,7 +75,6 @@ class useImageDownloader: img.save(self.dest, format="webp") img.close() except requests.exceptions.ConnectionError: - print("🔴🔴🔴🔴🔴🔴🔴") time.sleep(5) @@ -102,7 +103,7 @@ class CheckArtistImages: """ img_path = ( - helpers.app_dir + settings.APP_DIR + "/images/artists/" + helpers.create_safe_name(artistname) + ".webp" @@ -115,14 +116,15 @@ class CheckArtistImages: if url is None: return - useImageDownloader(url, img_path)() def __call__(self): self.artists = helpers.Get.get_all_artists() with ThreadPoolExecutor() as pool: - pool.map(self.download_image, self.artists) + iter = pool.map(self.download_image, self.artists) + for i in iter: + pass print("Done fetching images") diff --git a/server/app/helpers.py b/server/app/helpers.py index ea11a63b..102e2b49 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -8,12 +8,12 @@ from datetime import datetime from typing import Dict, Set from typing import List +import requests + from app import models from app import settings from app import instances -app_dir = settings.APP_DIR - def background(func): """ @@ -73,6 +73,7 @@ def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]: return tracklist + def is_valid_file(filename: str) -> bool: """ Checks if a file is valid. Returns True if it is, False if it isn't. @@ -97,7 +98,7 @@ def check_artist_image(image: str) -> str: Checks if the artist image is valid. """ img_name = image.replace("/", "::") + ".webp" - + app_dir = settings.APP_DIR if not os.path.exists(os.path.join(app_dir, "images", "artists", img_name)): return use_memoji() else: @@ -194,3 +195,15 @@ class Get: """ p = instances.playlist_instance.get_all_playlists() return [models.Playlist(p) for p in p] + + +class Ping: + """Checks if there is a connection to the internet by pinging google.com""" + + @staticmethod + def __call__() -> bool: + try: + requests.get("https://google.com") + return True + except requests.exceptions.ConnectionError: + return False diff --git a/server/app/lib/folderslib.py b/server/app/lib/folderslib.py index 81217298..b5872736 100644 --- a/server/app/lib/folderslib.py +++ b/server/app/lib/folderslib.py @@ -14,11 +14,11 @@ class Dir: is_sym: bool -def get_folder_track_count(foldername: str) -> int: +def get_folder_track_count(path: str) -> int: """ Returns the number of files associated with a folder. """ - tracks = instances.tracks_instance.find_tracks_inside_path_regex(foldername) + tracks = instances.tracks_instance.find_tracks_inside_path_regex(path) return len(tracks) diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 5ca7d31c..6fa2aead 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -89,10 +89,23 @@ class CreateAlbums: prealbums = self.filter_processed(self.db_albums, prealbums) print(f"📌 {len(prealbums)}") + s = time.time() albums = [] + for album in tqdm(prealbums, desc="Creating albums"): a = self.create_album(album) - albums.append(a) + if a is not None: + albums.append(a) + + # with ThreadPoolExecutor() as pool: + # iterator = pool.map(self.create_album, prealbums) + + # for i in iterator: + # if i is not None: + # albums.append(i) + + d = time.time() - s + Log(f"Created {len(albums)} albums in {d} seconds") if len(albums) > 0: instances.album_instance.insert_many(albums) @@ -131,15 +144,20 @@ class CreateAlbums: hash = album.hash album = {"image": None} + iter = 0 while album["image"] is None: track = UseBisection(self.db_tracks, "albumhash", [hash])()[0] if track is not None: + iter += 1 album = create_album(track) self.db_tracks.remove(track) else: album["image"] = hash - - album = Album(album) - return album + try: + album = Album(album) + return album + except KeyError: + print(f"📌 {iter}") + print(album) diff --git a/server/assets/default.webp b/server/assets/default.webp new file mode 100644 index 0000000000000000000000000000000000000000..d9795fa92300799db572c92fce72a617903e8cf7 GIT binary patch literal 4082 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 k0jfX3pc<)LUMO7kFOa;^B Date: Mon, 20 Jun 2022 13:00:59 +0300 Subject: [PATCH 12/56] provide fallback image return fallback image if image is not found @imgserver --- server/app/imgserver/__init__.py | 29 +++++++++++++++++---------- server/app/prep.py | 34 ++++++++++++++++++++++++++++++++ server/app/settings.py | 5 +++-- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/server/app/imgserver/__init__.py b/server/app/imgserver/__init__.py index cc5057b6..04a3b59e 100644 --- a/server/app/imgserver/__init__.py +++ b/server/app/imgserver/__init__.py @@ -12,11 +12,13 @@ def join(*args: Tuple[str]) -> str: HOME = path.expanduser("~") -ROOT_PATH = path.join(HOME, ".alice", "images") +APP_DIR = join(HOME, ".alice") +IMG_PATH = path.join(APP_DIR, "images") -THUMB_PATH = join(ROOT_PATH, "thumbnails") -ARTIST_PATH = join(ROOT_PATH, "artists") -PLAYLIST_PATH = join(ROOT_PATH, "playlists") +ASSETS_PATH = join(APP_DIR, "assets") +THUMB_PATH = join(IMG_PATH, "thumbnails") +ARTIST_PATH = join(IMG_PATH, "artists") +PLAYLIST_PATH = join(IMG_PATH, "playlists") @app.route("/") @@ -24,6 +26,16 @@ def hello(): return "Hello mf" +def send_fallback_img(): + img = join(ASSETS_PATH, "default.webp") + exists = path.exists(img) + + if not exists: + return "", 404 + + return send_from_directory(ASSETS_PATH, "default.webp") + + @app.route("/t/") def send_thumbnail(imgpath: str): fpath = join(THUMB_PATH, imgpath) @@ -32,7 +44,7 @@ def send_thumbnail(imgpath: str): if exists: return send_from_directory(THUMB_PATH, imgpath) - return {"msg": "Not found"}, 404 + return send_fallback_img() @app.route("/a/") @@ -43,7 +55,7 @@ def send_artist_image(imgpath: str): if exists: return send_from_directory(ARTIST_PATH, imgpath) - return {"msg": "Not found"}, 404 + return send_fallback_img() @app.route("/p/") @@ -54,11 +66,8 @@ def send_playlist_image(imgpath: str): if exists: return send_from_directory(PLAYLIST_PATH, imgpath) - return {"msg": "Not found"}, 404 + return send_fallback_img() -# TODO -# Return Fallback images instead of JSON - if __name__ == "__main__": app.run(threaded=True, port=9877) diff --git a/server/app/prep.py b/server/app/prep.py index 55831a9b..0335c371 100644 --- a/server/app/prep.py +++ b/server/app/prep.py @@ -2,10 +2,42 @@ Contains the functions to prepare the server for use. """ import os +import shutil from app import settings +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, + } + ] + + for entry in files: + src = os.path.join(os.getcwd(), entry["src"]) + print(f"Copying {src} to {entry['dest']}") + + if entry["is_dir"]: + shutil.copytree( + src, + entry["dest"], + ignore=shutil.ignore_patterns( + "*.pyc", + ), + copy_function=shutil.copy2, + dirs_exist_ok=True, + ) + break + + shutil.copy2(src, entry["dest"]) + + def create_config_dir() -> None: """ Creates the config directory if it doesn't exist. @@ -29,3 +61,5 @@ def create_config_dir() -> None: if not exists: os.makedirs(path) os.chmod(path, 0o755) + + CopyFiles() diff --git a/server/app/settings.py b/server/app/settings.py index 9cce056e..de2e0450 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -10,7 +10,9 @@ import multiprocessing CONFIG_FOLDER = ".alice" HOME_DIR = os.path.expanduser("~") APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER) -THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails") +IMG_PATH = os.path.join(APP_DIR, "images") + +THUMBS_PATH = os.path.join(IMG_PATH, "thumbnails") TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio" # HOME_DIR = TEST_DIR # URL @@ -42,4 +44,3 @@ CPU_COUNT = multiprocessing.cpu_count() class logger: enable = True - From 61f9af86aa2aee7cd94e62ad499d1aaf986910c5 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 20 Jun 2022 13:16:21 +0300 Subject: [PATCH 13/56] remove default images and memoji implementation --- server/app/api/album.py | 2 +- server/app/helpers.py | 20 ------------------ server/setup/default-images/thumbnails/0.webp | Bin 5008 -> 0 bytes server/setup/default-images/thumbnails/1.webp | Bin 22818 -> 0 bytes .../setup/default-images/thumbnails/10.webp | Bin 4830 -> 0 bytes .../setup/default-images/thumbnails/11.webp | Bin 7332 -> 0 bytes .../setup/default-images/thumbnails/12.webp | Bin 8244 -> 0 bytes .../setup/default-images/thumbnails/13.webp | Bin 17308 -> 0 bytes .../setup/default-images/thumbnails/14.webp | Bin 8094 -> 0 bytes .../setup/default-images/thumbnails/15.webp | Bin 14840 -> 0 bytes .../setup/default-images/thumbnails/16.webp | Bin 11536 -> 0 bytes .../setup/default-images/thumbnails/17.webp | Bin 8710 -> 0 bytes .../setup/default-images/thumbnails/18.webp | Bin 5970 -> 0 bytes .../setup/default-images/thumbnails/19.webp | Bin 4740 -> 0 bytes server/setup/default-images/thumbnails/2.webp | Bin 16416 -> 0 bytes .../setup/default-images/thumbnails/20.webp | Bin 16906 -> 0 bytes server/setup/default-images/thumbnails/3.webp | Bin 3928 -> 0 bytes server/setup/default-images/thumbnails/4.webp | Bin 12588 -> 0 bytes server/setup/default-images/thumbnails/5.webp | Bin 10942 -> 0 bytes server/setup/default-images/thumbnails/6.webp | Bin 12006 -> 0 bytes server/setup/default-images/thumbnails/7.webp | Bin 22680 -> 0 bytes server/setup/default-images/thumbnails/8.webp | Bin 5782 -> 0 bytes server/setup/default-images/thumbnails/9.webp | Bin 25978 -> 0 bytes 23 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 server/setup/default-images/thumbnails/0.webp delete mode 100644 server/setup/default-images/thumbnails/1.webp delete mode 100644 server/setup/default-images/thumbnails/10.webp delete mode 100644 server/setup/default-images/thumbnails/11.webp delete mode 100644 server/setup/default-images/thumbnails/12.webp delete mode 100644 server/setup/default-images/thumbnails/13.webp delete mode 100644 server/setup/default-images/thumbnails/14.webp delete mode 100644 server/setup/default-images/thumbnails/15.webp delete mode 100644 server/setup/default-images/thumbnails/16.webp delete mode 100644 server/setup/default-images/thumbnails/17.webp delete mode 100644 server/setup/default-images/thumbnails/18.webp delete mode 100644 server/setup/default-images/thumbnails/19.webp delete mode 100644 server/setup/default-images/thumbnails/2.webp delete mode 100644 server/setup/default-images/thumbnails/20.webp delete mode 100644 server/setup/default-images/thumbnails/3.webp delete mode 100644 server/setup/default-images/thumbnails/4.webp delete mode 100644 server/setup/default-images/thumbnails/5.webp delete mode 100644 server/setup/default-images/thumbnails/6.webp delete mode 100644 server/setup/default-images/thumbnails/7.webp delete mode 100644 server/setup/default-images/thumbnails/8.webp delete mode 100644 server/setup/default-images/thumbnails/9.webp diff --git a/server/app/api/album.py b/server/app/api/album.py index 11b4ccef..3b4347d1 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -103,7 +103,7 @@ def get_albumartists(): for artist in artists: artist_obj = { "name": artist, - "image": helpers.check_artist_image(helpers.create_safe_name(artist)), + "image": helpers.create_safe_name(artist) + ".webp", } final_artists.append(artist_obj) diff --git a/server/app/helpers.py b/server/app/helpers.py index 102e2b49..b38dca54 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -85,26 +85,6 @@ def is_valid_file(filename: str) -> bool: return False -def use_memoji(): - """ - Returns a path to a random memoji image. - """ - path = str(random.randint(0, 20)) + ".svg" - return "defaults/" + path - - -def check_artist_image(image: str) -> str: - """ - Checks if the artist image is valid. - """ - img_name = image.replace("/", "::") + ".webp" - app_dir = settings.APP_DIR - if not os.path.exists(os.path.join(app_dir, "images", "artists", img_name)): - return use_memoji() - else: - return img_name - - def create_album_hash(title: str, artist: str) -> str: """ Creates a simple hash for an album diff --git a/server/setup/default-images/thumbnails/0.webp b/server/setup/default-images/thumbnails/0.webp deleted file mode 100644 index c0cc614a732f82ec4b195426f3151c67314a751d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5008 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 a9}V@xyL4{RZzxMIOeMq;ucQfz&;S5<$F%7H diff --git a/server/setup/default-images/thumbnails/1.webp b/server/setup/default-images/thumbnails/1.webp deleted file mode 100644 index e2d20e3a27fc047a26912e18870c74448d057843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22818 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 K2SP(?_y7P$nNf5A diff --git a/server/setup/default-images/thumbnails/12.webp b/server/setup/default-images/thumbnails/12.webp deleted file mode 100644 index 340b5fd75330da7a448d7e38126f40529e898d19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8244 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 diff --git a/server/setup/default-images/thumbnails/13.webp b/server/setup/default-images/thumbnails/13.webp deleted file mode 100644 index 4bebe508891cf65802624f566fffd8347f02cd6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17308 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 HIsgCw^S3!; diff --git a/server/setup/default-images/thumbnails/14.webp b/server/setup/default-images/thumbnails/14.webp deleted file mode 100644 index 4f348c24c4d570c4ede6d24dffc3eb779eab9503..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8094 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 diff --git a/server/setup/default-images/thumbnails/15.webp b/server/setup/default-images/thumbnails/15.webp deleted file mode 100644 index 64f6df159cd0c63a6c0cb7e90a78809f00049322..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14840 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 diff --git a/server/setup/default-images/thumbnails/16.webp b/server/setup/default-images/thumbnails/16.webp deleted file mode 100644 index 15c1797f65f240f65023513bdf6d2b5242d9a626..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11536 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 Csmc`q diff --git a/server/setup/default-images/thumbnails/17.webp b/server/setup/default-images/thumbnails/17.webp deleted file mode 100644 index d515661c1c36330057e2c0c6655a565f8e35bad8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8710 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 diff --git a/server/setup/default-images/thumbnails/18.webp b/server/setup/default-images/thumbnails/18.webp deleted file mode 100644 index 0ef1c0324eb143406b64b48cd23258f39abf18c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5970 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 Aga7~l diff --git a/server/setup/default-images/thumbnails/19.webp b/server/setup/default-images/thumbnails/19.webp deleted file mode 100644 index a3edbdd904df70b38dc51971ee28ecea6c96c7bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4740 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 S2y5fb2pSCMSkk1l0001mk{8PW diff --git a/server/setup/default-images/thumbnails/2.webp b/server/setup/default-images/thumbnails/2.webp deleted file mode 100644 index 94e9e51cd127c1a719e1489f9a9588a6a77b4197..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16416 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 diff --git a/server/setup/default-images/thumbnails/3.webp b/server/setup/default-images/thumbnails/3.webp deleted file mode 100644 index 567b7828018bcfa590a231900d8a40b555186a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3928 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 diff --git a/server/setup/default-images/thumbnails/4.webp b/server/setup/default-images/thumbnails/4.webp deleted file mode 100644 index daeee3251fd3005be3bed970c94d81b77c77fd07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12588 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 OrdQ*5sG~9*`v3q*Z;u!N diff --git a/server/setup/default-images/thumbnails/5.webp b/server/setup/default-images/thumbnails/5.webp deleted file mode 100644 index 36bb172c2528d6d2357a6e2c223b965d60eea02b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10942 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 z zYkPX9c@8FbI0RY6{MI{Qe&mR{O%0lV6&ukW@; zJZ-Xc)Tvh`>j9hoRaI|b=7BfC42qLegaNvw)|{6^yJY_L;p#06FPBwe^ik8*dH-?${o=MfuzE!N|zjP0}HsP@;EX*Q?o1&fxCr$y*q88UItppX0=Kt z0_Fb+It)?VK&lVb)$_BVpk7V)6kohHblx}3A4R`C*rebIzXWkP=!t-&<>FUhsu_jJ zt7R3^RQC@=Z+Y%wUyjuWT2pSxhst>mU*VDe^w~{u9UwqaX*;CKSQm;Z50$8ownk#9 zWJ(M>xPyLjFAh`$lf6fw5E9h2tQV3*!DS%s_FCOv&1)drUUaVLnrmra^TmKDls2Ld z6RN>h5Jc~e1d06AfetnE>o^_(#fA4rVlyFlfz<+kbmzRwkoQMEAd1EiPO(Y%8{Ml81!bvf3qH6ist({YcU*&iL&(r!NxEJyL#}SYz;q1_twUa;qj7T=-4fb&`ZG$@!6w=MfIKy)G;v@3{ zy|vlAicnB839$9)WINq_SycH42#NenP5@T-%nG(GIzc!#=?e&SVAU~@F|Keeg5{lF z(?jaBK1(H(t!$MXb(Z?a6`xE|MQ#EdTnshrPVaCsnTpjH8(*Y0Cm`kgzy|$0H*Dg{ z+Gkv(O}5={j^8VwJU1C1M?kAjoHTLOqUIE(uT6;Et(E(N2*mL%1%TRsW6I+$^Dcf; z^9`a=N5@~TCJwdn6~cA3_Zmm<`jNtfzLoi8o ze+6S}-bd)x;hPjwC(JD1Nepprdnksrn#V4vW1z717gsj7lQuYit zs+lYEjk%KxSh$rjr$N?^wT_`R+0v(cTd(9eFmqUB5>LPi)!2%kBsfZhywd8nc+eOS zx%6fl9AEpE&fq730?|5zN;E6}IEOQ6ybA@Ft9u>qc3UFfDlI>%8gEY%R;x!Q zl1IUHdO1qP`mY-U()!lToQepKJ5ke$#GPMb9zS~jxgYbnDLC=(XadItvn~GAu2!=A z=Wn!rfwkt<^9Ry6=7yJ_&G=Zlr5>pSHN4Ng#Mww|kSsRKqpH4C7EPR9{#`N@$-uF( z&&@^eu~*!+5pu+>uV=rw4k`r_bSXRhT z`zP&^)7IqwR7ST2i}(*f>fFw? zmg4|)07QwVU1(vlpLLwttFm(VtrW0{zaPHnmGxw1w%P$?KIP%PfHowir0Z=u>@(zx z4N3wRcmW=gs`pLS)Z!!2XZn2n2p3j@%XJ{PL`pKGQ$qQfN$Kv(QI>nQi0Vtxu!oXoBMimN zHagJkc+|o0$SOYoHN+{t3b_CrZ#Pn>(t2WYCGcE%b=epm2iJ@bS92sHD-IEd2#l{@1 z--;gW4mq6!q1h4Q10?!U>Az;qK9MZy@EkN$m6#Kb9+2_U&Pe>_J`mrNDS%+;imZh{ zyDaU#nl&du4+`;OYir0DP4+*g$n!u80H%2!F+zlDAO&^_`iM#;yM!PC|!nuU) zW)9$!by}$Wu{$IUB(302Fp7=Q0Cl0F2AlGaUv?CkqWg$59;I1m;j~nG9)WKa2!U~> zn2F8CwXSWk--u&y{Q{SmM3D$|$OE}*Sc32D)!^ zkk~ktcQck&mNbt>!v304Xowm@3``CwAH?6&D;1oU_I22lWX**aN%a6Fn4y5?p-fZQ z3m6mtv1^hVfNXF}%LK_9>zZZiK?L>nFv1FxA9crxQ8$hwkI%l7%i$mUwVifSaWx<> z_0h|Q4MXFw$NpltCI^TxSILHB040Sy@OuKmsjVg=cE11LP~c^c>lX8|JRM!QEPiYVIn3?K zvCbw)X@KS}e6xFOz?Aeq;wiD#Cqi5_efpQFuA&`<)`5p7+g)IuBxgA-jk!yVjSsMx zVj=DMpD22_$e?8KcN)D?5OyRAMOyma?B;U}X7E#2$aT@$qjs&CMnQ1|XDMC~!gL9D zNJ39uq30!piX)9i^CpvOvJ6w6lVt2A9mn_h zTf?wguL{xNA0;`U*Zn!`vxnxa6_9mR&cq)GpYgz)`dTgE>b^`voWAb&W7ITit=3p$ zW?&>zXuON(+U?710X@sEFE+Zw{DdZyhM{tR!QlBb9ne-@*zL=Tl^pQA^6sk;GpR%c zQh?9ht^4B|I~HAP_bB*A#vW2AQXU|E;P^95y^C-iY0zv?zsCjF=k^XN_!d|HrHTi8 zqu1WVK=SlyZgrji$0^CMUMf$|>@mmCupNQz+oAZ9D*x zR@vySo~4Ie3+w@qNne+vHrgU+>24_4N2weLKt@@#UNQWtjP?}m*y{PWT5pr2}uj^*9NA!8K{u zI9atFqkxLl)}{wor6$=FHyVE#U!Jdf*S=iU=ttn#Lkn+*qb^-d$aijHI9+pZ%7@*p zU;?d3$322)G~b=>_kEKh`}dH0ajYfI>D7_^$za9>Y|Lq`15mq)x#v1&`)+nugSqG4 zEt&Nz?`;v~k#)WU{TJTsQE4gA=>ay+DSVSJzxR?MiOIEeE_aZEKuGN(Y`Zhy(%D)_ zvUFl;!^iaNDzMaS)ERs%WSKutt6z8SJ#J0nf`)f9PRjB_DnDU;8hxTTEH1G!)}gbl z>*@j-0M4fjg5Y>)=*e(zqKK3gwQcm5q2)A#j2rQn!lg8NIaJGDpjofOv_?QLlD~vb z-q{WiY8z4tRzRe0|2h@(_{2*0E7$a+bNA2tvadMHc_Z2}%T%BR`-vac3!vmJBYq#Y zM66`ax(3N$X%R$l^Pi6O_b(S>Uve*U+Gjgt7d z$&Gv)&MRlhkL{u5@@L6>zHIsR&C8}(H0{vHDCY#PT@A(;_%J`pG(>FjTp4OKc_-NS z5AFI8zw9#iM&Gp?+L03XZPY4cg%JF!duin`Io$_{faO|ETJof9iO)eg06i?uoUER} zc|ix3XJ;-cT8TDsgsX#+h)>GUmQpn4ypV0i1&*eqDZR|a$=Oq5jl~Kt8rG+%6!en0 zgf&t{NAt{#d+S%L-mJ7Scu!_JFkV{2cg4uM^>X%rR;M>>>+y@Z_x8ycx+phVm#sdY z1KX4=H?iiyr*^o86;8xXRbZPPlwL=81PE6P$PJ~yMR@}*_Pgt_J28v|&+aOQ z!^Yh_v-k7nhXgNXZa#3NFbBEP77HjRKwM1*01GFss}Rd-0IqF)#oA{u`|my zR$L@js50r+1|+D*WH1{O8(g7QDnh_sP|175-TIs2^qTG64uk^^)ZgD^F^=BAguMYu zgY9w@g=esh=3i=e8}d6C6YeMkY>%Q9hzRHp%FWDSn6 zw~-?|VE5{-^6;P9{RNLb2&eT&oj9$S=ji~?IWM&$9sZ6y;;8$eJ%Qh5F4IA{%4TGQ zqxaM!coBN@Y>Dc~zl9L^UO`dOg)@XbwXl#w4am$Mx`NhoqxQg!mq)XImtXQD3v?RR z;Y73-Z#QWiBlFQ8=m=!Pzt6dThvy_F9~bMYb6cwkI-qffQ0Q2B$5CabHSsKZ;t)x9 z!q(z)Q~q)Oe^fpZ1;tG?V?V?#X@GVReGyg7r4g6hSRl(+PM<-Mbv^X?UK(1?n8V}3 zJeyBdX%v_^Qz+K3|4h|@ZPW@4ILjfhcWnA%#y8j|Xp%|xZ&V5I)h+ZWS1JIGGjIR^ E01)Dd%m4rY diff --git a/server/setup/default-images/thumbnails/7.webp b/server/setup/default-images/thumbnails/7.webp deleted file mode 100644 index 8ffe570eec6b948f776cedc8ca299568b0c06bff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22680 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%0DQ)Bhlbjk&NtghydWp` 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 diff --git a/server/setup/default-images/thumbnails/8.webp b/server/setup/default-images/thumbnails/8.webp deleted file mode 100644 index 19b66c5b1663a7e1928b7e553e00c9ff8a5b4a71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5782 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 UaMSj5$-tli9aP_BP80wD02An#nE(I) diff --git a/server/setup/default-images/thumbnails/9.webp b/server/setup/default-images/thumbnails/9.webp deleted file mode 100644 index 363a9ed327631057471d3252d9e95e076ea8233c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25978 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 From 2b5e9c02bb04a098c1b575fc08b35aef5c7c3f0a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 21 Jun 2022 08:46:43 +0300 Subject: [PATCH 14/56] add timeout to Ping --- server/app/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/helpers.py b/server/app/helpers.py index b38dca54..a3c5a520 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -183,7 +183,7 @@ class Ping: @staticmethod def __call__() -> bool: try: - requests.get("https://google.com") + requests.get("https://google.com", timeout=10) return True - except requests.exceptions.ConnectionError: + except (requests.exceptions.ConnectionError, requests.Timeout): return False From 3cd0527962da296018e926c6cb165bbe648cfa76 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 21 Jun 2022 16:24:27 +0300 Subject: [PATCH 15/56] use count_documents to get folder count - map filenames with db data --- server/app/db/mongodb/tracks.py | 12 +++++++++--- server/app/functions.py | 14 ++++++-------- server/app/lib/folderslib.py | 17 +++++++++++++---- server/app/lib/populate.py | 1 - server/app/prep.py | 1 - src/components/LeftSidebar/nowPlaying.vue | 1 + src/components/shared/ArtistCard.vue | 1 + src/components/shared/SongItem.vue | 14 +++++--------- src/components/shared/TrackItem.vue | 6 ++++++ 9 files changed, 41 insertions(+), 26 deletions(-) diff --git a/server/app/db/mongodb/tracks.py b/server/app/db/mongodb/tracks.py index 9bd3d0d1..a7c3b1be 100644 --- a/server/app/db/mongodb/tracks.py +++ b/server/app/db/mongodb/tracks.py @@ -83,6 +83,13 @@ class Tracks(MongoTracks): songs = self.collection.find({"folder": query}).sort("title", pymongo.ASCENDING) return convert_many(songs) + def find_songs_by_filenames(self, filenames: list) -> list: + """ + Returns a list of all the tracks matching the filenames in the query params. + """ + songs = self.collection.find({"filepath": {"$in": filenames}}) + return convert_many(songs) + def find_songs_by_folder_og(self, query: str) -> list: """ Returns an unsorted list of all the track matching the folder in the query params @@ -90,14 +97,13 @@ class Tracks(MongoTracks): songs = self.collection.find({"folder": query}) return convert_many(songs) - def find_tracks_inside_path_regex(self, path: str) -> list: + def find_tracks_inside_path_regex(self, path: str) -> int: """ Returns a list of all the tracks matching the path in the query params. """ - songs = self.collection.find( + return self.collection.count_documents( {"filepath": {"$regex": f"^{path}", "$options": "i"}} ) - return convert_many(songs) def find_songs_by_artist(self, query: str) -> list: """ diff --git a/server/app/functions.py b/server/app/functions.py index 561760be..e8520266 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -22,16 +22,14 @@ def reindex_tracks(): Checks for new songs every 5 minutes. """ - while True: - trackslib.validate_tracks() + # while True: + trackslib.validate_tracks() - Populate() - CreateAlbums() + Populate() + CreateAlbums() - if helpers.Ping()(): - CheckArtistImages()() - - time.sleep(60) + if helpers.Ping()(): + CheckArtistImages()() @helpers.background diff --git a/server/app/lib/folderslib.py b/server/app/lib/folderslib.py index b5872736..c3f866b0 100644 --- a/server/app/lib/folderslib.py +++ b/server/app/lib/folderslib.py @@ -1,11 +1,14 @@ 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.logger import Log @dataclass @@ -28,7 +31,7 @@ def create_folder(dir: Dir) -> Folder: "name": dir.path.split("/")[-1], "path": dir.path, "is_sym": dir.is_sym, - "trackcount": get_folder_track_count(dir.path), + "trackcount": instances.tracks_instance.find_tracks_inside_path_regex(dir.path), } return Folder(folder) @@ -59,11 +62,17 @@ class getFnF: dirs.append(Dir(**dir)) elif entry.is_file() and entry.name.endswith((".mp3", ".flac")): files.append(entry.path) - tracks = instances.tracks_instance.find_songs_by_folder(self.path) + + tracks = instances.tracks_instance.find_songs_by_filenames(files) tracks = [Track(track) for track in tracks] + s = time.time() - folders = [create_folder(dir) for dir in dirs] - + # folders = [create_folder(dir) for dir in dirs] + with ThreadPoolExecutor() as pool: + iter = pool.map(create_folder, dirs) + folders = [i for i in iter if i is not None] + d = time.time() - s + Log(f"Did that in {d} seconds") folders = filter(lambda f: f.trackcount > 0, folders) return tracks, folders diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 6fa2aead..7509585b 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from pprint import pprint import time from concurrent.futures import ThreadPoolExecutor from typing import List diff --git a/server/app/prep.py b/server/app/prep.py index 0335c371..bb80b71b 100644 --- a/server/app/prep.py +++ b/server/app/prep.py @@ -21,7 +21,6 @@ class CopyFiles: for entry in files: src = os.path.join(os.getcwd(), entry["src"]) - print(f"Copying {src} to {entry['dest']}") if entry["is_dir"]: shutil.copytree( diff --git a/src/components/LeftSidebar/nowPlaying.vue b/src/components/LeftSidebar/nowPlaying.vue index 995399fc..0b7c8e00 100644 --- a/src/components/LeftSidebar/nowPlaying.vue +++ b/src/components/LeftSidebar/nowPlaying.vue @@ -102,6 +102,7 @@ const queue = useQStore(); .title { font-weight: 900; + word-break: break-all; } .artists { diff --git a/src/components/shared/ArtistCard.vue b/src/components/shared/ArtistCard.vue index d5d2a608..5f2acfc8 100644 --- a/src/components/shared/ArtistCard.vue +++ b/src/components/shared/ArtistCard.vue @@ -63,6 +63,7 @@ defineProps<{ font-size: 0.9rem; font-weight: 510; max-width: 7rem; + text-transform: capitalize; } } diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index 4cf7391d..aeda9c2f 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -117,15 +117,6 @@ function emitUpdate(track: Track) { user-select: none; -moz-user-select: none; - .context { - position: fixed; - top: 0; - left: 0; - height: 45px; - width: 45px; - background-color: red; - } - @include tablet-landscape { grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr; } @@ -145,6 +136,8 @@ function emitUpdate(track: Track) { } .song-album { + word-break: break-all; + .album { cursor: pointer; max-width: max-content; @@ -156,6 +149,8 @@ function emitUpdate(track: Track) { } .song-artists { + word-break: break-all; + .artist { cursor: pointer; } @@ -199,6 +194,7 @@ function emitUpdate(track: Track) { .title { cursor: pointer; + word-break: break-all; } } diff --git a/src/components/shared/TrackItem.vue b/src/components/shared/TrackItem.vue index a03a6a03..7086f3a5 100644 --- a/src/components/shared/TrackItem.vue +++ b/src/components/shared/TrackItem.vue @@ -122,7 +122,13 @@ const playThis = (track: Track) => { margin: 0 0.5rem 0 0; background-image: url(../../assets/images/null.webp); } + + .title { + word-break: break-all; + } + .artist { + word-break: break-all; font-size: small; color: rgba(255, 255, 255, 0.637); } From a92e1be7993b3bc5a023bfed7037a93302313d0e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 21 Jun 2022 17:13:19 +0300 Subject: [PATCH 16/56] blind write thumbnails validating methods --- server/app/api/__init__.py | 2 +- server/app/functions.py | 24 +++++----- server/app/helpers.py | 2 +- server/app/lib/albumslib.py | 92 ++++++++++++++++++++++++++++++------- server/app/settings.py | 4 +- 5 files changed, 91 insertions(+), 33 deletions(-) diff --git a/server/app/api/__init__.py b/server/app/api/__init__.py index b8cfb9ee..38416d8a 100644 --- a/server/app/api/__init__.py +++ b/server/app/api/__init__.py @@ -16,7 +16,7 @@ def initialize() -> None: """ functions.start_watchdog() prep.create_config_dir() - functions.reindex_tracks() + functions.run_checks() initialize() diff --git a/server/app/functions.py b/server/app/functions.py index e8520266..dc29a355 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -3,33 +3,33 @@ This module contains functions for the server """ import os import time +from concurrent.futures import ThreadPoolExecutor from io import BytesIO import requests -from app import helpers -from app import settings -from app.lib import watchdoge -from app.lib.populate import Populate, CreateAlbums from PIL import Image -from concurrent.futures import ThreadPoolExecutor -from app.lib import trackslib +from app import helpers, settings +from app.lib import watchdoge +from app.lib.albumslib import ValidateThumbs @helpers.background -def reindex_tracks(): +def run_checks(): """ Checks for new songs every 5 minutes. """ # while True: - trackslib.validate_tracks() + # trackslib.validate_tracks() - Populate() - CreateAlbums() + # Populate() + # CreateAlbums() - if helpers.Ping()(): - CheckArtistImages()() + # if helpers.Ping()(): + # CheckArtistImages()() + + ValidateThumbs() @helpers.background diff --git a/server/app/helpers.py b/server/app/helpers.py index a3c5a520..12fe7d73 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -104,7 +104,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 '/\\:*?"<>|']) + return "".join([i for i in name if i not in '/\\:*?"<>|#']) class UseBisection: diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index 5c389833..e8f2f28d 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -1,37 +1,95 @@ """ This library contains all the functions related to albums. """ -from pprint import pprint +from dataclasses import dataclass +import os import random from typing import List -from app import helpers, instances, models +from app import helpers, models from app.lib import taglib from tqdm import tqdm +from app.settings import THUMBS_PATH +from app import instances -def get_all_albums() -> List[models.Album]: + +# def get_all_albums() -> List[models.Album]: +# """ +# Returns a list of album objects for all albums in the database. +# """ +# print("Getting all albums...") + +# albums: List[models.Album] = [] + +# db_albums = instances.album_instance.get_all_albums() + +# for album in tqdm(db_albums, desc="Creating albums"): +# aa = models.Album(album) +# albums.append(aa) + +# return albums + + +@dataclass +class Thumbnail: + filename: str + + +class RipAlbumImage: """ - Returns a list of album objects for all albums in the database. + Rips a thumbnail for the given album hash. """ - print("Getting all albums...") - albums: List[models.Album] = [] + def __init__(self, hash: str) -> None: + tracks = instances.tracks_instance.find_tracks_by_hash(hash) + tracks = [models.Track(track) for track in tracks] - db_albums = instances.album_instance.get_all_albums() + for track in tracks: + ripped = taglib.extract_thumb(track.filepath, hash + ".webp") - for album in tqdm(db_albums, desc="Creating albums"): - aa = models.Album(album) - albums.append(aa) - - return albums + if ripped: + break -def validate() -> None: - """ - Creates album objects for all albums and returns - a list of track objects - """ +class ValidateThumbs: + @staticmethod + def remove_obsolete(): + """ + Removes unreferenced thumbnails from the thumbnails folder. + """ + entries = os.scandir(THUMBS_PATH) + entries = [entry for entry in entries if entry.is_file()] + + albums = helpers.Get.get_all_albums() + thumbs = [Thumbnail(album.hash + ".webp") for album in albums] + + for entry in tqdm(entries, desc="Validating thumbnails"): + e = helpers.UseBisection(thumbs, "filename", [entry.name])() + if e is not None: + os.remove(entry.path) + + @staticmethod + def find_lost_thumbnails(): + """ + Re-rip lost album thumbnails + """ + entries = os.scandir(THUMBS_PATH) + entries = [Thumbnail(entry) for entry in entries if entry.is_file()] + + albums = helpers.Get.get_all_albums() + thumbs = [(album.hash + ".webp") for album in albums] + + for t in thumbs: + e = helpers.UseBisection(entries, "filename", [t]) + + if e is None: + hash = t.split(".")[0] + RipAlbumImage(hash) + + def __init__(self) -> None: + self.remove_obsolete() + self.find_lost_thumbnails() def get_album_duration(album: List[models.Track]) -> int: diff --git a/server/app/settings.py b/server/app/settings.py index de2e0450..f6eb0ff7 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -2,7 +2,6 @@ Contains default configs """ import os -from dataclasses import dataclass import multiprocessing @@ -15,7 +14,8 @@ IMG_PATH = os.path.join(APP_DIR, "images") THUMBS_PATH = os.path.join(IMG_PATH, "thumbnails") TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio" # HOME_DIR = TEST_DIR -# URL + +# URLS IMG_BASE_URI = "http://127.0.0.1:8900/images/" IMG_ARTIST_URI = IMG_BASE_URI + "artists/" IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/" From 5fb920f958f0f6b4b76f289ee19a54af9e7b55a4 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 22 Jun 2022 11:11:13 +0300 Subject: [PATCH 17/56] finalize thumbnail validators - add logger colors --- server/app/functions.py | 12 ++++---- server/app/lib/albumslib.py | 12 ++++++-- server/app/lib/folderslib.py | 6 +--- server/app/lib/populate.py | 15 ++-------- server/app/lib/watchdoge.py | 9 ++++-- server/app/logger.py | 53 +++++++++++++++++++++++++++++++++--- 6 files changed, 76 insertions(+), 31 deletions(-) diff --git a/server/app/functions.py b/server/app/functions.py index dc29a355..84dabfd2 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -12,6 +12,8 @@ from PIL import Image from app import helpers, settings from app.lib import watchdoge from app.lib.albumslib import ValidateThumbs +from app.lib import trackslib +from app.lib.populate import CreateAlbums, Populate @helpers.background @@ -21,13 +23,13 @@ def run_checks(): """ # while True: - # trackslib.validate_tracks() + trackslib.validate_tracks() - # Populate() - # CreateAlbums() + Populate() + CreateAlbums() - # if helpers.Ping()(): - # CheckArtistImages()() + if helpers.Ping()(): + CheckArtistImages()() ValidateThumbs() diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index e8f2f28d..eedccb03 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -1,6 +1,7 @@ """ This library contains all the functions related to albums. """ +from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass import os import random @@ -80,13 +81,18 @@ class ValidateThumbs: albums = helpers.Get.get_all_albums() thumbs = [(album.hash + ".webp") for album in albums] - for t in thumbs: - e = helpers.UseBisection(entries, "filename", [t]) + def rip_image(t_hash: str): + e = helpers.UseBisection(entries, "filename", [t_hash])()[0] if e is None: - hash = t.split(".")[0] + hash = t_hash.split(".")[0] RipAlbumImage(hash) + return e + + with ThreadPoolExecutor() as pool: + pool.map(rip_image, thumbs) + def __init__(self) -> None: self.remove_obsolete() self.find_lost_thumbnails() diff --git a/server/app/lib/folderslib.py b/server/app/lib/folderslib.py index c3f866b0..1632b66f 100644 --- a/server/app/lib/folderslib.py +++ b/server/app/lib/folderslib.py @@ -8,7 +8,6 @@ from app.models import Folder from app.models import Track from app import instances -from app.logger import Log @dataclass @@ -65,14 +64,11 @@ class getFnF: tracks = instances.tracks_instance.find_songs_by_filenames(files) tracks = [Track(track) for track in tracks] - s = time.time() - # folders = [create_folder(dir) for dir in dirs] with ThreadPoolExecutor() as pool: iter = pool.map(create_folder, dirs) folders = [i for i in iter if i is not None] - d = time.time() - s - Log(f"Did that in {d} seconds") + folders = filter(lambda f: f.trackcount > 0, folders) return tracks, folders diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 7509585b..417e5e7a 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -4,12 +4,12 @@ from concurrent.futures import ThreadPoolExecutor from typing import List from app import settings +from app.logger import logg from app.helpers import Get, UseBisection, create_album_hash from app.helpers import run_fast_scandir from app.instances import tracks_instance from app.lib.albumslib import create_album from app.lib.taglib import get_tags -from app.logger import Log from app.models import Album, Track from tqdm import tqdm @@ -45,8 +45,6 @@ class Populate: if track["filepath"] in self.files: self.files.remove(track["filepath"]) - Log(f"Found {len(self.files)} untagged tracks") - def get_tags(self, file: str): tags = get_tags(file) @@ -60,16 +58,14 @@ class Populate: Loops through all the untagged files and tags them. """ - s = time.time() - print(f"Started tagging files") + logg.info("Tagging untagged tracks...") with ThreadPoolExecutor() as executor: executor.map(self.get_tags, self.files) if len(self.tagged_tracks) > 0: tracks_instance.insert_many(self.tagged_tracks) - d = time.time() - s - Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds") + logg.info(f"Tagged {len(self.tagged_tracks)} tracks.") @dataclass @@ -86,9 +82,7 @@ class CreateAlbums: prealbums = self.create_pre_albums(self.db_tracks) prealbums = self.filter_processed(self.db_albums, prealbums) - print(f"📌 {len(prealbums)}") - s = time.time() albums = [] for album in tqdm(prealbums, desc="Creating albums"): @@ -103,9 +97,6 @@ class CreateAlbums: # if i is not None: # albums.append(i) - d = time.time() - s - Log(f"Created {len(albums)} albums in {d} seconds") - if len(albums) > 0: instances.album_instance.insert_many(albums) diff --git a/server/app/lib/watchdoge.py b/server/app/lib/watchdoge.py index f3360bc7..2838077b 100644 --- a/server/app/lib/watchdoge.py +++ b/server/app/lib/watchdoge.py @@ -4,7 +4,7 @@ This library contains the classes and functions related to the watchdog file wat import os import time -from app import api +from app.logger import logg from app import instances from app import models from app.helpers import Get, create_album_hash @@ -27,7 +27,12 @@ class OnMyWatch: def run(self): event_handler = Handler() self.observer.schedule(event_handler, self.directory, recursive=True) - self.observer.start() + + try: + self.observer.start() + except OSError: + logg.error("Could not start watchdog.") + return try: while True: diff --git a/server/app/logger.py b/server/app/logger.py index ee844ccd..e67d184a 100644 --- a/server/app/logger.py +++ b/server/app/logger.py @@ -1,8 +1,53 @@ +import logging + from app.settings import logger -class Log: +class CustomFormatter(logging.Formatter): - def __init__(self, msg): - if logger.enable: - print("\n🦋 " + msg + "\n") + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + # format = ( + # "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" + # ) + format = "%(levelname)s: @%(name)s: >>> %(message)s (%(filename)s:%(lineno)d)" + + FORMATS = { + logging.DEBUG: grey + format + reset, + logging.INFO: grey + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +logg = logging.getLogger("ALICE_MUSIC_SERVER") +logg.setLevel(logging.DEBUG) + +# create console handler with a higher log level +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) + +ch.setFormatter(CustomFormatter()) + +logg.addHandler(ch) + + +def get_logger(): + if logger.enable: + return logg + + return None + + +logg = get_logger() + +# copied from: https://stackoverflow.com/a/56944256: From de7aba3ba7babc2d057877ec60ab1e5bc13b961a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 23 Jun 2022 08:30:55 +0300 Subject: [PATCH 18/56] show toast if album return 204 --- server/app/api/album.py | 2 +- server/app/helpers.py | 33 +++++++++++++++++++-------------- server/app/lib/albumslib.py | 21 ++++++++++++++------- server/app/logger.py | 4 ++-- server/app/settings.py | 13 ------------- src/composables/pages/album.ts | 18 +++++++++++++----- src/stores/pages/album.ts | 3 ++- 7 files changed, 51 insertions(+), 43 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index 3b4347d1..e37c185b 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -49,7 +49,7 @@ def get_album(): album = instances.album_instance.find_album_by_hash(albumhash) if not album: - return {"error": "Album not found."}, 404 + return {"error": "Album not created yet."}, 204 album = models.Album(album) diff --git a/server/app/helpers.py b/server/app/helpers.py index 12fe7d73..6cdd9dec 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -2,7 +2,6 @@ This module contains mini functions for the server. """ import os -import random import threading from datetime import datetime from typing import Dict, Set @@ -11,8 +10,8 @@ from typing import List import requests from app import models -from app import settings from app import instances +from app.lib.albumslib import Thumbnail def background(func): @@ -85,12 +84,15 @@ 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 '/\\:*?"<>|&']) + hash = "".join([i for i in lower if i not in ill_chars]) return hash @@ -104,7 +106,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 '/\\:*?"<>|#']) + return "".join([i for i in name if i not in ill_chars]) class UseBisection: @@ -116,21 +118,20 @@ class UseBisection: """ def __init__(self, source: List, search_from: str, queries: List[str]) -> None: - self.list = source - self.queries = queries - self.search_from = search_from - self.list.sort(key=lambda x: getattr(x, search_from)) + self.source_list = source + self.queries_list = queries + self.attr = search_from + self.source_list.sort(key=lambda x: getattr(x, search_from)) def find(self, query: str): left = 0 - right = len(self.list) - 1 + right = len(self.source_list) - 1 while left <= right: mid = (left + right) // 2 - - if self.list[mid].__getattribute__(self.search_from) == query: - return self.list[mid] - elif self.list[mid].__getattribute__(self.search_from) > query: + if self.source_list[mid].__getattribute__(self.attr) == query: + return self.source_list[mid] + elif self.source_list[mid].__getattribute__(self.attr) > query: right = mid - 1 else: left = mid + 1 @@ -138,7 +139,11 @@ class UseBisection: return None def __call__(self) -> List: - return [self.find(query) for query in self.queries] + if len(self.source_list) == 0: + print("🚀🚀🚀🚀🚀🚀🚀") + return [None] + + return [self.find(query) for query in self.queries_list] class Get: diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index eedccb03..dcb70438 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -2,6 +2,7 @@ This library contains all the functions related to albums. """ from concurrent.futures import ThreadPoolExecutor +from multiprocessing import Manager from dataclasses import dataclass import os import random @@ -13,7 +14,7 @@ from tqdm import tqdm from app.settings import THUMBS_PATH from app import instances - +from app.logger import logg # def get_all_albums() -> List[models.Album]: # """ @@ -67,7 +68,8 @@ class ValidateThumbs: for entry in tqdm(entries, desc="Validating thumbnails"): e = helpers.UseBisection(thumbs, "filename", [entry.name])() - if e is not None: + + if e is None: os.remove(entry.path) @staticmethod @@ -76,7 +78,7 @@ class ValidateThumbs: Re-rip lost album thumbnails """ entries = os.scandir(THUMBS_PATH) - entries = [Thumbnail(entry) 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,13 +87,18 @@ class ValidateThumbs: e = helpers.UseBisection(entries, "filename", [t_hash])()[0] if e is None: - hash = t_hash.split(".")[0] + hash = t_hash.replace(".webp", "") RipAlbumImage(hash) - return e + logg.info("Ripping lost album thumbnails...") + # with ThreadPoolExecutor() as pool: + # i = pool.map(rip_image, thumbs) + # [a for a in i] + # ⚠️ empty lists are sent to the useBisection function as the source list. + for thumb in thumbs: + rip_image(thumb) - with ThreadPoolExecutor() as pool: - pool.map(rip_image, thumbs) + logg.info("Ripping lost album thumbnails...done") def __init__(self) -> None: self.remove_obsolete() diff --git a/server/app/logger.py b/server/app/logger.py index e67d184a..1c4c5785 100644 --- a/server/app/logger.py +++ b/server/app/logger.py @@ -13,7 +13,7 @@ class CustomFormatter(logging.Formatter): # format = ( # "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" # ) - format = "%(levelname)s: @%(name)s: >>> %(message)s (%(filename)s:%(lineno)d)" + format = "[%(asctime)s] [%(levelname)s] [@%(name)s] >>> %(message)s [%(filename)s:%(lineno)d]" FORMATS = { logging.DEBUG: grey + format + reset, @@ -25,7 +25,7 @@ class CustomFormatter(logging.Formatter): def format(self, record): log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) + formatter = logging.Formatter(log_fmt, "%H:%M:%S") return formatter.format(record) diff --git a/server/app/settings.py b/server/app/settings.py index f6eb0ff7..ccb2bace 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -26,19 +26,6 @@ DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp" LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" -P_COLORS = [ - "rgb(4, 40, 196)", - "rgb(196, 4, 68)", - "rgb(4, 99, 59)", - "rgb(161, 87, 1)", - "rgb(1, 161, 22)", - "rgb(116, 1, 161)", - "rgb(0, 0, 0)", - "rgb(95, 95, 95)", - "rgb(141, 132, 2)", - "rgb(141, 11, 2)", -] - CPU_COUNT = multiprocessing.cpu_count() diff --git a/src/composables/pages/album.ts b/src/composables/pages/album.ts index bdd32702..3739f426 100644 --- a/src/composables/pages/album.ts +++ b/src/composables/pages/album.ts @@ -1,9 +1,13 @@ -import axios, { AxiosError } from "axios"; import state from "../state"; import { AlbumInfo, Track } from "../../interfaces"; import useAxios from "../useAxios"; +import { NotifType, useNotifStore } from "@/stores/notification"; -const getAlbumTracks = async (album: string, artist: string) => { +const getAlbumData = async ( + album: string, + artist: string, + ToastStore: typeof useNotifStore +) => { const url = state.settings.uri + "/album"; interface AlbumData { @@ -19,9 +23,13 @@ const getAlbumTracks = async (album: string, artist: string) => { }, }); - if (status == 404) { + if (status == 204) { + ToastStore().showNotification("Album not created yet!", NotifType.Error); return { - info: {}, + info: { + album: album, + artist: artist, + }, tracks: [], }; } @@ -63,4 +71,4 @@ const getAlbumBio = async (album: string, albumartist: string) => { } }; -export { getAlbumTracks, getAlbumArtists, getAlbumBio }; +export { getAlbumData as getAlbumTracks, getAlbumArtists, getAlbumBio }; diff --git a/src/stores/pages/album.ts b/src/stores/pages/album.ts index aa391c81..f1af23ae 100644 --- a/src/stores/pages/album.ts +++ b/src/stores/pages/album.ts @@ -1,4 +1,5 @@ import { defineStore } from "pinia"; +import { useNotifStore } from "../notification"; import { Track, Artist, AlbumInfo } from "../../interfaces"; import { getAlbumTracks, @@ -21,7 +22,7 @@ export default defineStore("album", { * @param albumartist artist of the album */ async fetchTracksAndArtists(title: string, albumartist: string) { - const tracks = await getAlbumTracks(title, albumartist); + const tracks = await getAlbumTracks(title, albumartist, useNotifStore); const artists = await getAlbumArtists(title, albumartist); this.tracks = tracks.tracks; From 3eaab97f1f920081fe51150afb02aa337ab2f46a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 23 Jun 2022 12:32:07 +0300 Subject: [PATCH 19/56] replace background image with svg - add symlink svg - fix validate playlist thumbnails --- package.json | 4 +- server/app/functions.py | 6 +- server/app/lib/albumslib.py | 34 +- server/app/lib/playlistlib.py | 39 +- server/app/logger.py | 7 +- src/assets/icons/album.svg | 2 +- src/assets/icons/symlink.svg | 3 + src/components/FolderView/FolderItem.vue | 39 +- src/components/LeftSidebar/Navigation.vue | 102 ++-- src/interfaces.ts | 1 + src/svg.d.ts | 4 + tsconfig.json | 3 +- vite.config.js | 4 +- yarn.lock | 712 +++++++++++++++++++++- 14 files changed, 835 insertions(+), 125 deletions(-) create mode 100644 src/assets/icons/symlink.svg create mode 100644 src/svg.d.ts diff --git a/package.json b/package.json index abe0176e..1397fad3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "register-service-worker": "^1.7.1", "sass": "^1.49.0", "sass-loader": "^10", + "vite-svg-loader": "^3.4.0", "vue": "^3.0.0", "vue-debounce": "^3.0.2", "vue-router": "^4.0.0-0", @@ -28,7 +29,8 @@ "@vue/compiler-sfc": "^3.0.0", "eslint": "^8.7.0", "eslint-plugin-vue": "^8.3.0", - "vite": "^2.5.4" + "vite": "^2.5.4", + "vue-svg-loader": "^0.16.0" }, "packageManager": "yarn@3.1.1" } diff --git a/server/app/functions.py b/server/app/functions.py index 84dabfd2..e62128a9 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -11,9 +11,10 @@ from PIL import Image from app import helpers, settings from app.lib import watchdoge -from app.lib.albumslib import ValidateThumbs +from app.lib.albumslib import ValidateAlbumThumbs from app.lib import trackslib from app.lib.populate import CreateAlbums, Populate +from app.lib.playlistlib import ValidatePlaylistThumbs @helpers.background @@ -31,7 +32,8 @@ def run_checks(): if helpers.Ping()(): CheckArtistImages()() - ValidateThumbs() + ValidateAlbumThumbs() + ValidatePlaylistThumbs() @helpers.background diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index dcb70438..f8c8b925 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -1,36 +1,16 @@ """ This library contains all the functions related to albums. """ -from concurrent.futures import ThreadPoolExecutor -from multiprocessing import Manager -from dataclasses import dataclass import os import random +from dataclasses import dataclass from typing import List -from app import helpers, models +from app import helpers, instances, models from app.lib import taglib -from tqdm import tqdm - -from app.settings import THUMBS_PATH -from app import instances from app.logger import logg - -# def get_all_albums() -> List[models.Album]: -# """ -# Returns a list of album objects for all albums in the database. -# """ -# print("Getting all albums...") - -# albums: List[models.Album] = [] - -# db_albums = instances.album_instance.get_all_albums() - -# for album in tqdm(db_albums, desc="Creating albums"): -# aa = models.Album(album) -# albums.append(aa) - -# return albums +from app.settings import THUMBS_PATH +from tqdm import tqdm @dataclass @@ -54,7 +34,7 @@ class RipAlbumImage: break -class ValidateThumbs: +class ValidateAlbumThumbs: @staticmethod def remove_obsolete(): """ @@ -90,7 +70,7 @@ class ValidateThumbs: hash = t_hash.replace(".webp", "") RipAlbumImage(hash) - logg.info("Ripping lost album thumbnails...") + logg.info("Ripping lost album thumbnails") # with ThreadPoolExecutor() as pool: # i = pool.map(rip_image, thumbs) # [a for a in i] @@ -98,7 +78,7 @@ class ValidateThumbs: for thumb in thumbs: rip_image(thumb) - logg.info("Ripping lost album thumbnails...done") + logg.info("Ripping lost album thumbnails ... ✅") def __init__(self) -> None: self.remove_obsolete() diff --git a/server/app/lib/playlistlib.py b/server/app/lib/playlistlib.py index 88cdcad5..a1d9f4d8 100644 --- a/server/app/lib/playlistlib.py +++ b/server/app/lib/playlistlib.py @@ -7,9 +7,6 @@ import string from datetime import datetime from typing import List -from tqdm import tqdm - -from app import api from app import exceptions from app import instances from app import models @@ -19,9 +16,13 @@ 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() + def add_track(playlistid: str, trackid: str): """ @@ -93,27 +94,31 @@ def save_p_image(file: datastructures.FileStorage, pid: str): return img_path, thumb_path -def validate_images(): +class ValidatePlaylistThumbs: """ Removes all unused images in the images/playlists folder. """ - images = [] - p = instances.playlist_instance.get_all_playlists() - playlists = [models.Playlist(p) for p in p] - for playlist in playlists: - if playlist.image: - img_path = playlist.image.split("/")[-1] - thumb_path = playlist.thumb.split("/")[-1] + def __init__(self) -> None: + images = [] + playlists = Get.get_all_playlists() - images.append(img_path) - images.append(thumb_path) + logg.info("Validating playlist thumbnails") + for playlist in playlists: + if playlist.image: + img_path = playlist.image.split("/")[-1] + thumb_path = playlist.thumb.split("/")[-1] - p_path = os.path.join(settings.APP_DIR, "images", "playlists") + images.append(img_path) + images.append(thumb_path) - for image in os.listdir(p_path): - if image not in images: - os.remove(os.path.join(p_path, image)) + 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)) + + logg.info("Validating playlist thumbnails ... ✅") def create_new_date(): diff --git a/server/app/logger.py b/server/app/logger.py index 1c4c5785..15b91233 100644 --- a/server/app/logger.py +++ b/server/app/logger.py @@ -13,7 +13,7 @@ class CustomFormatter(logging.Formatter): # format = ( # "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" # ) - format = "[%(asctime)s] [%(levelname)s] [@%(name)s] >>> %(message)s [%(filename)s:%(lineno)d]" + format = "[%(asctime)s] [%(levelname)s] [@%(name)s]ℹ️ %(message)s" FORMATS = { logging.DEBUG: grey + format + reset, @@ -42,10 +42,7 @@ logg.addHandler(ch) def get_logger(): - if logger.enable: - return logg - - return None + return logg logg = get_logger() diff --git a/src/assets/icons/album.svg b/src/assets/icons/album.svg index 345ba566..5f52fc75 100644 --- a/src/assets/icons/album.svg +++ b/src/assets/icons/album.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/symlink.svg b/src/assets/icons/symlink.svg new file mode 100644 index 00000000..69136112 --- /dev/null +++ b/src/assets/icons/symlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/FolderView/FolderItem.vue b/src/components/FolderView/FolderItem.vue index 8e4d1441..8ca2b6d5 100644 --- a/src/components/FolderView/FolderItem.vue +++ b/src/components/FolderView/FolderItem.vue @@ -1,33 +1,36 @@ diff --git a/src/interfaces.ts b/src/interfaces.ts index 03b96aa5..c8dacfbe 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -22,6 +22,7 @@ export interface Folder { path: string; trackcount: number; subdircount: number; + is_sym: boolean; } export interface AlbumInfo { diff --git a/src/svg.d.ts b/src/svg.d.ts new file mode 100644 index 00000000..5e52f805 --- /dev/null +++ b/src/svg.d.ts @@ -0,0 +1,4 @@ +declare module "*.svg" { + const content: string; + export default content; +} diff --git a/tsconfig.json b/tsconfig.json index ffbf78bf..bc85a804 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,5 +5,6 @@ "baseUrl": ["./"], "@/*": ["./src/*"] } - } + }, + "include": ["src/svg.d.ts"] } diff --git a/vite.config.js b/vite.config.js index 2efe2a50..05579d05 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,11 @@ import { defineConfig } from "vite"; +import svgLoader from 'vite-svg-loader' import vue from "@vitejs/plugin-vue"; + const path = require("path"); export default defineConfig({ - plugins: [vue()], + plugins: [vue(), svgLoader()], resolve: { alias: { "@": path.resolve(__dirname, "src"), diff --git a/yarn.lock b/yarn.lock index 3aae9df2..a94d50e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@types/eslint-scope@^3.7.0": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" @@ -67,6 +72,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.14.tgz#33b9b94f789a8fedd30a68efdbca4dbb06b61f20" integrity sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng== +"@types/q@^1.5.1": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + "@vitejs/plugin-vue@^1.6.1": version "1.10.2" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-1.10.2.tgz#d718479e2789d8a94b63e00f23f1898ba239253a" @@ -82,6 +92,16 @@ estree-walker "^2.0.2" source-map "^0.6.1" +"@vue/compiler-core@3.2.37": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a" + integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + source-map "^0.6.1" + "@vue/compiler-dom@3.2.29": version "3.2.29" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715" @@ -90,6 +110,14 @@ "@vue/compiler-core" "3.2.29" "@vue/shared" "3.2.29" +"@vue/compiler-dom@3.2.37": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5" + integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ== + dependencies: + "@vue/compiler-core" "3.2.37" + "@vue/shared" "3.2.37" + "@vue/compiler-sfc@3.2.29", "@vue/compiler-sfc@^3.0.0": version "3.2.29" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead" @@ -106,6 +134,22 @@ postcss "^8.1.10" source-map "^0.6.1" +"@vue/compiler-sfc@^3.2.20": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4" + integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.37" + "@vue/compiler-dom" "3.2.37" + "@vue/compiler-ssr" "3.2.37" + "@vue/reactivity-transform" "3.2.37" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + "@vue/compiler-ssr@3.2.29": version "3.2.29" resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.29.tgz#37b15b32dcd2f6b410bb61fca3f37b1a92b7eb1e" @@ -114,6 +158,14 @@ "@vue/compiler-dom" "3.2.29" "@vue/shared" "3.2.29" +"@vue/compiler-ssr@3.2.37": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff" + integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw== + dependencies: + "@vue/compiler-dom" "3.2.37" + "@vue/shared" "3.2.37" + "@vue/devtools-api@^6.0.0-beta.18": version "6.0.0-beta.21.1" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.21.1.tgz#f1410f53c42aa67fa3b01ca7bdba891f69d7bc97" @@ -135,6 +187,17 @@ estree-walker "^2.0.2" magic-string "^0.25.7" +"@vue/reactivity-transform@3.2.37": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca" + integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.37" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + magic-string "^0.25.7" + "@vue/reactivity@3.2.29": version "3.2.29" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.29.tgz#afdc9c111d4139b14600be17ad80267212af6052" @@ -172,6 +235,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925" integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw== +"@vue/shared@3.2.37": + version "3.2.37" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702" + integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== + "@vueuse/core@^8.1.2", "@vueuse/core@^8.5.0": version "8.5.0" resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-8.5.0.tgz#2b7548e52165c88e1463756c36188e105d806543" @@ -372,6 +440,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -387,11 +462,29 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +array.prototype.reduce@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz#8167e80089f78bff70a99e20bd4201d4663b0a6f" + integrity sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.2" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + axios@^0.26.1: version "0.26.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" @@ -414,6 +507,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -445,6 +543,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -455,6 +561,15 @@ caniuse-lite@^1.0.30001286: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001306.tgz#582592afe243bad2223081b8abab07bf289cc699" integrity sha512-Wd1OuggRzg1rbnM5hv1wXs2VkxJH/AA+LuudlIqvZiCvivF+wJJe2mgBZC8gPMgI7D76PP5CTx8Luvaqc1V6OQ== +chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -483,6 +598,22 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -490,6 +621,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -500,6 +636,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -514,6 +655,65 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +csso@^4.0.2, csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + csstype@^2.6.8: version "2.6.19" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" @@ -536,6 +736,14 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + defu@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/defu/-/defu-6.0.0.tgz#b397a6709a2f3202747a3d9daf9446e41ad0c5fc" @@ -548,6 +756,57 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + electron-to-chromium@^1.4.17: version "1.4.63" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.63.tgz#866db72d1221fda89419dc22669d03833e11625d" @@ -566,11 +825,59 @@ enhanced-resolve@^5.8.3: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + esbuild-android-arm64@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" @@ -684,6 +991,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -782,6 +1094,11 @@ espree@^9.0.0, espree@^9.2.0, espree@^9.3.0: acorn-jsx "^5.3.1" eslint-visitor-keys "^3.1.0" +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -897,11 +1214,43 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + glob-parent@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -945,11 +1294,40 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1003,6 +1381,22 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1010,6 +1404,19 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -1017,6 +1424,13 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1029,11 +1443,59 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1048,6 +1510,14 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1070,6 +1540,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -1095,6 +1572,15 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== +loader-utils@^1.2.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + loader-utils@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" @@ -1128,6 +1614,16 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -1152,7 +1648,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -1162,6 +1658,13 @@ mitt@^3.0.0: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1192,6 +1695,59 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz#7965e6437a57278b587383831a9b829455a4bc37" + integrity sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ== + dependencies: + array.prototype.reduce "^1.0.4" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.1" + +object.values@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1280,6 +1836,11 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -1294,6 +1855,15 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -1357,6 +1927,11 @@ sass@^1.49.0: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" @@ -1392,6 +1967,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -1420,6 +2004,34 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -1448,6 +2060,13 @@ style-value-types@^5.1.0: hey-listen "^1.0.8" tslib "^2.3.1" +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1467,6 +2086,45 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-to-vue@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/svg-to-vue/-/svg-to-vue-0.7.0.tgz#ec86deb1742be38319462e36703af1dfa2f9fad9" + integrity sha512-Tg2nMmf3BQorYCAjxbtTkYyWPVSeox5AZUFvfy4MoWK/5tuQlnA/h3LAlTjV3sEvOC5FtUNovRSj3p784l4KOA== + dependencies: + svgo "^1.3.2" + +svgo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -1521,6 +2179,21 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -1528,11 +2201,29 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +vite-svg-loader@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.4.0.tgz#4638827fe86b85ecfcea1ad61dd972c351d5befd" + integrity sha512-xD3yb1FX+f4l9/TmsYIqyki8ncpcVsZ2gEJFh/wLuNNqt55C8OJ+JlcMWOA/Z9gRA+ylV/TA1wmJLxzZkCRqlA== + dependencies: + "@vue/compiler-sfc" "^3.2.20" + svgo "^2.7.0" + vite@^2.5.4: version "2.7.13" resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.13.tgz#99b56e27dfb1e4399e407cf94648f5c7fb9d77f5" @@ -1575,6 +2266,14 @@ vue-router@^4.0.0-0: dependencies: "@vue/devtools-api" "^6.0.0-beta.18" +vue-svg-loader@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/vue-svg-loader/-/vue-svg-loader-0.16.0.tgz#adccbdc9aca90132bde9c9d96cd49f74efecd345" + integrity sha512-2RtFXlTCYWm8YAEO2qAOZ2SuIF2NvLutB5muc3KDYoZq5ZeCHf8ggzSan3ksbbca7CJ/Aw57ZnDF4B7W/AkGtw== + dependencies: + loader-utils "^1.2.3" + svg-to-vue "^0.7.0" + vue@^3.0.0: version "3.2.29" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a" @@ -1629,6 +2328,17 @@ webpack@^5.64.4: watchpack "^2.3.1" webpack-sources "^3.2.3" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" From 71dc2715e1afa409bc7aa49c84aa6e124586a0b4 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 23 Jun 2022 12:49:47 +0300 Subject: [PATCH 20/56] add new symlink svg --- src/assets/icons/symlink.svg | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/assets/icons/symlink.svg b/src/assets/icons/symlink.svg index 69136112..2bf800c0 100644 --- a/src/assets/icons/symlink.svg +++ b/src/assets/icons/symlink.svg @@ -1,3 +1,12 @@ - + + + + + + + + + + From 7ae97370d67441b6d9c471587c32accccfc60eef Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 23 Jun 2022 15:39:19 +0300 Subject: [PATCH 21/56] update tsconfig.json --- src/components/LeftSidebar/Navigation.vue | 6 +++--- tsconfig.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/LeftSidebar/Navigation.vue b/src/components/LeftSidebar/Navigation.vue index bbef8a3a..706fc0ef 100644 --- a/src/components/LeftSidebar/Navigation.vue +++ b/src/components/LeftSidebar/Navigation.vue @@ -19,9 +19,9 @@ @@ -95,20 +67,6 @@ const menus = [ background-size: 1.5rem; } } - - // #mixes-icon { - // background-image: url(../../assets/icons/mix.svg); - // } - - // #folders-icon { - // background-image: url(../../assets/icons/folder.svg); - // } - // #settings-icon { - // background-image: url(../../assets/icons/settings.svg); - // } - // #tags-icon { - // background-image: url(../../assets/icons/tag.svg); - // } } svg { From 8e488c063bfc87d0c1ca8a6242a2ae0939f13e67 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Fri, 24 Jun 2022 10:35:30 +0300 Subject: [PATCH 23/56] add menu button on songlist item --- src/assets/css/_variables.scss | 5 +++ src/components/LeftSidebar/Navigation.vue | 2 +- src/components/shared/SongItem.vue | 39 ++++++++++++++++++++++- todo | 17 ++-------- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/assets/css/_variables.scss b/src/assets/css/_variables.scss index 358be400..52fe2039 100644 --- a/src/assets/css/_variables.scss +++ b/src/assets/css/_variables.scss @@ -46,6 +46,11 @@ $danger: $red; $track-hover: $gray4; $context: $gray5; +// SVG COLORS +$default: $accent; +$track-btn-svg: $red; +$side-nav-svg: $red; + // media query mixins @mixin phone-only { @media (max-width: 599px) { diff --git a/src/components/LeftSidebar/Navigation.vue b/src/components/LeftSidebar/Navigation.vue index f92eafab..e0c8ba9e 100644 --- a/src/components/LeftSidebar/Navigation.vue +++ b/src/components/LeftSidebar/Navigation.vue @@ -77,7 +77,7 @@ const menus = [ } svg > path { - fill: $red !important; + fill: $side-nav-svg; } } diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index aeda9c2f..bb10b63f 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -52,7 +52,10 @@

- {{ formatSeconds(props.song.length) }} +
{{ formatSeconds(props.song.length) }}
+
+ +
@@ -63,6 +66,7 @@ import useContextStore from "../../stores/context"; import useModalStore from "../../stores/modal"; import useQueueStore from "../../stores/queue"; import { ContextSrc } from "../../composables/enums"; +import OptionSvg from "../../assets/icons/more.svg"; import { ref } from "vue"; import trackContext from "../../contexts/track_context"; @@ -127,6 +131,10 @@ function emitUpdate(track: Track) { &:hover { background-color: $gray4; + + .options-icon { + opacity: 1 !important; + } } .song-duration { @@ -137,6 +145,7 @@ function emitUpdate(track: Track) { .song-album { word-break: break-all; + text-transform: capitalize; .album { cursor: pointer; @@ -174,6 +183,34 @@ function emitUpdate(track: Track) { .song-duration { font-size: 0.9rem; width: 5rem !important; + display: flex; + align-items: center; + gap: $smaller; + opacity: 1; + transition: all 0.2s ease-in; + + svg { + transition: all 0.2s ease-in; + transform: rotate(90deg); + stroke: $track-btn-svg; + + circle { + fill: $track-btn-svg; + } + } + + .options-icon { + opacity: 0; + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + aspect-ratio: 1; + + &:hover { + background-color: $gray5; + } + } } .flex { diff --git a/todo b/todo index 83035e5b..439903a3 100644 --- a/todo +++ b/todo @@ -18,20 +18,7 @@ ### Notes -- Maybe first process tags and store them to the database, then process albums from these tags. - -Like,this: -1. Tag files -2. Insert all into the database -3. Fetch all albums -4. Fetch all tracks -5. Create prealbums -6. Pop all processed albums -7. Use the following procedure to process single album image: - 7.1. Get a single album track, pop it from memory - 7.2. Try ripping image, - (i). if successful: hurray! we won't have to go further. - (ii). if failed, try getting another track from the same album, try ripping image. - (iii). If failed, repeat (ii) until success, or until you run out of tracks. In that case, set album image to fallback. + +- Resolve album page using albumhash instead of album title and artist \ No newline at end of file From a7fcb7f825956d868a284c617e811fd9d0aa87a6 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Fri, 24 Jun 2022 19:52:14 +0300 Subject: [PATCH 24/56] attach context menu to options icon --- src/components/shared/SongItem.vue | 56 +++++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index bb10b63f..ceaf64a6 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -53,9 +53,18 @@
{{ formatSeconds(props.song.length) }}
-
- -
+
+
+
@@ -77,6 +86,7 @@ const contextStore = useContextStore(); const context_on = ref(false); const imguri = paths.images.thumb; +const options_button_clicked = ref(false); const showContextMenu = (e: Event) => { e.preventDefault(); @@ -90,6 +100,7 @@ const showContextMenu = (e: Event) => { contextStore.$subscribe((mutation, state) => { if (!state.visible) { context_on.value = false; + options_button_clicked.value = false; } }); }; @@ -114,7 +125,7 @@ function emitUpdate(track: Track) { .songlist-item { display: grid; align-items: center; - grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr; + grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr 2.5rem; height: 3.75rem; text-align: left; gap: $small; @@ -122,11 +133,11 @@ function emitUpdate(track: Track) { -moz-user-select: none; @include tablet-landscape { - grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr; + grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2.5rem; } @include tablet-portrait { - grid-template-columns: 1.5rem 1.5fr 1fr; + grid-template-columns: 1.5rem 1.5fr 1fr 2.5rem; } &:hover { @@ -139,7 +150,7 @@ function emitUpdate(track: Track) { .song-duration { @include tablet-landscape { - display: none; + display: none !important; } } @@ -183,11 +194,18 @@ function emitUpdate(track: Track) { .song-duration { font-size: 0.9rem; width: 5rem !important; + + text-align: right; + } + + .options-icon { + opacity: 0; display: flex; align-items: center; - gap: $smaller; - opacity: 1; - transition: all 0.2s ease-in; + justify-content: center; + aspect-ratio: 1; + width: 2rem; + margin-right: 1rem; svg { transition: all 0.2s ease-in; @@ -199,20 +217,16 @@ function emitUpdate(track: Track) { } } - .options-icon { - opacity: 0; - display: flex; - align-items: center; - justify-content: center; - width: 2rem; - aspect-ratio: 1; - - &:hover { - background-color: $gray5; - } + &:hover { + background-color: $gray5; } } + .options_button_clicked { + background-color: $gray5; + opacity: 1; + } + .flex { position: relative; padding-left: 4rem; From abbca285f00a23cce5da42fa4e847377f88f3ea1 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 25 Jun 2022 09:12:44 +0300 Subject: [PATCH 25/56] set track album width to max-width --- src/components/shared/SongItem.vue | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index ceaf64a6..3cfe1b36 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -38,7 +38,7 @@ -
- {{ props.song.album }} -
+ {{ props.song.album }}
{{ formatSeconds(props.song.length) }}
@@ -157,11 +155,8 @@ function emitUpdate(track: Track) { .song-album { word-break: break-all; text-transform: capitalize; - - .album { - cursor: pointer; - max-width: max-content; - } + max-width: max-content; + cursor: pointer; @include tablet-portrait { display: none; From 92ef22596bd4070bf0bc513e0512314d2479bf7c Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 26 Jun 2022 18:46:17 +0300 Subject: [PATCH 26/56] fix removing duplicate tracks - add uniq_hash prop to Track class --- server/app/api/album.py | 1 + server/app/api/artist.py | 66 ++++++++++++++++++------------------- server/app/api/search.py | 1 + server/app/helpers.py | 25 +++++--------- server/app/lib/trackslib.py | 50 +--------------------------- server/app/models.py | 12 +++++++ 6 files changed, 57 insertions(+), 98 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index e37c185b..eff42f82 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -45,6 +45,7 @@ def get_album(): tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) tracks = [models.Track(t) for t in tracks] + tracks = helpers.RemoveDuplicates(tracks)() album = instances.album_instance.find_album_by_hash(albumhash) diff --git a/server/app/api/artist.py b/server/app/api/artist.py index 0958197a..e82d02c3 100644 --- a/server/app/api/artist.py +++ b/server/app/api/artist.py @@ -11,48 +11,48 @@ from flask import Blueprint artist_bp = Blueprint("artist", __name__, url_prefix="/") -@artist_bp.route("/artist/") -@cache.cached() -def get_artist_data(artist: str): - """Returns the artist's data, tracks and albums""" - artist = urllib.parse.unquote(artist) - artist_obj = instances.artist_instance.get_artists_by_name(artist) +# @artist_bp.route("/artist/") +# @cache.cached() +# def get_artist_data(artist: str): +# """Returns the artist's data, tracks and albums""" +# artist = urllib.parse.unquote(artist) +# artist_obj = instances.artist_instance.get_artists_by_name(artist) - def get_artist_tracks(): - songs = instances.tracks_instance.find_songs_by_artist(artist) +# def get_artist_tracks(): +# songs = instances.tracks_instance.find_songs_by_artist(artist) - return songs +# return songs - artist_songs = get_artist_tracks() - songs = helpers.remove_duplicates(artist_songs) +# artist_songs = get_artist_tracks() +# songs = helpers.remove_duplicates(artist_songs) - def get_artist_albums(): - artist_albums = [] - albums_with_count = [] +# def get_artist_albums(): +# artist_albums = [] +# albums_with_count = [] - albums = instances.tracks_instance.find_songs_by_albumartist(artist) +# albums = instances.tracks_instance.find_songs_by_albumartist(artist) - for song in albums: - if song["album"] not in artist_albums: - artist_albums.append(song["album"]) +# for song in albums: +# if song["album"] not in artist_albums: +# artist_albums.append(song["album"]) - for album in artist_albums: - count = 0 - length = 0 +# for album in artist_albums: +# count = 0 +# length = 0 - for song in artist_songs: - if song["album"] == album: - count = count + 1 - length = length + song["length"] +# for song in artist_songs: +# if song["album"] == album: +# count = count + 1 +# length = length + song["length"] - album_ = {"title": album, "count": count, "length": length} +# album_ = {"title": album, "count": count, "length": length} - albums_with_count.append(album_) +# albums_with_count.append(album_) - return albums_with_count +# return albums_with_count - return { - "artist": artist_obj, - "songs": songs, - "albums": get_artist_albums() - } +# return { +# "artist": artist_obj, +# "songs": songs, +# "albums": get_artist_albums() +# } diff --git a/server/app/api/search.py b/server/app/api/search.py index 15ab35ca..cb90f543 100644 --- a/server/app/api/search.py +++ b/server/app/api/search.py @@ -48,6 +48,7 @@ class DoSearch: """ self.tracks = helpers.Get.get_all_tracks() tracks = searchlib.SearchTracks(self.tracks, self.query)() + tracks = helpers.RemoveDuplicates(tracks)() SearchResults.tracks = tracks return tracks diff --git a/server/app/helpers.py b/server/app/helpers.py index 6cdd9dec..d0ebfdc6 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -1,7 +1,9 @@ """ This module contains mini functions for the server. """ +from dataclasses import dataclass import os +from pprint import pprint import threading from datetime import datetime from typing import Dict, Set @@ -51,26 +53,17 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]: return subfolders, files -def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]: - """ - Removes duplicates from a list. Returns a list without duplicates. - """ - song_num = 0 - while song_num < len(tracklist) - 1: - for index, song in enumerate(tracklist): - if ( - tracklist[song_num].title == song.title - and tracklist[song_num].album == song.album - and tracklist[song_num].artists == song.artists - and index != song_num - ): - tracklist.remove(song) +class RemoveDuplicates: + def __init__(self, tracklist: List[models.Track]) -> None: + self.tracklist = tracklist - song_num += 1 + def __call__(self) -> List[models.Track]: + uniq_hashes = set(t.uniq_hash for t in self.tracklist) + tracks = UseBisection(self.tracklist, "uniq_hash", uniq_hashes)() - return tracklist + return tracks def is_valid_file(filename: str) -> bool: diff --git a/server/app/lib/trackslib.py b/server/app/lib/trackslib.py index 34d037e2..f48847f2 100644 --- a/server/app/lib/trackslib.py +++ b/server/app/lib/trackslib.py @@ -2,11 +2,8 @@ This library contains all the functions related to tracks. """ import os -from pprint import pprint -from typing import List -from app import api, instances, models -from app.helpers import remove_duplicates +from app import instances from tqdm import tqdm @@ -23,51 +20,6 @@ def validate_tracks() -> None: instances.tracks_instance.remove_song_by_id(track["_id"]["$oid"]) -def get_album_tracks(albumname, artist): - """Returns api tracks matching an album""" - _tracks: List[models.Track] = [] - - for track in api.TRACKS: - if track.album == albumname and track.albumartist == artist: - _tracks.append(track) - - return remove_duplicates(_tracks) - - -def get_track_by_id(trackid: str) -> models.Track: - """Returns api track matching an id""" - for track in api.TRACKS: - try: - if track.trackid == trackid: - return track - except AttributeError: - print("AttributeError") - - -def find_track(tracks: list, hash: str) -> int | None: - """ - Finds an album by album title and artist. - """ - - left = 0 - right = len(tracks) - 1 - iter = 0 - - while left <= right: - iter += 1 - mid = (left + right) // 2 - - if tracks[mid]["albumhash"] == hash: - return mid - - if tracks[mid]["albumhash"] < hash: - left = mid + 1 - else: - right = mid - 1 - - return None - - def get_p_track(ptrack): return instances.tracks_instance.find_track_by_title_artists_album( ptrack["title"], ptrack["artists"], ptrack["album"] diff --git a/server/app/models.py b/server/app/models.py index 1c461294..915e225c 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -29,6 +29,7 @@ class Track: albumhash: str date: str image: str + uniq_hash: str def __init__(self, tags): self.trackid = tags["_id"]["$oid"] @@ -47,6 +48,17 @@ 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 + ) + + @staticmethod + def create_unique_hash(*args): + ill_chars = '/\\:*?"<>|#&' + + string = "".join(str(a) for a in args).replace(" ", "") + return "".join(string).strip(ill_chars).lower() + @dataclass(slots=True) class Artist: From 22ff52e86ef674f7050b1cc6af7a5937c7dc3697 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 26 Jun 2022 19:05:36 +0300 Subject: [PATCH 27/56] use album hash to resolve album page --- server/app/api/album.py | 12 ++++++------ src/components/contextMenu.vue | 6 +++--- src/components/shared/SongItem.vue | 3 +-- src/composables/pages/album.ts | 23 ++++++++--------------- src/contexts/track_context.ts | 11 +++++++---- src/interfaces.ts | 2 +- src/router/index.js | 9 +++------ src/stores/pages/album.ts | 13 ++++++------- src/views/AlbumView.vue | 5 +---- 9 files changed, 36 insertions(+), 48 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index eff42f82..64ed1da3 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -40,8 +40,7 @@ def get_albums(): def get_album(): """Returns all the tracks in the given album.""" data = request.get_json() - album, artist = data["album"], data["artist"] - albumhash = helpers.create_album_hash(album, artist) + albumhash = data["hash"] tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) tracks = [models.Track(t) for t in tracks] @@ -72,8 +71,10 @@ def get_album(): def get_album_bio(): """Returns the album bio for the given album.""" data = request.get_json() - fetch_bio = FetchAlbumBio(data["album"], data["albumartist"]) - bio = fetch_bio() + album_hash = data["hash"] + + album = instances.album_instance.find_album_by_hash(album_hash) + bio = FetchAlbumBio(album["title"], album["artist"])() if bio is None: return {"bio": "No bio found."}, 404 @@ -86,8 +87,7 @@ def get_albumartists(): """Returns a list of artists featured in a given album.""" data = request.get_json() - album, artist = data["album"], data["artist"] - albumhash = helpers.create_album_hash(album, artist) + albumhash = data["hash"] tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) tracks = [models.Track(t) for t in tracks] diff --git a/src/components/contextMenu.vue b/src/components/contextMenu.vue index c0127b1b..d84257d6 100644 --- a/src/components/contextMenu.vue +++ b/src/components/contextMenu.vue @@ -23,7 +23,7 @@ v-for="option in context.options" :key="option.label" :class="[{ critical: option.critical }, option.type]" - @click="option.action" + @click="option.action()" >
{{ option.label }}
@@ -32,7 +32,7 @@
@@ -177,7 +177,7 @@ const context = useContextStore(); .context-normalizedY { .context-item > .children { transform-origin: bottom right; - top: -.5rem; + top: -0.5rem; } } diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index 3cfe1b36..6104d5c2 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -42,8 +42,7 @@ :to="{ name: 'AlbumView', params: { - album: props.song.album, - artist: props.song.albumartist, + hash: props.song.albumhash, }, }" > diff --git a/src/composables/pages/album.ts b/src/composables/pages/album.ts index 3739f426..0118aea2 100644 --- a/src/composables/pages/album.ts +++ b/src/composables/pages/album.ts @@ -3,11 +3,7 @@ import { AlbumInfo, Track } from "../../interfaces"; import useAxios from "../useAxios"; import { NotifType, useNotifStore } from "@/stores/notification"; -const getAlbumData = async ( - album: string, - artist: string, - ToastStore: typeof useNotifStore -) => { +const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => { const url = state.settings.uri + "/album"; interface AlbumData { @@ -18,8 +14,7 @@ const getAlbumData = async ( const { data, status } = await useAxios({ url, props: { - album: album, - artist: artist, + hash: hash, }, }); @@ -27,8 +22,8 @@ const getAlbumData = async ( ToastStore().showNotification("Album not created yet!", NotifType.Error); return { info: { - album: album, - artist: artist, + album: "", + artist: "", }, tracks: [], }; @@ -37,12 +32,11 @@ const getAlbumData = async ( return data as AlbumData; }; -const getAlbumArtists = async (album: string, artist: string) => { +const getAlbumArtists = async (hash: string) => { const { data, error } = await useAxios({ url: state.settings.uri + "/album/artists", props: { - album: album, - artist: artist, + hash: hash, }, }); @@ -53,12 +47,11 @@ const getAlbumArtists = async (album: string, artist: string) => { return data.artists; }; -const getAlbumBio = async (album: string, albumartist: string) => { +const getAlbumBio = async (hash: string) => { const { data, status } = await useAxios({ url: state.settings.uri + "/album/bio", props: { - album: album, - albumartist: albumartist, + hash: hash, }, }); diff --git a/src/contexts/track_context.ts b/src/contexts/track_context.ts index d45f5a14..649c3d79 100644 --- a/src/contexts/track_context.ts +++ b/src/contexts/track_context.ts @@ -1,7 +1,10 @@ import { Playlist, Track } from "../interfaces"; import Router from "../router"; import { Option } from "../interfaces"; -import { getAllPlaylists, addTrackToPlaylist } from "../composables/pages/playlists"; +import { + getAllPlaylists, + addTrackToPlaylist, +} from "../composables/pages/playlists"; import useQueueStore from "../stores/queue"; import useModalStore from "../stores/modal"; @@ -15,7 +18,7 @@ import useModalStore from "../stores/modal"; export default async ( track: Track, modalStore: typeof useModalStore, - QueueStore: typeof useQueueStore, + QueueStore: typeof useQueueStore ): Promise => { const separator: Option = { type: "separator", @@ -79,7 +82,7 @@ export default async ( QueueStore().playTrackNext(track); }, icon: "add_to_queue", - } + }; const go_to_folder: Option = { label: "Go to Folder", @@ -114,7 +117,7 @@ export default async ( action: () => { Router.push({ name: "AlbumView", - params: { album: track.album, artist: track.albumartist }, + params: { hash: track.albumhash }, }); }, icon: "album", diff --git a/src/interfaces.ts b/src/interfaces.ts index c8dacfbe..c6bd34e8 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -45,7 +45,7 @@ export interface Artist { export interface Option { type?: string; label?: string; - action?: Function; + action?: () => void; children?: Option[] | false; icon?: string; critical?: Boolean; diff --git a/src/router/index.js b/src/router/index.js index c1315f28..38d5824a 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -62,17 +62,14 @@ const routes = [ component: AlbumsExplorer, }, { - path: "/albums/:album/:artist", + path: "/albums/:hash", name: "AlbumView", component: AlbumView, beforeEnter: async (to) => { state.loading.value = true; - await useAStore().fetchTracksAndArtists( - to.params.album, - to.params.artist - ); + await useAStore().fetchTracksAndArtists(to.params.hash); state.loading.value = false; - useAStore().fetchBio(to.params.album, to.params.artist); + useAStore().fetchBio(to.params.hash); }, }, { diff --git a/src/stores/pages/album.ts b/src/stores/pages/album.ts index f1af23ae..c471ca9b 100644 --- a/src/stores/pages/album.ts +++ b/src/stores/pages/album.ts @@ -21,9 +21,9 @@ export default defineStore("album", { * @param title title of the album * @param albumartist artist of the album */ - async fetchTracksAndArtists(title: string, albumartist: string) { - const tracks = await getAlbumTracks(title, albumartist, useNotifStore); - const artists = await getAlbumArtists(title, albumartist); + async fetchTracksAndArtists(hash: string) { + const tracks = await getAlbumTracks(hash, useNotifStore); + const artists = await getAlbumArtists(hash); this.tracks = tracks.tracks; this.info = tracks.info; @@ -31,12 +31,11 @@ export default defineStore("album", { }, /** * Fetches the album bio from the server - * @param title title of the album - * @param albumartist artist of the album + * @param {string} hash title of the album */ - fetchBio(title: string, albumartist: string) { + fetchBio(hash: string) { this.bio = null; - getAlbumBio(title, albumartist).then((bio) => { + getAlbumBio(hash).then((bio) => { this.bio = bio; }); }, diff --git a/src/views/AlbumView.vue b/src/views/AlbumView.vue index 0628fdc4..b2679bb6 100644 --- a/src/views/AlbumView.vue +++ b/src/views/AlbumView.vue @@ -29,10 +29,7 @@ import { onBeforeRouteUpdate } from "vue-router"; const album = useAStore(); onBeforeRouteUpdate(async (to) => { - await album.fetchTracksAndArtists( - to.params.album.toString(), - to.params.artist.toString() - ); + await album.fetchTracksAndArtists(to.params.hash.toString()); album.fetchBio(to.params.album.toString(), to.params.artist.toString()); }); From 4b522fd317c495a384dafa55b00abe5ed1f45401 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 26 Jun 2022 19:28:02 +0300 Subject: [PATCH 28/56] sort tracks by tracknumber in album page --- src/stores/pages/album.ts | 15 ++++++++++++--- src/views/AlbumView.vue | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/stores/pages/album.ts b/src/stores/pages/album.ts index c471ca9b..a9aac3dd 100644 --- a/src/stores/pages/album.ts +++ b/src/stores/pages/album.ts @@ -7,6 +7,16 @@ import { getAlbumBio, } from "../../composables/pages/album"; +function sortTracks(tracks: Track[]) { + return tracks.sort((a, b) => { + if (a.tracknumber && b.tracknumber) { + return a.tracknumber - b.tracknumber; + } + + return 0; + }); +} + export default defineStore("album", { state: () => ({ info: {}, @@ -18,14 +28,13 @@ export default defineStore("album", { /** * Fetches a single album information, artists and its tracks from the server * using the title and album-artist of the album. - * @param title title of the album - * @param albumartist artist of the album + * @param hash title of the album */ async fetchTracksAndArtists(hash: string) { const tracks = await getAlbumTracks(hash, useNotifStore); const artists = await getAlbumArtists(hash); - this.tracks = tracks.tracks; + this.tracks = sortTracks(tracks.tracks); this.info = tracks.info; this.artists = artists; }, diff --git a/src/views/AlbumView.vue b/src/views/AlbumView.vue index b2679bb6..8f789ef1 100644 --- a/src/views/AlbumView.vue +++ b/src/views/AlbumView.vue @@ -30,7 +30,7 @@ const album = useAStore(); onBeforeRouteUpdate(async (to) => { await album.fetchTracksAndArtists(to.params.hash.toString()); - album.fetchBio(to.params.album.toString(), to.params.artist.toString()); + album.fetchBio(to.params.hash.toString()); }); From 866f3278a570f433cfaf9b40122d9a1acd219f71 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 26 Jun 2022 19:37:48 +0300 Subject: [PATCH 29/56] show track number as index in album page --- src/components/FolderView/SongList.vue | 23 +++++++++++++++++++++-- src/interfaces.ts | 1 + src/views/AlbumView.vue | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/FolderView/SongList.vue b/src/components/FolderView/SongList.vue index e5fb9e65..9a5d95ac 100644 --- a/src/components/FolderView/SongList.vue +++ b/src/components/FolderView/SongList.vue @@ -10,10 +10,10 @@
(); let route = useRoute().name; @@ -68,6 +69,24 @@ function updateQueue(track: Track) { break; } } + +function getTracks() { + if (props.on_album_page) { + let tracks = props.tracks.map((track, index) => { + track.index = track.tracknumber; + return track; + }); + + return tracks; + } + + let tracks = props.tracks.map((track, index) => { + track.index = index + 1; + return track; + }); + + return tracks; +} \ No newline at end of file + diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 22008810..5c41c3e0 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -53,18 +53,17 @@ const imguri = paths.images.thumb; const nav = useNavStore(); useVisibility(albumheaderthing, nav.toggleShowPlay); - - From a23b6200eb8ef6fee1d733001df3213275082833 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 30 Jun 2022 14:01:48 +0300 Subject: [PATCH 34/56] add colors to album page header - add colors attribute to the album class - render color gradient in the album page --- server/app/db/mongodb/albums.py | 10 ++++++ server/app/functions.py | 20 ++++++----- server/app/lib/colorlib.py | 53 ++++++++++++++--------------- server/app/models.py | 8 +++++ src/components/AlbumView/Header.vue | 14 ++++++-- src/interfaces.ts | 6 ++-- src/views/AlbumView.vue | 1 - 7 files changed, 70 insertions(+), 42 deletions(-) diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 030c900c..4999c146 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -2,6 +2,7 @@ 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 @@ -59,3 +60,12 @@ class Albums(MongoAlbums): """ album = self.collection.find_one({"hash": hash}) return convert_one(album) + + def set_album_colors(self, colors: List[str], album_id: str) -> None: + """ + Sets the colors for an album. + """ + self.collection.update_one( + {"_id": ObjectId(album_id)}, + {"$set": {"colors": colors}}, + ) diff --git a/server/app/functions.py b/server/app/functions.py index e62128a9..4bccdd64 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -15,6 +15,7 @@ from app.lib.albumslib import ValidateAlbumThumbs from app.lib import trackslib from app.lib.populate import CreateAlbums, Populate from app.lib.playlistlib import ValidatePlaylistThumbs +from app.lib.colorlib import ProcessAlbumColors @helpers.background @@ -23,17 +24,20 @@ def run_checks(): Checks for new songs every 5 minutes. """ - # while True: - trackslib.validate_tracks() + while True: + trackslib.validate_tracks() - Populate() - CreateAlbums() + Populate() + CreateAlbums() - if helpers.Ping()(): - CheckArtistImages()() + if helpers.Ping()(): + CheckArtistImages()() - ValidateAlbumThumbs() - ValidatePlaylistThumbs() + ValidateAlbumThumbs() + ValidatePlaylistThumbs() + ProcessAlbumColors() + + time.sleep(300) @helpers.background diff --git a/server/app/lib/colorlib.py b/server/app/lib/colorlib.py index 5882461f..e410d870 100644 --- a/server/app/lib/colorlib.py +++ b/server/app/lib/colorlib.py @@ -1,17 +1,19 @@ -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 -def get_image_colors(image) -> list: +from app.helpers import Get +from app import settings +from app.models import Album +from app.logger import get_logger + +log = get_logger() + + +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 +26,25 @@ 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: + def __init__(self) -> None: + log.info("Processing album colors") + all_albums = Get.get_all_albums() - track_colors = get_image_colors(img) + all_albums = [a for a in all_albums if len(a.colors) == 0] - tc_dict = { - "filepath": filepath, - "colors": track_colors, - } + for a in all_albums: + self.process_color(a) - instances.track_color_instance.insert_track_color(tc_dict) + log.info("Processing album colors ... ✅") + @staticmethod + def process_color(album: Album): + img = settings.THUMBS_PATH + "/" + album.image -def save_t_colors(): - _bar = Bar("Processing image colors", max=len(api.DB_TRACKS)) + colors = get_image_colors(img) - for track in api.DB_TRACKS: - filepath = track["filepath"] - album_art = return_album_art(filepath) + if len(colors) > 0: + instances.album_instance.set_album_colors(colors, album.albumid) - if album_art is not None: - img = Image.open(BytesIO(album_art)) - save_track_colors(img, filepath) - - _bar.next() - - _bar.finish() + return colors diff --git a/server/app/models.py b/server/app/models.py index 915e225c..3cb4d42b 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -82,6 +82,7 @@ class Album: Creates an album object """ + albumid: str title: str artist: str hash: str @@ -92,14 +93,21 @@ 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.albumid = tags["_id"]["$oid"] self.title = tags["title"] self.artist = tags["artist"] self.date = tags["date"] 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/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 5c41c3e0..7a016d2d 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -1,6 +1,13 @@ From 5acb8cb84d3c94e5e599fa643af32740628588c9 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 30 Jun 2022 17:35:46 +0300 Subject: [PATCH 35/56] check if album colors have contrast - remove albumid field from album class - set accent color to $red --- server/app/api/album.py | 12 +++++- server/app/db/mongodb/albums.py | 4 +- server/app/lib/colorlib.py | 2 +- server/app/models.py | 2 - src/assets/css/_variables.scss | 2 +- src/components/AlbumView/Header.vue | 50 +++++++++++------------ src/components/LeftSidebar/Navigation.vue | 2 +- src/components/shared/PlayBtnRect.vue | 5 ++- src/composables/pages/album.ts | 1 + 9 files changed, 42 insertions(+), 38 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index 64ed1da3..f4ec080e 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -41,20 +41,28 @@ 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 = albumslib.get_album_duration(tracks) + except AttributeError: + album.duration = 0 if ( album.count == 1 diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 4999c146..eeceb6d0 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -61,11 +61,11 @@ class Albums(MongoAlbums): album = self.collection.find_one({"hash": hash}) return convert_one(album) - def set_album_colors(self, colors: List[str], album_id: str) -> None: + def set_album_colors(self, colors: List[str], hash: str) -> None: """ Sets the colors for an album. """ self.collection.update_one( - {"_id": ObjectId(album_id)}, + {"hash": hash}, {"$set": {"colors": colors}}, ) diff --git a/server/app/lib/colorlib.py b/server/app/lib/colorlib.py index e410d870..38f8e481 100644 --- a/server/app/lib/colorlib.py +++ b/server/app/lib/colorlib.py @@ -45,6 +45,6 @@ class ProcessAlbumColors: colors = get_image_colors(img) if len(colors) > 0: - instances.album_instance.set_album_colors(colors, album.albumid) + instances.album_instance.set_album_colors(colors, album.hash) return colors diff --git a/server/app/models.py b/server/app/models.py index 3cb4d42b..fc24ed12 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -82,7 +82,6 @@ class Album: Creates an album object """ - albumid: str title: str artist: str hash: str @@ -96,7 +95,6 @@ class Album: colors: List[str] = field(default_factory=list) def __init__(self, tags): - self.albumid = tags["_id"]["$oid"] self.title = tags["title"] self.artist = tags["artist"] self.date = tags["date"] 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/Header.vue b/src/components/AlbumView/Header.vue index 7a016d2d..c5b7ea58 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -4,7 +4,7 @@ class="a-header rounded" :style="{ backgroundImage: `linear-gradient( - 37deg, ${album.colors[0]}, ${album.colors[3]} + 37deg, ${props.album.colors[0]}, ${props.album.colors[3]} )`, }" > @@ -17,7 +17,7 @@ v-motion-slide-from-left >
-
+
Soundtrack @@ -46,14 +46,14 @@ import useVisibility from "@/composables/useVisibility"; import useNavStore from "@/stores/nav"; import useAlbumStore from "@/stores/pages/album"; -import { ref } from "vue"; +import { reactive, ref } from "vue"; import { playSources } from "../../composables/enums"; import { formatSeconds } from "../../composables/perks"; import { paths } from "../../config"; import { AlbumInfo } from "../../interfaces"; import PlayBtnRect from "../shared/PlayBtnRect.vue"; -defineProps<{ +const props = defineProps<{ album: AlbumInfo; }>(); @@ -61,7 +61,22 @@ const albumheaderthing = ref(null); const imguri = paths.images.thumb; const nav = useNavStore(); +const colors = reactive({ + color1: props.album.colors[0], + color2: props.album.colors[3], +}); + useVisibility(albumheaderthing, nav.toggleShowPlay); + +function isLight(rgb: string = props.album.colors[0]) { + if (rgb == null || undefined) return false; + + const [r, g, b] = rgb.match(/\d+/g)!.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + console.log(brightness); + + return brightness > 150; +} diff --git a/src/components/shared/PlayBtnRect.vue b/src/components/shared/PlayBtnRect.vue index 0d5f1b4f..0f9f6000 100644 --- a/src/components/shared/PlayBtnRect.vue +++ b/src/components/shared/PlayBtnRect.vue @@ -11,7 +11,7 @@ diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index d7b8bc3b..d904db8c 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -157,6 +157,10 @@ function emitUpdate(track: Track) { max-width: max-content; cursor: pointer; + &:hover { + text-decoration: underline; + } + @include tablet-portrait { display: none; } From 77a5d2b7c24c0f17409ba3a503b5b22d8b4b5c9e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 2 Jul 2022 08:21:10 +0300 Subject: [PATCH 37/56] send processing album colors to a background thread - use white color as default album page play button color - return 404 if album is None on get_album_bio() --- server/app/api/album.py | 7 +++- server/app/functions.py | 23 ++++++++---- server/app/lib/populate.py | 2 +- src/components/AlbumView/Header.vue | 13 ++----- .../PlaylistView/FeaturedArtists.vue | 37 +++++-------------- src/components/contextMenu.vue | 4 +- src/views/AlbumView.vue | 26 ++++--------- 7 files changed, 44 insertions(+), 68 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index f4ec080e..1a616a70 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -80,12 +80,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/functions.py b/server/app/functions.py index 4bccdd64..3e8ab7e8 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -16,13 +16,16 @@ from app.lib import trackslib from app.lib.populate import CreateAlbums, Populate from app.lib.playlistlib import ValidatePlaylistThumbs from app.lib.colorlib import ProcessAlbumColors +from app.logger import get_logger +log = get_logger() @helpers.background def run_checks(): """ Checks for new songs every 5 minutes. """ + ValidateAlbumThumbs() while True: trackslib.validate_tracks() @@ -33,9 +36,12 @@ def run_checks(): if helpers.Ping()(): CheckArtistImages()() - ValidateAlbumThumbs() + @helpers.background + def process_album_colors(): + ProcessAlbumColors() + ValidatePlaylistThumbs() - ProcessAlbumColors() + process_album_colors() time.sleep(300) @@ -80,14 +86,17 @@ 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): @@ -116,21 +125,21 @@ class CheckArtistImages: ) 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 + [print(i) for i in iter] print("Done fetching images") diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 417e5e7a..81c8774f 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -144,7 +144,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/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 270a4899..91975789 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -78,16 +78,14 @@ function isLight(rgb: string = props.album.colors[0]) { const [r, g, b] = rgb.match(/\d+/g)!.map(Number); const brightness = (r * 299 + g * 587 + b * 114) / 1000; - return brightness > 170; + return brightness > 150; } function getButtonColor(colors: string[] = props.album.colors) { const base_color = colors[0]; - console.log(colors.length); - if (colors.length === 0) return { color: "#000" }; + if (colors.length === 0) return { color: "#fff", isDark: true }; for (let i = 0; i < colors.length; i++) { - // if (isLight(colors[i])) break; if (theyContrast(base_color, colors[i])) { return { color: colors[i], @@ -97,7 +95,8 @@ function getButtonColor(colors: string[] = props.album.colors) { } return { - color: "#000", + color: "#fff", + isDark: true, }; } @@ -124,10 +123,6 @@ function rgbToArray(rgb: string) { function theyContrast(color1: string, color2: string) { return contrast(rgbToArray(color1), rgbToArray(color2)) > 3; } - -console.log( - contrast(rgbToArray(props.album.colors[0]), rgbToArray(props.album.colors[3])) -); From 14182e78cda99ac90a960bdec80986cffe9862fd Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 2 Jul 2022 11:02:29 +0300 Subject: [PATCH 41/56] add a bottom padding when the bottom area is expanded - attach a ''resetBottomPadding" event to the album header component - add function documentation to the header and albumview components. --- src/components/AlbumView/Header.vue | 73 ++++++++++++++++++++++++----- src/components/shared/SongItem.vue | 1 - src/views/AlbumView.vue | 31 +++++++++--- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 30bc63c5..26a979d9 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -61,18 +61,37 @@ const props = defineProps<{ album: AlbumInfo; }>(); +const emit = defineEmits<{ + (event: "resetBottomPadding"): void; +}>(); + const albumheaderthing = ref(null); const imguri = paths.images.thumb; const nav = useNavStore(); -const colors = reactive({ - color1: props.album.colors[0], - color2: props.album.colors[3], -}); +/** + * Calls the `toggleShowPlay` method which toggles the play button in the nav. + * Emits the `resetBottomPadding` event to reset the album page content bottom padding. + * + * @param {boolean} state the new visibility state of the album page header. + */ +function handleVisibilityState(state: boolean) { + if (state) { + emit("resetBottomPadding"); + } -useVisibility(albumheaderthing, nav.toggleShowPlay); + nav.toggleShowPlay(state); +} -function isLight(rgb: string = props.album.colors[0]) { +useVisibility(albumheaderthing, handleVisibilityState); + +/** + * Returns `true` if the rgb color passed is light. + * + * @param {string} rgb The color to check whether it's light or dark. + * @returns {boolean} true if color is light, false if color is dark. + */ +function isLight(rgb: string = props.album.colors[0]): boolean { if (rgb == null || undefined) return false; const [r, g, b] = rgb.match(/\d+/g)!.map(Number); @@ -81,7 +100,18 @@ function isLight(rgb: string = props.album.colors[0]) { return brightness > 170; } -function getButtonColor(colors: string[] = props.album.colors) { +interface BtnColor { + color: string; + isDark: boolean; +} + +/** + * Returns the first contrasting color in the album colors. + * + * @param {string[]} colors The album colors to choose from. + * @returns {BtnColor} A color to use as the play button background + */ +function getButtonColor(colors: string[] = props.album.colors): BtnColor { const base_color = colors[0]; if (colors.length === 0) return { color: "#fff", isDark: true }; @@ -100,6 +130,12 @@ function getButtonColor(colors: string[] = props.album.colors) { }; } +/** + * Returns the luminance of a color. + * @param r The red value of the color. + * @param g The green value of the color. + * @param b The blue value of the color. + */ function luminance(r: any, g: any, b: any) { let a = [r, g, b].map(function (v) { v /= 255; @@ -108,18 +144,33 @@ function luminance(r: any, g: any, b: any) { return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } -function contrast(rgb1: number[], rgb2: number[]) { - let lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]); - let lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]); +/** + * Returns a contrast ratio of `color1`:`color2` + * @param {string} color1 The first color + * @param {string} color2 The second color + */ +function contrast(color1: number[], color2: number[]): number { + let lum1 = luminance(color1[0], color1[1], color1[2]); + let lum2 = luminance(color2[0], color2[1], color2[2]); let brightest = Math.max(lum1, lum2); let darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); } -function rgbToArray(rgb: string) { +/** + * Converts a rgb color string to an array of the form: `[r, g, b]` + * @param rgb The color to convert + * @returns {number[]} The array representation of the color + */ +function rgbToArray(rgb: string): number[] { return rgb.match(/\d+/g)!.map(Number); } +/** + * Returns true if the `color2` contrast with `color1`. + * @param color1 The first color + * @param color2 The second color + */ function theyContrast(color1: string, color2: string) { return contrast(rgbToArray(color1), rgbToArray(color2)) > 3; } diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index d904db8c..8d2d63a7 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -127,7 +127,6 @@ function emitUpdate(track: Track) { text-align: left; gap: $small; user-select: none; - -moz-user-select: none; @include tablet-landscape { grid-template-columns: 1.5rem 1.5fr 1fr 1fr 2.5rem; diff --git a/src/views/AlbumView.vue b/src/views/AlbumView.vue index 157bcc4e..d21ffdd0 100644 --- a/src/views/AlbumView.vue +++ b/src/views/AlbumView.vue @@ -1,8 +1,8 @@