diff --git a/app/api/album.py b/app/api/album.py index 2aebdbb..cd752c0 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -7,7 +7,7 @@ from dataclasses import asdict from flask import Blueprint, request -from app.db.sqlite.albums import SQLiteAlbumMethods as adb +from app.db.sqlite.albumcolors import SQLiteAlbumMethods as adb from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as lastfmdb from app.models import FavType, Track diff --git a/app/api/colors.py b/app/api/colors.py index 1a1b3ad..6513f90 100644 --- a/app/api/colors.py +++ b/app/api/colors.py @@ -8,11 +8,9 @@ api = Blueprint("colors", __name__, url_prefix="/colors") def get_album_color(albumhash: str): album = Store.get_album_by_hash(albumhash) - if len(album.colors) > 0: - return { - "color": album.colors[0] - } + msg = {"color": ""} - return { - "color": "" - } + if album is None or len(album.colors) == 0: + return msg, 404 + + return {"color": album.colors[0]} diff --git a/app/api/track.py b/app/api/track.py index 1377f9e..fe65897 100644 --- a/app/api/track.py +++ b/app/api/track.py @@ -49,7 +49,7 @@ def send_track_file(trackhash: str): try: return send_file(track.filepath, mimetype=audio_type) - except FileNotFoundError: + except (FileNotFoundError, OSError) as e: return msg, 404 return msg, 404 diff --git a/app/db/sqlite/albums.py b/app/db/sqlite/albumcolors.py similarity index 70% rename from app/db/sqlite/albums.py rename to app/db/sqlite/albumcolors.py index e3bdd41..2e80492 100644 --- a/app/db/sqlite/albums.py +++ b/app/db/sqlite/albumcolors.py @@ -44,3 +44,23 @@ class SQLiteAlbumMethods: return tuples_to_albums(albums) return [] + + @staticmethod + def exists(albumhash: str, cur: Cursor = None): + """ + Checks if an album exists in the database. + """ + + sql = "SELECT COUNT(1) FROM albums WHERE albumhash = ?" + + def _exists(cur: Cursor): + cur.execute(sql, (albumhash,)) + count = cur.fetchone()[0] + + return count != 0 + + if cur: + return _exists(cur) + + with SQLiteManager() as cur: + return _exists(cur) diff --git a/app/db/sqlite/artists.py b/app/db/sqlite/artistcolors.py similarity index 69% rename from app/db/sqlite/artists.py rename to app/db/sqlite/artistcolors.py index 9ad8c5c..8268ea3 100644 --- a/app/db/sqlite/artists.py +++ b/app/db/sqlite/artistcolors.py @@ -43,3 +43,22 @@ class SQLiteArtistMethods: for artist in cur_.fetchall(): yield artist + + @staticmethod + def exists(artisthash: str, cur: Cursor = None): + """ + Checks if an artist exists in the database. + """ + sql = "SELECT COUNT(1) FROM artists WHERE artisthash = ?" + + def _exists(cur: Cursor): + cur.execute(sql, (artisthash,)) + count = cur.fetchone()[0] + + return count != 0 + + if cur: + return _exists(cur) + + with SQLiteManager() as cur: + return _exists(cur) diff --git a/app/lib/albumslib.py b/app/lib/albumslib.py index e154c93..d1b95da 100644 --- a/app/lib/albumslib.py +++ b/app/lib/albumslib.py @@ -1,3 +1,22 @@ """ Contains methods relating to albums. -""" \ No newline at end of file +""" + +from tqdm import tqdm +from app.store.albums import AlbumStore +from app.store.tracks import TrackStore + + +def validate_albums(): + """ + Removes albums that have no tracks. + + Probably albums that were added from incompletely written files. + """ + + album_hashes = {t.albumhash for t in TrackStore.tracks} + albums = AlbumStore.albums + + for album in tqdm(albums, desc="Validating albums"): + if album.albumhash not in album_hashes: + AlbumStore.remove_album(album) diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index dd0eca3..2ebb3e7 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -9,8 +9,8 @@ import colorgram from tqdm import tqdm from app import settings -from app.db.sqlite.albums import SQLiteAlbumMethods as db -from app.db.sqlite.artists import SQLiteArtistMethods as adb +from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb +from app.db.sqlite.artistcolors import SQLiteArtistMethods as adb from app.db.sqlite.utils import SQLiteManager from app.store.artists import ArtistStore @@ -58,11 +58,8 @@ class ProcessAlbumColors: with SQLiteManager() as cur: try: for album in tqdm(albums, desc="Processing missing album colors"): - sql = "SELECT COUNT(1) FROM albums WHERE albumhash = ?" - cur.execute(sql, (album.albumhash,)) - count = cur.fetchone()[0] - - if count != 0: + exists = aldb.exists(album.albumhash, cur=cur) + if exists: continue colors = process_color(album.albumhash) @@ -72,7 +69,7 @@ class ProcessAlbumColors: album.set_colors(colors) color_str = json.dumps(colors) - db.insert_one_album(cur, album.albumhash, color_str) + aldb.insert_one_album(cur, album.albumhash, color_str) finally: cur.close() @@ -90,12 +87,9 @@ class ProcessArtistColors: for artist in tqdm( all_artists, desc="Processing missing artist colors" ): - sql = "SELECT COUNT(1) FROM artists WHERE artisthash = ?" + exists = adb.exists(artist.artisthash, cur=cur) - cur.execute(sql, (artist.artisthash,)) - count = cur.fetchone()[0] - - if count != 0: + if exists: continue colors = process_color(artist.artisthash, is_album=False) diff --git a/app/lib/populate.py b/app/lib/populate.py index b193aa2..3ca2094 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -9,10 +9,10 @@ from tqdm import tqdm from app import settings from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb -from app.db.sqlite.lastfm.similar_artists import \ - SQLiteLastFMSimilarArtists as lastfmdb +from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as lastfmdb from app.db.sqlite.settings import SettingsSQLMethods as sdb from app.db.sqlite.tracks import SQLiteTrackMethods +from app.lib.albumslib import validate_albums from app.lib.artistlib import CheckArtistImages from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors from app.lib.taglib import extract_thumb, get_tags @@ -51,6 +51,8 @@ class Populate: POPULATE_KEY = key validate_tracks() + validate_albums() + tracks = get_all_tracks() dirs_to_scan = sdb.get_root_dirs() @@ -122,8 +124,7 @@ class Populate: if track.last_mod == os.path.getmtime(track.filepath): unmodified.add(track.filepath) continue - except FileNotFoundError: - print(f"File not found: {track.filepath}") + except (FileNotFoundError, OSError) as e: TrackStore.remove_track_obj(track) remove_tracks_by_filepaths(track.filepath) diff --git a/app/lib/taglib.py b/app/lib/taglib.py index ecae79c..b63a689 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -78,7 +78,11 @@ def extract_date(date_str: str | None) -> int | None: def get_tags(filepath: str): filetype = filepath.split(".")[-1] filename = (filepath.split("/")[-1]).replace(f".{filetype}", "") - last_mod = os.path.getmtime(filepath) + + try: + last_mod = os.path.getmtime(filepath) + except FileNotFoundError: + return None try: tags = TinyTag.get(filepath) diff --git a/app/lib/trackslib.py b/app/lib/trackslib.py index 8883e50..6bb1a30 100644 --- a/app/lib/trackslib.py +++ b/app/lib/trackslib.py @@ -11,9 +11,9 @@ from app.store.tracks import TrackStore def validate_tracks() -> None: """ - Gets all songs under the ~/ directory. + Removes track records whose files no longer exist. """ - for track in tqdm(TrackStore.tracks, desc="Checking for deleted tracks"): + for track in tqdm(TrackStore.tracks, desc="Validating tracks"): if not os.path.exists(track.filepath): TrackStore.remove_track_obj(track) tdb.remove_tracks_by_filepaths(track.filepath) diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index 60d8598..21fa2f9 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -1,6 +1,7 @@ """ This library contains the classes and functions related to the watchdog file watcher. """ +import json import os import sqlite3 import time @@ -9,10 +10,14 @@ from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer from app import settings + from app.db.sqlite.settings import SettingsSQLMethods as sdb from app.db.sqlite.tracks import SQLiteManager from app.db.sqlite.tracks import SQLiteTrackMethods as db -from app.lib.taglib import get_tags +from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb + +from app.lib.colorlib import process_color +from app.lib.taglib import extract_thumb, get_tags from app.logger import log from app.models import Artist, Track from app.store.albums import AlbumStore @@ -123,6 +128,22 @@ class Watcher: self.run() +def handle_colors(cur: sqlite3.Cursor, albumhash: str): + exists = aldb.exists(albumhash, cur) + + if exists: + return + + colors = process_color(albumhash, is_album=True) + + if colors is None: + return + + aldb.insert_one_album(cur=cur, albumhash=albumhash, colors=json.dumps(colors)) + + return colors + + def add_track(filepath: str) -> None: """ Processes the audio tags for a given file ands add them to the database and store. @@ -138,14 +159,23 @@ def add_track(filepath: str) -> None: if tags is None or tags["bitrate"] == 0 or tags["duration"] == 0: return + colors = None + with SQLiteManager() as cur: db.insert_one_track(tags, cur) + extracted = extract_thumb(filepath, tags["albumhash"] + ".webp") + + if not extracted: + return + + colors = handle_colors(cur, tags["albumhash"]) track = Track(**tags) TrackStore.add_track(track) if not AlbumStore.album_exists(track.albumhash): album = AlbumStore.create_album(track) + album.set_colors(colors) AlbumStore.add_album(album) artists: list[Artist] = track.artist + track.albumartist # type: ignore @@ -154,6 +184,7 @@ def add_track(filepath: str) -> None: if not ArtistStore.artist_exists(artist.artisthash): ArtistStore.add_artist(Artist(artist.name)) + extract_thumb(filepath, track.image) def remove_track(filepath: str) -> None: """ @@ -277,7 +308,7 @@ class Handler(PatternMatchingEventHandler): if current_size == previous_size: # Wait for a short duration to ensure the file write operation is complete - time.sleep(0.5) + time.sleep(5) # Check the file size again current_size = os.path.getsize(event.src_path) diff --git a/app/models/album.py b/app/models/album.py index 417162d..7bd9f90 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -157,7 +157,6 @@ class Album: return self.title.strip().endswith(" EP") def check_is_single(self, tracks: list[Track]): - """ Checks if the album is a single. """ diff --git a/app/serializers/album.py b/app/serializers/album.py index 0242847..1f1b090 100644 --- a/app/serializers/album.py +++ b/app/serializers/album.py @@ -1,5 +1,5 @@ from dataclasses import asdict -from app.models.album import Album +from app.models import Album def album_serializer(album: Album, to_remove: set[str]) -> dict: diff --git a/app/store/albums.py b/app/store/albums.py index 21bd2ba..ea2771c 100644 --- a/app/store/albums.py +++ b/app/store/albums.py @@ -4,7 +4,7 @@ import random from tqdm import tqdm from app.models import Album, Track -from app.db.sqlite.albums import SQLiteAlbumMethods as aldb +from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb from .tracks import TrackStore from ..utils.hashing import create_hash @@ -133,6 +133,13 @@ class AlbumStore: """ return albumhash in "-".join([a.albumhash for a in cls.albums]) + @classmethod + def remove_album(cls, album: Album): + """ + Removes an album from the store. + """ + cls.albums.remove(album) + @classmethod def remove_album_by_hash(cls, albumhash: str): """ diff --git a/app/store/artists.py b/app/store/artists.py index 1f3a748..1fb079c 100644 --- a/app/store/artists.py +++ b/app/store/artists.py @@ -2,7 +2,7 @@ import json from tqdm import tqdm -from app.db.sqlite.artists import SQLiteArtistMethods as ardb +from app.db.sqlite.artistcolors import SQLiteArtistMethods as ardb from app.lib.artistlib import get_all_artists from app.models import Artist from app.utils.bisection import UseBisection diff --git a/poetry.lock b/poetry.lock index 38b34a2..09117d0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,31 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "about-time" +version = "4.2.1" +description = "Easily measure timing and throughput of code blocks, with beautiful human friendly representations." +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece"}, + {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, +] + +[[package]] +name = "alive-progress" +version = "3.1.4" +description = "A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "alive-progress-3.1.4.tar.gz", hash = "sha256:74a95d8d0d42bc99d3a3725dbd06ebb852245f1b64e301a7c375b92b22663f7b"}, + {file = "alive_progress-3.1.4-py3-none-any.whl", hash = "sha256:c80ad87ce9c1054b01135a87fae69ecebbfc2107497ae87cbe6aec7e534903db"}, +] + +[package.dependencies] +about-time = "4.2.1" +grapheme = "0.6.0" + [[package]] name = "altgraph" version = "0.17.3" @@ -280,6 +306,19 @@ files = [ Flask = ">=0.9" Six = "*" +[[package]] +name = "grapheme" +version = "0.6.0" +description = "Unicode grapheme helpers" +optional = false +python-versions = "*" +files = [ + {file = "grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca"}, +] + +[package.extras] +test = ["pytest", "sphinx", "sphinx-autobuild", "twine", "wheel"] + [[package]] name = "gunicorn" version = "20.1.0" @@ -1329,4 +1368,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "81941c92f1b4a468b554fc7eeae857897f352142cc3c03393349297ac11dcaae" +content-hash = "da0e11b5066258d0a56917ea1143fa7196c6de88bd7d6b9f2fc060d84e6bf36f" diff --git a/pyproject.toml b/pyproject.toml index cc62e98..fa53ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ Unidecode = "^1.3.6" psutil = "^5.9.4" show-in-file-manager = "^1.1.4" pendulum = "^2.1.2" +alive-progress = "^3.1.4" [tool.poetry.dev-dependencies] pylint = "^2.15.5" diff --git a/tests/sqlite/test_sqlite_actions.py b/tests/sqlite/test_sqlite_actions.py index 07a4527..141d5a0 100644 --- a/tests/sqlite/test_sqlite_actions.py +++ b/tests/sqlite/test_sqlite_actions.py @@ -1,7 +1,7 @@ import json import sqlite3 import os -from app.db.sqlite.artists import SQLiteArtistMethods +from app.db.sqlite.artistcolors import SQLiteArtistMethods from app.db.sqlite.queries import CREATE_APPDB_TABLES from app.db.sqlite.utils import SQLiteManager