A LOTTTT ...

+ fix help text
+ run populate once when -nps flag is used
+ update app version
+ sort tracks by track and disc no. when saving to playlist
+ serialize search results
+ update tags.artist -> tags.artists
+ update tags.albumartist -> tags.albumartists
+ remove artist images from serialized albums
+ add function to serialize artists for cards
+ misc
This commit is contained in:
mungai-njoroge 2023-08-10 10:30:42 +03:00
parent 5cf188dd38
commit 0a703dcc0f
20 changed files with 149 additions and 103 deletions

View File

@ -4,15 +4,17 @@ Contains all the album routes.
import random
from dataclasses import asdict
from typing import Any
from flask import Blueprint, request
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.lib.albumslib import sort_by_track_no
from app.models import FavType, Track
from app.serializers.album import serialize_for_card
from app.serializers.track import track_serializer
from app.serializers.track import serialize_track
from app.store.albums import AlbumStore
from app.store.tracks import TrackStore
from app.utils.hashing import create_hash
@ -83,7 +85,7 @@ def get_album_tracks_and_info():
album.is_favorite = check_is_fav(albumhash, FavType.album)
return {
"tracks": [track_serializer(t, remove_disc=False) for t in tracks],
"tracks": [serialize_track(t, remove_disc=False) for t in tracks],
"info": album,
}
@ -94,13 +96,7 @@ def get_album_tracks(albumhash: str):
Returns all the tracks in the given album, sorted by disc and track number.
"""
tracks = TrackStore.get_tracks_by_albumhash(albumhash)
tracks = [asdict(t) for t in tracks]
for t in tracks:
track = str(t["track"]).zfill(3)
t["_pos"] = int(f"{t['disc']}{track}")
tracks = sorted(tracks, key=lambda t: t["_pos"])
sort_by_track_no(tracks)
return {"tracks": tracks}
@ -210,5 +206,3 @@ def get_similar_albums():
pass
return {"albums": [serialize_for_card(a) for a in albums[:limit]]}

View File

@ -11,7 +11,7 @@ from showinfm import show_in_file_manager
from app import settings
from app.db.sqlite.settings import SettingsSQLMethods as db
from app.lib.folderslib import GetFilesAndDirs, get_folders
from app.serializers.track import track_serializer
from app.serializers.track import serialize_track
from app.store.tracks import TrackStore as store
from app.utils.wintools import is_windows, win_replace_slash
@ -142,7 +142,7 @@ def get_tracks_in_path():
tracks = store.get_tracks_in_path(path)
tracks = sorted(tracks, key=lambda i: i.last_mod)
tracks = (track_serializer(t) for t in tracks if Path(t.filepath).exists())
tracks = (serialize_track(t) for t in tracks if Path(t.filepath).exists())
return {
"tracks": list(tracks)[:300],

View File

@ -11,6 +11,7 @@ from PIL import UnidentifiedImageError, Image
from app import models
from app.db.sqlite.playlists import SQLitePlaylistMethods
from app.lib import playlistlib
from app.lib.albumslib import sort_by_track_no
from app.models.track import Track
from app.store.albums import AlbumStore
from app.store.tracks import TrackStore
@ -69,7 +70,6 @@ def send_all_playlists():
"""
Gets all the playlists.
"""
# get the no_images query param
no_images = request.args.get("no_images", False)
playlists = PL.get_all_playlists()
@ -141,7 +141,9 @@ def get_album_trackhashes(albumhash: str):
Returns a list of trackhashes in an album.
"""
tracks = TrackStore.get_tracks_by_albumhash(albumhash)
return [t.trackhash for t in tracks]
tracks = sort_by_track_no(tracks)
return [t["trackhash"] for t in tracks]
def get_artist_trackhashes(artisthash: str):
@ -410,7 +412,6 @@ def save_item_as_playlist():
trackhashes = get_album_trackhashes(itemhash)
elif itemtype == "artist":
trackhashes = get_artist_trackhashes(itemhash)
else:
trackhashes = []

View File

