merge refactors pr #364 from @michilyy

* Save to DB only unique trackhashes

* Add check if track already exists in playlist

* replace all paths with `pathlib.Path`

* `architecture.md`:
* add config folder layout

`config.py`:
* fix bug where `pathlib.Path` cannot be serialized

`files.py`:
* remove unused imports
* update path concatenation to `pathlib.Path`
* add config-folder creation

`imgserver.py`:
* fix serialisation bug

`playlistlib.py`:
* update path concatenation to `pathlib.Path`

* update all `settings.Paths` usages to new singleton `Paths` class.

* update all usages of `settings.Paths`

* `files.py`:
* rework assets copy function.
* remove unused loop and unused `shutil.copy2` function

`settings.py`
* fix recursion exception in `Paths`

* `settings.py`:
* remove Singleton and `@property` todos from `Paths`

* `__init__.py`:
* remove now unused function `create_config_dir()`

`setup.files`:
* remove because merged into `settings.Paths()`
  for more central and clear flow how the base path gets decided

`settings.py`:
* add `copy_assets` function

`start_swingmusic.py`:
* add configurable settings.Paths class

`__main__.py`:
* update click to used correct default path

* remove wrong commited egg files

* remove change in the wrong branch

* add forgotten `property` decorator
update `get_files_and_dirs` to use pathlib where possible

`config.py`:
* update type annotation

`folders.py`:
* convert `pathlib` to posix path where needed for sub-functions

`folderlib.py`:
* rework `get_files_and_dirs` to use `pathlib` where possible

`settings.py`:
* add forgotten `@property`

`start_swingmusic.py`:
* remove second `log_startup_info()`

* `artistlib.py`:
* fix calling property
`tagger.py`:
* fix comparing elements in `pathlib.Path`

* add support for repeating lyrics.

* rework lyrics api and lib

* update most path functions.
add type-hint pathlib where needed

* for serialization paths are converted to posix path

* use `open` instead of `os.open`
update `metaclass` with constant

* fix initial config exception if empty file existed

* update `userConfig` with `InitVar` to be excluded from `asdict`

* remove `is_windows_slash()`
rework path function to use pathlib

* convert `pathlib.Path` to `str` for serialization

* fixing bug with str + pathlib

* `__main__.py`:
* update click to use package version
* remove now unused function `print_version`

`filesystem.py`:
* rework `CWD` to use importlib

`pyproject.toml`:
* disable namespace for `importlib.resources` to work correctly

* update `lyrics.py`:
* remove unused functions
* simplify functions

* fix bug where assets get created on root

* remove unused code

* update lyrics for clearer structure.

* add support for unsynced lyrics

* fix wrong return type in unsynced lyrics

* update `/check` to use `send_lyrics`

* prefer tags to duplicates

* `lyrics.py`:
* add docs to a function group

* `logger.py`:
* add logging config dict.
* combine Logging into one file
* add socket logger
* add debug mode to logger
* add JSONL formater

* `logger.py`:
* update config to directly use the formater.
resolves circular import exception

`__main__.py`:
* add logger setup to main

`start_swingmusic.py`:
* add debug option to cli

* `lyrics.py`:
* add offset support

* add `setuptools-scm` to get version from git

* add support for docker build with scm

* add support for docker build with scm
need someone who can test the changes workflow

* update all usage of `version.txt` to `metadata.version()`

* 2x update all usage of `version.txt` to `metadata.version()`

* update to no local_scheme version

* provide fix for #331.
convert `sql.Row` and `TrackTable` to dict before converting to dataclass.

* fix `__main__.py`:
* wrong import and uncommited changes
* add debug and base_path parameter

* fix logger pathlib

* add client build workflow

* set name

* split client from build

* try fixing builds

* try another fix

* try also another fix

* try again something new

* try again something new

* change runner

* fix failed run because of malformed runner

* add wheel builds

* remove systems from pure python build

* add isolated pyinstaller build

* artifacts with names

* wrong wheel path

* try fetch-depth for tag fetch

* disable fail-fast.
add wheel installation

