diff --git a/server/app/helpers.py b/server/app/helpers.py index 7d46fb2e..0d74865e 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -1,5 +1,5 @@ """ -This module contains mimi functions for the server. +This module contains mini functions for the server. """ import datetime import os @@ -27,9 +27,7 @@ def background(func): return background_func -def run_fast_scandir(__dir: str, - ext: list, - full=False) -> Dict[List[str], List[str]]: +def run_fast_scandir(__dir: str, ext: list, full=False) -> Dict[List[str], List[str]]: """ Scans a directory for files with a specific extension. Returns a list of files and folders in the directory. """ @@ -62,10 +60,12 @@ def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]: 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): + 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) song_num += 1 @@ -108,8 +108,7 @@ def check_artist_image(image: str) -> str: """ img_name = image.replace("/", "::") + ".webp" - if not os.path.exists(os.path.join(app_dir, "images", "artists", - img_name)): + if not os.path.exists(os.path.join(app_dir, "images", "artists", img_name)): return use_memoji() else: return img_name @@ -125,3 +124,11 @@ class Timer: def stop(self): self.end = time.time() print(str(datetime.timedelta(seconds=round(self.end - self.begin)))) + + +def create_album_hash(title: str, artist: str) -> str: + """ + Creates a simple hash for an album + """ + return (title + artist).replace(" ", "").lower() + diff --git a/server/app/imgserver/__init__.py b/server/app/imgserver/__init__.py index 96999d00..cc5057b6 100644 --- a/server/app/imgserver/__init__.py +++ b/server/app/imgserver/__init__.py @@ -37,7 +37,6 @@ def send_thumbnail(imgpath: str): @app.route("/a/") def send_artist_image(imgpath: str): - print(ARTIST_PATH) fpath = join(ARTIST_PATH, imgpath) exists = path.exists(fpath) diff --git a/server/app/lib/albumslib.py b/server/app/lib/albumslib.py index 3c673486..f5c0f39e 100644 --- a/server/app/lib/albumslib.py +++ b/server/app/lib/albumslib.py @@ -1,20 +1,20 @@ """ This library contains all the functions related to albums. """ +from copy import deepcopy import random import urllib -from pprint import pprint from typing import List from app import api -from app import functions from app import instances from app import models -from app import settings from app.lib import taglib from app.lib import trackslib from progress.bar import Bar +from app import helpers + def get_all_albums() -> List[models.Album]: """ @@ -23,6 +23,7 @@ def get_all_albums() -> List[models.Album]: print("Getting all albums...") albums: List[models.Album] = [] + db_albums = instances.album_instance.get_all_albums() _bar = Bar("Creating albums", max=len(db_albums)) @@ -44,7 +45,7 @@ def create_everything() -> List[models.Track]: albums: list[models.Album] = get_all_albums() api.ALBUMS = albums - api.ALBUMS.sort(key=lambda x: x.title) + api.ALBUMS.sort(key=lambda x: x.hash) tracks = trackslib.create_all_tracks() @@ -57,6 +58,7 @@ def find_album(albumtitle: str, artist: str) -> int or None: """ Finds an album by album title and artist. """ + left = 0 right = len(api.ALBUMS) - 1 iter = 0 @@ -64,12 +66,15 @@ def find_album(albumtitle: str, artist: str) -> int or None: while left <= right: iter += 1 mid = (left + right) // 2 + hash = helpers.create_album_hash(albumtitle, artist) - if api.ALBUMS[mid].title == albumtitle and api.ALBUMS[ - mid].artist == artist: - return mid + try: + if api.ALBUMS[mid].hash == hash: + return mid + except: + print(api.ALBUMS[mid]) - if api.ALBUMS[mid].title < albumtitle: + if api.ALBUMS[mid].hash < hash: left = mid + 1 else: right = mid - 1 @@ -125,18 +130,33 @@ def get_album_image(album: list) -> str: return use_defaults() +class GetAlbumTracks: + """ + Finds all the tracks that match a specific album, given the album title + and album artist. + """ + + def __init__(self, album: str, artist: str) -> None: + self.hash = helpers.create_album_hash(album, artist) + self.tracks = api.DB_TRACKS + self.tracks.sort(key=lambda x: x["albumhash"]) + + def find_tracks(self): + tracks = [] + index = trackslib.find_track(self.tracks, self.hash) + + while index is not None: + track = self.tracks[index] + tracks.append(track) + self.tracks.remove(track) + index = trackslib.find_track(self.tracks, self.hash) + + api.DB_TRACKS.extend(tracks) + return tracks + + def get_album_tracks(album: str, artist: str) -> List: - tracks = [] - - for track in api.DB_TRACKS: - try: - if track["album"] == album and track["albumartist"] == artist: - tracks.append(track) - except TypeError: - pprint(track, indent=4) - print(album, artist) - - return tracks + return GetAlbumTracks(album, artist).find_tracks() def create_album(track) -> models.Album: @@ -144,20 +164,21 @@ def create_album(track) -> models.Album: Generates and returns an album object from a track object. """ album = { - "album": track["album"], + "title": track["album"], "artist": track["albumartist"], } - album_tracks = get_album_tracks(album["album"], album["artist"]) + album_tracks = get_album_tracks(album["title"], album["artist"]) album["date"] = album_tracks[0]["date"] album["artistimage"] = urllib.parse.quote_plus( - album_tracks[0]["albumartist"] + ".webp") + album_tracks[0]["albumartist"] + ".webp" + ) album["image"] = get_album_image(album_tracks) - return models.Album(album) + return album def search_albums_by_name(query: str) -> List[models.Album]: diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 4a929471..2c460285 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -3,7 +3,7 @@ from os import path from app import api from app import settings -from app.helpers import run_fast_scandir +from app.helpers import create_album_hash, run_fast_scandir from app.instances import album_instance from app.instances import tracks_instance from app.lib import folderslib @@ -11,7 +11,7 @@ from app.lib.albumslib import create_album from app.lib.albumslib import find_album from app.lib.taglib import get_tags from app.logger import Log -from app.models import Track +from app.models import Album, Track from progress.bar import Bar @@ -44,6 +44,7 @@ class Populate: self.tag_files() self.create_pre_albums() self.create_albums() + api.ALBUMS.sort(key=lambda x: x.hash) self.create_tracks() self.create_folders() @@ -73,6 +74,9 @@ class Populate: self.folders.add(folder) if tags is not None: + tags["albumhash"] = create_album_hash( + tags["album"], tags["albumartist"] + ) self.tagged_tracks.append(tags) api.DB_TRACKS.append(tags) @@ -108,16 +112,17 @@ class Populate: if index is None: try: track = [ - track for track in self.tagged_tracks + track + for track in self.tagged_tracks if track["album"] == album["title"] and track["albumartist"] == album["artist"] ][0] album = create_album(track) - api.ALBUMS.append(album) + api.ALBUMS.append(Album(album)) self.albums.append(album) - album_instance.insert_album(asdict(album)) + album_instance.insert_album(album) except IndexError: print("😠\n") @@ -128,8 +133,9 @@ class Populate: bar.next() bar.finish() - Log(f"{exist_count} of {len(self.pre_albums)} albums were already in the database" - ) + Log( + f"{exist_count} of {len(self.pre_albums)} albums were already in the database" + ) def create_tracks(self): """ @@ -137,13 +143,16 @@ class Populate: """ bar = Bar("Creating tracks", max=len(self.tagged_tracks)) failed_count = 0 + for track in self.tagged_tracks: try: album_index = find_album(track["album"], track["albumartist"]) album = api.ALBUMS[album_index] track["image"] = album.image + upsert_id = tracks_instance.insert_song(track) track["_id"] = {"$oid": str(upsert_id)} + api.TRACKS.append(Track(track)) except: # 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. @@ -151,19 +160,21 @@ class Populate: bar.next() bar.finish() - Log(f"Added {len(self.tagged_tracks) - failed_count} of {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums" - ) + Log( + f"Added {len(self.tagged_tracks) - failed_count} of {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums" + ) def create_folders(self): """ Creates the folder objects for all the tracks. """ bar = Bar("Creating folders", max=len(self.folders)) - old_f_count = len(api.FOLDERS) for folder in self.folders: api.VALID_FOLDERS.add(folder) + fff = folderslib.create_folder(folder) api.FOLDERS.append(fff) + bar.next() bar.finish() diff --git a/server/app/lib/trackslib.py b/server/app/lib/trackslib.py index 1aa25373..d6d7897a 100644 --- a/server/app/lib/trackslib.py +++ b/server/app/lib/trackslib.py @@ -26,6 +26,7 @@ def create_all_tracks() -> List[models.Track]: except FileNotFoundError: instances.tracks_instance.remove_song_by_id(track["_id"]["$oid"]) api.DB_TRACKS.remove(track) + try: tracks.append(models.Track(track)) except KeyError: @@ -54,3 +55,27 @@ def get_track_by_id(trackid: str) -> models.Track: for track in api.TRACKS: if track.trackid == trackid: return track + + +def find_track(tracks: list, hash: str) -> int or 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 diff --git a/server/app/lib/watchdoge.py b/server/app/lib/watchdoge.py index 0f0568ed..c6970fbd 100644 --- a/server/app/lib/watchdoge.py +++ b/server/app/lib/watchdoge.py @@ -8,11 +8,13 @@ from app import api from app import instances from app import models from app.lib import folderslib -from app.lib.albumslib import create_album +from app.lib.albumslib import create_album, find_album from app.lib.taglib import get_tags from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer +from app.helpers import create_album_hash + class OnMyWatch: """ @@ -48,14 +50,24 @@ def add_track(filepath: str) -> None: tags = get_tags(filepath) if tags is not None: - instances.tracks_instance.insert_song(tags) - tags = instances.tracks_instance.get_song_by_path(tags["filepath"]) - + tags["albumhash"] = create_album_hash(tags["album"], tags["albumartist"]) api.DB_TRACKS.append(tags) - album = create_album(tags) - api.ALBUMS.append(album) + + albumindex = find_album(tags["album"], tags["albumartist"]) + + if albumindex is not None: + album = api.ALBUMS[albumindex] + else: + album_data = create_album(tags) + instances.album_instance.insert_album(album_data) + + album = models.Album(album_data) + api.ALBUMS.append(album) tags["image"] = album.image + upsert_id = instances.tracks_instance.insert_song(tags) + tags["_id"] = {"$oid": str(upsert_id)} + api.TRACKS.append(models.Track(tags)) folder = tags["folder"] @@ -74,8 +86,7 @@ def remove_track(filepath: str) -> None: fpath = filepath.replace(fname, "") try: - trackid = instances.tracks_instance.get_song_by_path( - filepath)["_id"]["$oid"] + trackid = instances.tracks_instance.get_song_by_path(filepath)["_id"]["$oid"] except TypeError: print(f"💙 Watchdog Error: Error removing track {filepath} TypeError") return diff --git a/server/app/models.py b/server/app/models.py index 4717ad60..7b3d4a97 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -3,12 +3,11 @@ Contains all the models for objects generation and typing. """ from dataclasses import dataclass from dataclasses import field -from datetime import date from typing import List from app import api -from app import settings from app.exceptions import TrackExistsInPlaylist +from app import helpers @dataclass @@ -30,6 +29,7 @@ class Track: image: str tracknumber: int discnumber: int + albumhash: str def __init__(self, tags): @@ -46,6 +46,7 @@ class Track: self.image = tags["image"] self.tracknumber = tags["tracknumber"] self.discnumber = tags["discnumber"] + self.albumhash = tags['albumhash'] @dataclass @@ -59,22 +60,26 @@ class Album: date: int artistimage: str image: str + hash: str count: int = 0 duration: int = 0 def __init__(self, tags): - self.title = tags["album"] + self.title = tags["title"] self.artist = tags["artist"] self.date = tags["date"] self.artistimage = tags["artistimage"] self.image = tags["image"] + self.hash = helpers.create_album_hash(self.title, self.artist) 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): + if ( + track.title == ptrack["title"] + and track.artists == ptrack["artists"] + and ptrack["album"] == track.album + ): return track diff --git a/server/app/prep.py b/server/app/prep.py index db1d3b5d..55831a9b 100644 --- a/server/app/prep.py +++ b/server/app/prep.py @@ -13,7 +13,6 @@ def create_config_dir() -> None: _home_dir = os.path.expanduser("~") config_folder = os.path.join(_home_dir, settings.CONFIG_FOLDER) - print(config_folder) dirs = [ "",