@ -44,7 +44,7 @@ class HandleArgs:
"""
Runs Pyinstaller.
"""
if ALLARGS.build.value in ARGS:
if ALLARGS.build in ARGS:
with open("pyinstaller.config.ini", "w", encoding="utf-8") as file:
config["DEFAULT"]["BUILD"] = "True"
config.write(file)
@ -73,8 +73,8 @@ class HandleArgs:
@staticmethod
def handle_port():
if ALLARGS.port.value in ARGS:
index = ARGS.index(ALLARGS.port.value)
if ALLARGS.port in ARGS:
index = ARGS.index(ALLARGS.port)
try:
port = ARGS[index + 1]
except IndexError:
@ -89,8 +89,8 @@ class HandleArgs:
@staticmethod
def handle_host():
if ALLARGS.host.value in ARGS:
index = ARGS.index(ALLARGS.host.value)
if ALLARGS.host in ARGS:
index = ARGS.index(ALLARGS.host)
try:
host = ARGS[index + 1]
@ -105,8 +105,8 @@ class HandleArgs:
"""
Modifies the config path.
"""
if ALLARGS.config.value in ARGS:
index = ARGS.index(ALLARGS.config.value)
if ALLARGS.config in ARGS:
index = ARGS.index(ALLARGS.config)
try:
config_path = ARGS[index + 1]
@ -125,34 +125,34 @@ class HandleArgs:
@staticmethod
def handle_no_feat():
# if ArgsEnum.no_feat in ARGS:
if any((a in ARGS for a in ALLARGS.show_feat.value)):
if any((a in ARGS for a in ALLARGS.show_feat)):
settings.FromFlags.EXTRACT_FEAT = False
@staticmethod
def handle_remove_prod():
if any((a in ARGS for a in ALLARGS.show_prod.value)):
if any((a in ARGS for a in ALLARGS.show_prod)):
settings.FromFlags.REMOVE_PROD = False
@staticmethod
def handle_cleaning_albums():
if any((a in ARGS for a in ALLARGS.dont_clean_albums.value)):
if any((a in ARGS for a in ALLARGS.dont_clean_albums)):
settings.FromFlags.CLEAN_ALBUM_TITLE = False
@staticmethod
def handle_cleaning_tracks():
if any((a in ARGS for a in ALLARGS.dont_clean_tracks.value)):
if any((a in ARGS for a in ALLARGS.dont_clean_tracks)):
settings.FromFlags.REMOVE_REMASTER_FROM_TRACK = False
@staticmethod
def handle_periodic_scan():
if any((a in ARGS for a in ALLARGS.no_periodic_scan.value)):
if any((a in ARGS for a in ALLARGS.no_periodic_scan)):
settings.FromFlags.DO_PERIODIC_SCANS = False
@staticmethod
def handle_periodic_scan_interval():
if any((a in ARGS for a in ALLARGS.periodic_scan_interval.value)):
if any((a in ARGS for a in ALLARGS.periodic_scan_interval)):
index = [
ARGS.index(a) for a in ALLARGS.periodic_scan_interval.value if a in ARGS
ARGS.index(a) for a in ALLARGS.periodic_scan_interval if a in ARGS
][0]
try:
@ -177,12 +177,12 @@ class HandleArgs:
@staticmethod
def handle_help():
if any((a in ARGS for a in ALLARGS.help.value)):
if any((a in ARGS for a in ALLARGS.help)):
print(HELP_MESSAGE)
sys.exit(0)
@staticmethod
def handle_version():
if any((a in ARGS for a in ALLARGS.version.value)):
if any((a in ARGS for a in ALLARGS.version)):
print(settings.Release.APP_VERSION)
sys.exit(0)

View File

@ -42,7 +42,15 @@ class SQLiteTrackMethods:
:date, :disc, :duration, :filepath, :folder, :genre, :last_mod, :title, :track, :trackhash)
"""
#
track = OrderedDict(sorted(track.items()))
track["artist"] = track["artists"]
track["albumartist"] = track["albumartists"]
del track["artists"]
del track["albumartists"]
cur.execute(sql, track)
@classmethod

View File

@ -2,11 +2,15 @@
Contains methods relating to albums.
"""
from dataclasses import asdict
from typing import Any
from alive_progress import alive_bar
from app.logger import log
from app.models.track import Track
from app.store.albums import AlbumStore
from app.store.tracks import TrackStore
from app.logger import log
def validate_albums():
@ -25,3 +29,15 @@ def validate_albums():
if album.albumhash not in album_hashes:
AlbumStore.remove_album(album)
bar()
def sort_by_track_no(tracks: list[Track]) -> list[dict[str, Any]]:
tracks = [asdict(t) for t in tracks]
for t in tracks:
track = str(t["track"]).zfill(3)
t["_pos"] = int(f"{t['disc']}{track}")
tracks = sorted(tracks, key=lambda t: t["_pos"])
return tracks

View File