* add install system packages

* add debug

* fix wheel install
fix pyinstaller spec file

* try fix for pyinstaller

* try another fix

* build on release

* add concrete release types

* only run on released or pre-released

* try release upload

* reformat upload

* fix needs tag

* identifiable pyinstaller builds

* compress client folder before uploading

* update to src build

* remove no more needed aarch64 build script
rename pyinstaller assets to lowercase

* remove unneeded code

* fix: save to DB only unique track hashes

* replace click with argparse

* set concrete types in argparse

* replace manuall path usages with pathlib

* remove unused `configs.py` file

* reformat `start_swingmusic.py`

* fix empty set startup exception

* optimizing static files serve function

* fixing bug in optimisation of static files serve function

* fix folder view bug

* colorlib.py:
* fix wrong type exception
* remove singe use Index_everything class
* update logging of populate.py

* cleanup files

* fix settings.py Paths copy function.
Created folder on file.

* add exist check to folder

* remove unused `INFO` class

* fix multiprocessing bug on windows

* potential icon fix for pyinstaller
fix multiple logging bug

* fix argparse config path bug
add jobs file

* cleanup code fragments
fix logging issue
add notes to function

* note that concurrent creates own sys.modules

* refactor some lyrics plugin condition
remove unused import from hashing

* refactor taglib.py

* update import statements to be static

* playlistlib.py:
* refactoring and more doc strings
populate.py:
* add poc bugfix
settings.py:
* add typehint

* possible bugfix for multitreading globals

* folder.py:
* add check if provided path is absolute
populate.py:
* add bug note
settings.py:
* add possible error from Singleton implementation
start_swingmusic.py:
* correct spelling
* pass resolved path to Paths
tagger.py:
* add logging

* trying out fixes for multithreading

* only upload results not metadata

* fix build action again

* folder.py:
* strictly use pathlib where possible

folderlib.py:
* add missing docstring to function, who really need it.

track.py:
* refactor some code

folder.py:
* refactor some more code

* Merge DBPath class and Paths class.
Update all usages of DBPath

folderslib.py:
* fix bug with logging

taglib.py:
* add missing docstring

settings.py:
* merge classes
* refactor

* network.py:
* add more docstring

config.py:
* update pathlib usage

tools.py:
* refactor
* add docstrings

* colorlib.py:
* add docstring

Refactor App builder into grouped config settings.

* update assets access for migration

* Update FUNDING.yml

* Update FUNDING.yml

* upgrade tinytag in requirements.txt

* update readme

* update license

* update readme

* Update README.md

* Update README.md

* cleanup requirements.txt
remove unused import in audio_segment.py
add entrypoint.sh for appimage support
update pyproject.toml for optional dependencies
add appimage to github workflow

* fix invalid workflow file

* AppImage build needs more research.
Commenting for now

* testing a new build workflow

* add libev installation

* update workflow to new optional dependencies

* trying again another fix

* finally fix all optional deps installation correctly

* remove AppImage poc

* albumslib.py:
* add docstring

folder.py:
* add unix path fix

update logger name to `__name__`

* update build with docker
update Dockerfile with git
fix typo in lyrics.py
add dynamic deps back

* add log for static folder

* add missing import

* add some more todos

* add support for AppImages even when it's not perfect.

* quick bugfix for wrong appimage config path

* fix uploading not finding AppImages builds aka wrong pattern

* optimise docker build by using artifacts.
Add client path option.
change docstring to sphinx format

* add todos

* Now support AppImages for real:
manually build AppImage as we are building a complex project.

* fix missing dep in AppImage build

* add full AppImage metadata

* add missing image file.

* only update swingmusic appimage not tool

* add todo and fix AppImage build again.

* Try fixing some path mixup in AppImage build

* add debug tag to action

* correct path to appimage folder

* do not download tool before checkout

* Another fix for path in appimage build

* extend config files with more information

* default client dir is now inside the config dir.
TODOs updated.

* default client dir is now inside the config dir.
TODOs updated.
Add priority todos.