@ -144,7 +144,7 @@ def get_artists_from_tracks(tracks: list[Track]) -> set[str]:
"""
artists = set()
master_artist_list = [[x.name for x in t.artist] for t in tracks]
master_artist_list = [[x.name for x in t.artists] for t in tracks]
artists = artists.union(*master_artist_list)
return artists

View File

@ -164,11 +164,11 @@ class Populate:
if not AlbumStore.album_exists(track.albumhash):
AlbumStore.add_album(AlbumStore.create_album(track))
for artist in track.artist:
for artist in track.artists:
if not ArtistStore.artist_exists(artist.artisthash):
ArtistStore.add_artist(Artist(artist.name))
for artist in track.albumartist:
for artist in track.albumartists:
if not ArtistStore.artist_exists(artist.artisthash):
ArtistStore.add_artist(Artist(artist.name))

View File

@ -7,14 +7,17 @@ from rapidfuzz import process, utils
from unidecode import unidecode
from app import models
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
from app.models.enums import FavType
from app.models.track import Track
from app.serializers.album import serialize_for_card as serialize_album
from app.serializers.album import serialize_for_card_many as serialize_albums
from app.serializers.artist import serialize_for_cards
from app.serializers.track import serialize_track, serialize_tracks
from app.store.albums import AlbumStore
from app.store.artists import ArtistStore
from app.store.tracks import TrackStore
from app.utils.remove_duplicates import remove_duplicates
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
# ratio = fuzz.ratio
# wratio = fuzz.WRatio
@ -303,6 +306,8 @@ class TopResults:
else:
top_tracks = TopResults.get_track_items(result, query, limit=tracks_limit)
top_tracks = serialize_tracks(top_tracks)
if tracks_only:
return top_tracks
@ -311,10 +316,19 @@ class TopResults:
else:
albums = TopResults.get_album_items(result, query, limit=albums_limit)
albums = serialize_albums(albums)
if albums_only:
return albums
artists = SearchArtists(query)()[:artists_limit]
artists = serialize_for_cards(artists)
if result["type"] == "track":
result["item"] = serialize_track(result["item"])
if result["type"] == "album":
result["item"] = serialize_album(result["item"])
return {
"top_result": result,

View File

@ -7,12 +7,10 @@ from tinytag import TinyTag
from app.settings import Defaults, Paths
from app.utils.hashing import create_hash
from app.utils.parsers import (parse_artist_from_filename,
parse_title_from_filename)
from app.utils.parsers import parse_artist_from_filename, parse_title_from_filename
from app.utils.wintools import win_replace_slash
def parse_album_art(filepath: str):
"""
Returns the album art for a given audio file.
@ -163,6 +161,9 @@ def get_tags(filepath: str):
tags.filetype = filetype
tags.last_mod = last_mod
tags.artists = tags.artist
tags.albumartists = tags.albumartist
tags = tags.__dict__
# delete all tag properties that start with _ (tinytag internals)
@ -182,6 +183,8 @@ def get_tags(filepath: str):
"track_total",
"year",
"bitdepth",
"artist",
"albumartist",
]
for tag in to_delete:

View File

@ -176,7 +176,7 @@ def add_track(filepath: str) -> None:
album.set_colors(colors)
AlbumStore.add_album(album)
artists: list[Artist] = track.artist + track.albumartist # type: ignore
artists: list[Artist] = track.artists + track.albumartists # type: ignore
for artist in artists:
if not ArtistStore.artist_exists(artist.artisthash):
@ -202,7 +202,7 @@ def remove_track(filepath: str) -> None:
if empty_album:
AlbumStore.remove_album_by_hash(track.albumhash)
artists: list[Artist] = track.artist + track.albumartist # type: ignore
artists: list[Artist] = track.artists + track.albumartists # type: ignore
for artist in artists:
empty_artist = not ArtistStore.artist_has_tracks(artist.artisthash)

View File

@ -1,14 +1,11 @@
from dataclasses import dataclass
from app.settings import get_flag, ParserFlags
from app.settings import ParserFlags, get_flag
from app.utils.hashing import create_hash
from app.utils.parsers import (
split_artists,
remove_prod,
parse_feat_from_title,
clean_title,
get_base_title_and_versions,
)
from app.utils.parsers import (clean_title, get_base_title_and_versions,
parse_feat_from_title, remove_prod,
split_artists)
from .artist import ArtistMinimal
@ -19,9 +16,9 @@ class Track:
"""
album: str
albumartist: str | list[ArtistMinimal]
albumartists: str | list[ArtistMinimal]
albumhash: str
artist: str | list[ArtistMinimal]
artists: str | list[ArtistMinimal]
bitrate: int
copyright: str
date: int
@ -48,8 +45,8 @@ class Track:
self.og_album = self.album
self.last_mod = int(self.last_mod)
if self.artist is not None:
artists = split_artists(self.artist)
if self.artists is not None:
artists = split_artists(self.artists)
new_title = self.title
if get_flag(ParserFlags.EXTRACT_FEAT):
@ -78,12 +75,11 @@ class Track:
self.recreate_albumhash()
self.artist_hashes = "-".join(create_hash(a, decode=True) for a in artists)
self.artist = [ArtistMinimal(a) for a in artists]
self.artists = [ArtistMinimal(a) for a in artists]
albumartists = split_artists(self.albumartist)
self.albumartist = [ArtistMinimal(a) for a in albumartists]
albumartists = split_artists(self.albumartists)
self.albumartists = [ArtistMinimal(a) for a in albumartists]
self.filetype = self.filepath.rsplit(".", maxsplit=1)[-1]
self.image = self.albumhash + ".webp"
if self.genre is not None and self.genre != "":
@ -102,20 +98,20 @@ class Track:
return
self.trackhash = create_hash(
", ".join([a.name for a in self.artist]), self.og_album, self.title
", ".join([a.name for a in self.artists]), self.og_album, self.title
)
def recreate_artists_hash(self):
"""
Recreates a track's artist hashes if the artist list was altered
"""
self.artist_hashes = "-".join(a.artisthash for a in self.artist)
self.artist_hashes = "-".join(a.artisthash for a in self.artists)
def recreate_albumhash(self):
"""
Recreates an albumhash of a track to merge all versions of an album.
"""
self.albumhash = create_hash(self.album, self.albumartist)
self.albumhash = create_hash(self.album, self.albumartists)
def rename_album(self, new_album: str):
"""
@ -126,7 +122,7 @@ class Track:
def add_artists(self, artists: list[str], new_album_title: str):
for artist in artists:
if create_hash(artist) not in self.artist_hashes:
self.artist.append(ArtistMinimal(artist))
self.artists.append(ArtistMinimal(artist))
self.recreate_artists_hash()
self.rename_album(new_album_title)

View File

@ -21,6 +21,11 @@ def run_periodic_scans():
# ValidateAlbumThumbs()
# ValidatePlaylistThumbs()
try:
Populate(key=get_random_str())
except PopulateCancelledError:
pass
while get_flag(ParserFlags.DO_PERIODIC_SCANS):
try:
Populate(key=get_random_str())

View File