* Auto download client when client not found.
Respects user provided dir.

* rename `requests` submodule to `request`

* poc for arm AppImage builds

* try out another fix

* fix typo in build.yml

* add missing arch tag

* fix uploading double names

* unique naming

* enable fallback version for project.

* do not download client into readonly dir.

* fix relative client download path. Client was resolved into parent of config.

* remove client backup path as client is now downloadable

* `Paths` checks if config folder exists and creates it if necessary.
logger no more creates the config folder.
`app_builder.py`: static route no more with '/client'

* path are only created in MainProcess.
fix gz file not found.

* move assets into src and update usages accordingly

* remove solved todos

* Only upload artefacts if not draft/master aka only on tag

* wrong type in assets copy

* update log with correct priority

* add debug statements and logging to Paths

* remove debugging statement

* remove double version tag from docker build

* fork save release protection

* fix typo

* add fallback client dir for static builds.

* update argparse to new param

* add missing import pathlib

* add sparse checkout as we do not need everything downloaded

* add assets copy check

* init logger bevor Paths

* remove unused import

* check if logdir exists and create if not

* only add exec info to file

* remove exception log from cli

* move logging into main.
Allows tools support again.

* UserConfig now correctly uses _finished key.
Bug where _finished was never written

* double save serverId.
update root_dir to trow no exception on init.
remove debug param

* clean up TODOs

---------