@ -8,20 +8,20 @@ Usage: swingmusic [options]
Swing Music is a beautiful, self-hosted music player for your local audio files. Like a cooler Spotify ... but bring your own music.
Options:
{', '.join(args.help.value)}: Show this help message
{', '.join(args.version.value)}: Show the app version
{', '.join(args.help)}: Show this help message
{', '.join(args.version)}: Show the app version
{args.host}: Set the host
{args.port}: Set the port
{args.config}: Set the config path
{', '.join(args.show_feat.value)}: Do not extract featured artists from the song title
{', '.join(args.show_prod.value)}: Do not hide producers in the song title
{', '.join(args.dont_clean_albums.value)}: Don't clean album titles. Cleaning is done by removing information in
{', '.join(args.show_feat)}: Do not extract featured artists from the song title
{', '.join(args.show_prod)}: Do not hide producers in the song title
{', '.join(args.dont_clean_albums)}: Don't clean album titles. Cleaning is done by removing information in
parentheses and showing it separately
{', '.join(args.dont_clean_tracks.value)}: Don't remove remaster information from track titles
{', '.join(args.no_periodic_scan.value)}: Disable periodic scan
{', '.join(args.periodic_scan_interval.value)}: Set the periodic scan interval in seconds. Default is 300 seconds (5
{', '.join(args.dont_clean_tracks)}: Don't remove remaster information from track titles
{', '.join(args.no_periodic_scan)}: Disable periodic scan
{', '.join(args.periodic_scan_interval)}: Set the periodic scan interval in seconds. Default is 300 seconds (5
minutes)
{args.build}: Build the application (in development)

View File

@ -9,6 +9,10 @@ def album_serializer(album: Album, to_remove: set[str]) -> dict:
for key in to_remove:
album_dict.pop(key, None)
# remove artist images
for artist in album_dict["albumartists"]:
artist.pop("image", None)
return album_dict
@ -16,7 +20,7 @@ def serialize_for_card(album: Album):
props_to_remove = {
"duration",
"count",
"albumartist_hashes",
"albumartists_hashes",
"og_title",
"base_title",
"genres",

View File

@ -1,25 +1,23 @@
# from dataclasses import asdict
from dataclasses import asdict
from app.models.artist import Artist
# def album_serializer(album: Artist, to_remove: set[str]) -> ArtistMinimal:
# album_dict = asdict(album)
def serialize_for_card(artist: Artist):
artist_dict = asdict(artist)
# to_remove.update(key for key in album_dict.keys() if key.startswith("is_"))
# for key in to_remove:
# album_dict.pop(key, None)
props_to_remove = {
"is_favorite",
"trackcount",
"duration",
"albumcount",
}
# return album_dict
for key in props_to_remove:
artist_dict.pop(key, None)
return artist_dict
# Traceback (most recent call last):
# File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
# self.run()
# File "/usr/lib/python3.10/threading.py", line 953, in run
# self._target(*self._args, **self._kwargs)
# File "/usr/lib/python3.10/multiprocessing/pool.py", line 579, in _handle_results
# task = get()
# File "/usr/lib/python3.10/multiprocessing/connection.py", line 251, in recv
# return _ForkingPickler.loads(buf.getbuffer())
# File "/home/cwilvx/.cache/pypoetry/virtualenvs/swing_music_player-xIXBgWdk-py3.10/lib/python3.10/site-packages/requests/exceptions.py", line 41, in __init__
# CompatJSONDecodeError.__init__(self, *args)
# TypeError: JSONDecodeError.__init__() missing 2 required positional arguments: 'doc' and 'pos'
def serialize_for_cards(artists: list[Artist]):
return [serialize_for_card(a) for a in artists]

View File

@ -3,7 +3,7 @@ from dataclasses import asdict
from app.models.track import Track
def track_serializer(track: Track, to_remove: set = {}, remove_disc=True) -> dict:
def serialize_track(track: Track, to_remove: set = {}, remove_disc=True) -> dict:
album_dict = asdict(track)
props = {
"date",
@ -14,6 +14,7 @@ def track_serializer(track: Track, to_remove: set = {}, remove_disc=True) -> dic
"copyright",
"disc",
"track",
"artist_hashes",
}.union(to_remove)
if not remove_disc:
@ -26,10 +27,15 @@ def track_serializer(track: Track, to_remove: set = {}, remove_disc=True) -> dic
for key in props:
album_dict.pop(key, None)
to_remove_images = ["artists", "albumartists"]
for key in to_remove_images:
for artist in album_dict[key]:
artist.pop("image", None)
return album_dict
def serialize_tracks(
tracks: list[Track], _remove: set = {}, remove_disc=True
) -> list[dict]:
return [track_serializer(t, _remove, remove_disc) for t in tracks]
return [serialize_track(t, _remove, remove_disc) for t in tracks]

View File

@ -2,13 +2,12 @@
Contains default configs
"""
import os
from enum import Enum
join = os.path.join
class Release:
APP_VERSION = "1.3.0.beta"
APP_VERSION = "1.3.0"
class Paths:
@ -129,7 +128,7 @@ class FLASKVARS:
cls.FLASK_HOST = host
class ALLARGS(Enum):
class ALLARGS:
"""
Enumerates the possible app arguments.
"""
@ -171,7 +170,7 @@ class FromFlags:
MERGE_ALBUM_VERSIONS = False
class ParserFlags(Enum):
class ParserFlags():
EXTRACT_FEAT = "EXTRACT_FEAT"
REMOVE_PROD = "REMOVE_PROD"
CLEAN_ALBUM_TITLE = "CLEAN_ALBUM_TITLE"
@ -183,7 +182,7 @@ class ParserFlags(Enum):
def get_flag(flag: ParserFlags) -> bool:
return getattr(FromFlags, flag.value)
return getattr(FromFlags, flag)
def get_scan_sleep_time() -> int:

View File

@ -3,10 +3,11 @@ import random
from tqdm import tqdm
from app.models import Album, Track
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb
from .tracks import TrackStore
from app.models import Album, Track
from ..utils.hashing import create_hash
from .tracks import TrackStore
class AlbumStore:
@ -19,7 +20,7 @@ class AlbumStore:
"""
return Album(
albumhash=track.albumhash,
albumartists=track.albumartist, # type: ignore
albumartists=track.albumartists, # type: ignore
title=track.og_album,
)

View File

@ -6,8 +6,9 @@ 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
from .tracks import TrackStore
from .albums import AlbumStore
from .tracks import TrackStore
class ArtistStore:
@ -92,7 +93,7 @@ class ArtistStore:
for track in TrackStore.tracks:
artists.update(track.artist_hashes)
album_artists: list[str] = [a.artisthash for a in track.albumartist]
album_artists: list[str] = [a.artisthash for a in track.albumartists]
artists.update(album_artists)
master_hash = "-".join(artists)