Co-authored-by: skilletfun <skilletfun.laptew.sergey@yandex.ru>
Co-authored-by: Mungai Njoroge <geoffreymungai45@gmail.com>
This commit is contained in:
michily
2025-08-28 09:28:11 +00:00
committed by GitHub
parent b4b0a6e11f
commit e770606567
197 changed files with 2961 additions and 2150 deletions
+16
View File
@@ -0,0 +1,16 @@
from swingmusic.models.album import Album
from swingmusic.models.track import Track
from swingmusic.models.artist import Artist, ArtistMinimal
from swingmusic.models.enums import FavType
from swingmusic.models.playlist import Playlist
from swingmusic.models.folder import Folder
__all__ = [
"Album",
"Track",
"Artist",
"ArtistMinimal",
"Playlist",
"Folder",
"FavType",
]
+191
View File
@@ -0,0 +1,191 @@
import dataclasses
from dataclasses import dataclass
from swingmusic.models.track import Track
from swingmusic.utils.hashing import create_hash
from swingmusic.utils.auth import get_current_userid
from swingmusic.utils.parsers import get_base_title_and_versions
@dataclass(slots=True)
class Album:
"""
Creates an album object
"""
albumartists: list[dict[str, str]]
albumhash: str
artisthashes: list[str]
base_title: str
color: str
created_date: int
date: int
duration: int
genres: list[dict[str, str]]
genrehashes: list[str]
og_title: str
title: str
trackcount: int
lastplayed: int
playcount: int
playduration: int
extra: dict
pathhash: str = ""
id: int = -1
type: str = "album"
image: str = ""
_score: float = 0
versions: list[str] = dataclasses.field(default_factory=list)
fav_userids: list[int] = dataclasses.field(default_factory=list)
weakhash: str = ""
@property
def is_favorite(self):
return get_current_userid() in self.fav_userids
def toggle_favorite_user(self, userid: int):
"""
Adds or removes the given user from the list of users
who have favorited the album.
"""
if userid in self.fav_userids:
self.fav_userids.remove(userid)
else:
self.fav_userids.append(userid)
def __post_init__(self):
self.image = self.albumhash + ".webp" + "?pathhash=" + self.pathhash
self.populate_versions()
self.weakhash = create_hash(
self.og_title, ",".join(a["name"] for a in self.albumartists)
)
def populate_versions(self):
_, self.versions = get_base_title_and_versions(self.og_title, get_versions=True)
if "super_deluxe" in self.versions:
self.versions.remove("deluxe")
# at this point, we should know the type of album
if "original" in self.versions and self.type == "soundtrack":
self.versions.remove("original")
self.versions = [v.replace("_", " ") for v in self.versions]
def check_type(self, tracks: list[Track], singleTrackAsSingle: bool):
"""
Runs all the checks to determine the type of album.
"""
if self.is_single(tracks, singleTrackAsSingle):
self.type = "single"
return
if self.is_soundtrack():
self.type = "soundtrack"
return
if self.is_live_album():
self.type = "live album"
return
if self.is_compilation():
self.type = "compilation"
return
if self.is_ep():
self.type = "ep"
return
self.type = "album"
def is_soundtrack(self) -> bool:
"""
Checks if the album is a soundtrack.
"""
keywords = ["motion picture", "soundtrack"]
for keyword in keywords:
if keyword in self.og_title.lower():
return True
return False
def is_compilation(self) -> bool:
"""
Checks if the album is a compilation.
"""
artists = [a["name"] for a in self.albumartists]
artists = "".join(artists).lower()
if "various artists" in artists:
return True
substrings = {
"the essential",
"best of",
"greatest hits",
"#1 hits",
"number ones",
"super hits",
"collection",
"anthology",
"great hits",
"biggest hits",
"the hits",
"the ultimate",
"compilation",
}
for substring in substrings:
if substring in self.title.lower():
return True
return False
def is_live_album(self):
"""
Checks if the album is a live album.
"""
keywords = ["live from", "live at", "live in", "live on", "mtv unplugged"]
for keyword in keywords:
if keyword in self.og_title.lower():
return True
return False
def is_ep(self) -> bool:
"""
Checks if the album is an EP.
"""
return self.title.strip().endswith(" EP")
# TODO: check against number of tracks
def is_single(self, tracks: list[Track], singleTrackAsSingle: bool):
"""
Checks if the album is a single.
"""
keywords = ["single version", "- single"]
# show_albums_as_singles = get_flag(SessionVarKeys.SHOW_ALBUMS_AS_SINGLES)
for keyword in keywords:
if keyword in self.og_title.lower():
return True
# REVIEW: Reading from the config file in a for loop will be slow
# TODO: Find a
if singleTrackAsSingle and self.trackcount == 1:
return True
if (
len(tracks) == 1
and (
create_hash(tracks[0].title) == create_hash(self.title)
or create_hash(tracks[0].title) == create_hash(self.og_title)
) # if they have the same title
# and tracks[0].track == 1
# and tracks[0].disc == 1
# TODO: Review -> Are the above commented checks necessary?
):
return True
+77
View File
@@ -0,0 +1,77 @@
import dataclasses
from dataclasses import dataclass
from swingmusic.utils.auth import get_current_userid
from swingmusic.utils.hashing import create_hash
@dataclass(slots=True)
class ArtistMinimal:
"""
ArtistMinimal class
"""
name: str
artisthash: str = ""
image: str = ""
def __init__(self, name: str):
self.name = name
self.artisthash = create_hash(self.name, decode=True)
self.image = self.artisthash + ".webp"
# hack to override all the variations from unreleased files (sorry guys!)
if self.artisthash == "5a37d5315e":
self.name = "Juice WRLD"
def to_json(self):
return {
"name": self.name,
"artisthash": self.artisthash,
}
@dataclass(slots=True)
class Artist:
"""
Artist class
"""
name: str
albumcount: int
artisthash: str
created_date: int
date: int
duration: int
genres: list[dict[str, str]]
genrehashes: list[str]
name: str
trackcount: int
lastplayed: int
playcount: int
playduration: int
extra: dict
id: int = -1
image: str = ""
_score: float = 0
color: str = ""
fav_userids: list[int] = dataclasses.field(default_factory=list)
@property
def is_favorite(self):
return get_current_userid() in self.fav_userids
def toggle_favorite_user(self, userid: int):
"""
Adds or removes the given user from the list of users
who have favorited this artist.
"""
if userid in self.fav_userids:
self.fav_userids.remove(userid)
else:
self.fav_userids.append(userid)
def __post_init__(self):
self.image = self.artisthash + ".webp"
+6
View File
@@ -0,0 +1,6 @@
class FavType:
"""Favorite types enum"""
track = "track"
album = "album"
artist = "artist"
+15
View File
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from typing import Any, Literal
@dataclass
class Favorite:
hash: str
type: Literal["album", "track", "artist"]
timestamp: int
userid: int
extra: dict[str, Any]
def __post_init__(self):
# remove the type prefix from the hash
self.hash = self.hash.replace(f"{self.type}_", "")
+9
View File
@@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass(slots=True, frozen=True)
class Folder:
name: str
path: str
is_sym: bool = False
trackcount: int = 0
+28
View File
@@ -0,0 +1,28 @@
from dataclasses import dataclass
from typing import Any
@dataclass
class SimilarArtistEntry:
artisthash: str
name: str
weight: float
scrobbles: int
listeners: int
@dataclass
class SimilarArtist:
artisthash: str
similar_artists: list[SimilarArtistEntry]
def get_artist_hash_set(self) -> set[str]:
"""
Returns a set of similar artists.
"""
if not self.similar_artists:
return set()
# INFO:
return set(a['artisthash'] for a in self.similar_artists)
+46
View File
@@ -0,0 +1,46 @@
from dataclasses import dataclass
from typing import Any, Literal
@dataclass
class TrackLog:
"""
Track play logger model
"""
id: int
trackhash: str
duration: int
timestamp: int
source: str
"""
The full source string, eg. "al:123456"
"""
userid: int
extra: dict[str, Any]
type = "track"
type_src = None
"""
The source identifier string, eg. albumhash, artisthash, etc.
"""
def __post_init__(self):
prefix_map = {
"mix:": "mix",
"al:": "album",
"ar:": "artist",
"fo:": "folder",
"pl:": "playlist",
"favorite": "favorite",
}
for prefix, srctype in prefix_map.items():
if self.source.startswith(prefix):
try:
self.type_src = self.source.split(":", 1)[1]
except IndexError:
self.type_src = None
self.type = srctype
break
+73
View File
@@ -0,0 +1,73 @@
import time
from dataclasses import asdict, dataclass, field
from typing import Any
from swingmusic.db.utils import row_to_dict
from swingmusic.lib.playlistlib import get_first_4_images
from swingmusic.serializers.track import serialize_tracks
from swingmusic.store.tracks import TrackStore
from swingmusic.utils.dates import seconds_to_time_string, timestamp_to_time_passed
from swingmusic.utils.hashing import create_hash
@dataclass
class Mix:
id: str
title: str
description: str
tracks: list[str]
sourcehash: str
userid: int
"""
A hash of the tracks used to generate the mix.
"""
timestamp: int = field(default_factory=lambda: int(time.time()))
extra: dict = field(default_factory=dict)
saved: bool = False
def to_full_dict(self):
tracks = TrackStore.get_tracks_by_trackhashes(self.tracks)[:40]
serialized_tracks = serialize_tracks(tracks)
_dict = asdict(self)
_dict["tracks"] = serialized_tracks
# if not self.extra.get("image"):
# _dict["images"] = get_first_4_images(tracks)
_dict["duration"] = seconds_to_time_string(sum(t.duration for t in tracks))
_dict["trackcount"] = len(tracks)
del _dict["extra"]["albums"]
del _dict["extra"]["artists"]
return _dict
def to_dict(self, convert_timestamp: bool = False):
item = asdict(self)
item["trackshash"] = create_hash(*self.tracks[:40])
item["type"] = "mix"
if convert_timestamp:
item["time"] = timestamp_to_time_passed(item["timestamp"])
del item["tracks"]
del item["extra"]["albums"]
del item["extra"]["artists"]
return item
@classmethod
def mix_to_dataclass(cls, entry: Any):
entry_dict = row_to_dict(entry)
entry_dict["id"] = entry_dict["mixid"]
del entry_dict["mixid"]
return Mix(**entry_dict)
@classmethod
def mixes_to_dataclasses(cls, entries: Any):
return [cls.mix_to_dataclass(entry) for entry in entries]
+53
View File
@@ -0,0 +1,53 @@
import dataclasses
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from swingmusic import settings
from swingmusic.utils.auth import get_current_userid
@dataclass(slots=True)
class Playlist:
"""Creates playlist objects"""
id: int | str
image: str | None
last_updated: str
name: str
settings: dict
trackhashes: list[str] = dataclasses.field(default_factory=list)
extra: dict[str, Any] = dataclasses.field(default_factory=dict)
_last_updated: str = ""
userid: int | None = None
thumb: str = ""
count: int = 0
duration: int = 0
has_image: bool = False
images: list[dict[str, str]] = dataclasses.field(default_factory=list)
pinned: bool = False
_score: float = 0
def __post_init__(self):
self.count = len(self.trackhashes)
if self.userid is None:
self.userid = get_current_userid()
self.pinned = self.settings.get("pinned", False)
self.has_image = (
settings.Paths().playlist_img_path / str(self.image)
).exists()
if self.image is not None:
self.thumb = "thumb_" + self.image
else:
self.image = "None"
self.thumb = "None"
def clear_lists(self):
"""
Removes data from lists to make it lighter for sending
over the API.
"""
self.trackhashes = []
+10
View File
@@ -0,0 +1,10 @@
from dataclasses import dataclass
@dataclass
class Plugin:
name: str
active: bool
settings: dict
extra: dict
+9
View File
@@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass
class StatItem:
cssclass: str
text: str
value: str | int
image: str | None = None
+228
View File
@@ -0,0 +1,228 @@
from dataclasses import asdict, dataclass, field
from swingmusic.config import UserConfig
from swingmusic.utils.auth import get_current_userid
from swingmusic.utils.hashing import create_hash
from swingmusic.utils.parsers import (
clean_title,
get_base_title_and_versions,
parse_feat_from_title,
remove_prod,
split_artists,
)
@dataclass(slots=True)
class Track:
"""
Track class
"""
id: int
album: str
albumartists: list[dict[str, str]]
albumhash: str
artists: list[dict[str, str]]
bitrate: int
copyright: str
date: int
disc: int
duration: int
filepath: str
folder: str
genres: str | list[dict[str, str]]
last_mod: int
title: str
track: int
trackhash: str
extra: dict
lastplayed: int
playcount: int
playduration: int
config: UserConfig
og_album: str = ""
og_title: str = ""
artisthashes: list[str] = field(default_factory=list)
genrehashes: list[str] = field(default_factory=list)
weakhash: str = ""
_pos: int = 0
_ati: str = ""
image: str = ""
_score: float = 0
explicit: bool = False
fav_userids: list[int] = field(default_factory=list)
@property
def is_favorite(self):
return get_current_userid() in self.fav_userids
@property
def pathhash(self):
return create_hash(self.folder)
def toggle_favorite_user(self, userid: int):
"""
Toggles the favorite status of the track for a given user.
Args:
userid (int): The ID of the user toggling the favorite status.
"""
if userid in self.fav_userids:
self.fav_userids.remove(userid)
else:
self.fav_userids.append(userid)
def __post_init__(self):
"""
Performs post-initialization processing on the track object.
This includes setting original values, processing artists and genres,
and removing duplicate artists.
"""
self.og_title = self.title
self.og_album = self.album
self.folder = self.folder + "/"
self.weakhash = create_hash(self.title, self.artists)
explicit_tag = self.extra.get("explicit", ["0"])
if isinstance(explicit_tag, list):
explicit_tag = explicit_tag[0] if explicit_tag else "0"
if isinstance(explicit_tag, str):
explicit_tag = explicit_tag.lower()
self.explicit = explicit_tag in ("1", "yes", "true")
else:
self.explicit = bool(explicit_tag)
self.image = self.albumhash + ".webp" + "?pathhash=" + self.pathhash
# self.extra = {
# "disc_total": self.extra.get("disc_total", 0),
# "track_total": self.extra.get("track_total", 0),
# "samplerate": self.extra.get("samplerate", -1),
# }
self.split_artists()
self.map_with_config()
self.process_genres()
# Remove duplicates from artists and albumartists
seen_artists = set()
self.artists = [
d
for d in self.artists
if tuple(d.items()) not in seen_artists
and not seen_artists.add(tuple(d.items()))
]
seen_albumartists = set()
self.albumartists = [
d
for d in self.albumartists
if tuple(d.items()) not in seen_albumartists
and not seen_albumartists.add(tuple(d.items()))
]
self.recreate_trackhash()
self.config = None
def split_artists(self):
"""
Splits the artists and albumartists based on the given separators,
and updates the artisthashes.
"""
def split(artists: str):
return [
{"name": a, "artisthash": create_hash(a, decode=True)}
for a in split_artists(artists, config=self.config)
]
self.artists = split(self.artists)
self.albumartists = split(self.albumartists)
self.artisthashes = [a["artisthash"] for a in self.artists]
def map_with_config(self):
"""
Applies various transformations to the track's title and album
based on the user's configuration settings.
"""
new_title = self.title
# Extract featured artists
if self.config.extractFeaturedArtists:
feat, new_title = parse_feat_from_title(self.title, self.config)
feat = [
{"name": f, "artisthash": create_hash(f, decode=True)} for f in feat
]
feat = [f for f in feat if f["artisthash"] not in self.artisthashes]
self.artists.extend(feat)
self.artisthashes.extend([f["artisthash"] for f in feat])
# Update album title for singles
# ie. album: "Title (feat. Artist)"
# title: "Title (feat. Artist)"
# becomes: album: "Title", title: "Title"
if self.og_album == self.og_title:
self.album = new_title
# Clean track title
if self.config.removeProdBy:
new_title = remove_prod(new_title)
# if self.title == new_title:
# self.album = new_title
if self.config.removeRemasterInfo:
new_title = clean_title(new_title)
self.title = new_title
# Clean album title
if self.config.cleanAlbumTitle:
self.album, _ = get_base_title_and_versions(self.album, get_versions=False)
if self.config.mergeAlbums:
self.albumhash = create_hash(
self.album, *(a["name"] for a in self.albumartists)
)
def process_genres(self):
"""
Processes and standardizes the genre information for the track.
"""
if self.genres:
src_genres: str = self.genres
src_genres = src_genres.lower()
# separators = {"/", ";", "&"}
separators = set(self.config.genreSeparators)
contains_rnb = "r&b" in src_genres
contains_rock = "rock & roll" in src_genres
if contains_rnb:
src_genres = src_genres.replace("r&b", "RnB")
if contains_rock:
src_genres = src_genres.replace("rock & roll", "rock")
for s in separators:
src_genres = src_genres.replace(s, ",")
genres_list: list[str] = src_genres.split(",")
self.genres = [
{"name": g.strip(), "genrehash": create_hash(g.strip())}
for g in genres_list
]
self.genrehashes = [g["genrehash"] for g in self.genres]
def recreate_trackhash(self):
"""
Recreates the trackhash based on the current title, album, and artist information.
"""
self.trackhash = create_hash(
self.title, self.album, *(artist["name"] for artist in self.artists)
)
def copy(self):
return Track(**{**asdict(self), "config": UserConfig()})
+35
View File
@@ -0,0 +1,35 @@
from dataclasses import asdict, field, dataclass
import json
@dataclass(slots=True)
class User:
id: int
image: str
password: str
username: str
roles: list[str]
extra: dict[str, str] = field(default_factory=dict)
# NOTE: roles: ['admin', 'user', 'curator']
roles: list[str] = field(default_factory=lambda: ["user"])
def todict(self):
this_dict = asdict(self)
del this_dict["password"]
if type(this_dict["roles"]) is str:
# INFO: this is an attempt to fix string roles!
try:
this_dict["roles"] = json.loads(this_dict["roles"])
except json.JSONDecodeError:
this_dict["roles"] = []
return this_dict
def todict_simplified(self):
return {
"id": self.id,
"username": self.username,
"firstname": self.extra["firstname"] if self.extra else "",
}