modularize src

+ merge main.py and manage.py
+ move start logic to swingmusic/__main__.py
+ add a run.py on the project root
This commit is contained in:
cwilvx
2025-05-25 20:35:54 +03:00
parent 76fc97e088
commit 86fabcd5e3
171 changed files with 658 additions and 627 deletions
+16
View File
@@ -0,0 +1,16 @@
from .album import Album
from .track import Track
from .artist import Artist, ArtistMinimal
from .enums import FavType
from .playlist import Playlist
from .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 .track import Track
from ..utils.hashing import create_hash
from swingmusic.utils.auth import get_current_userid
from ..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 = (
Path(settings.Paths.get_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()})
+28
View File
@@ -0,0 +1,28 @@
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"]
return this_dict
def todict_simplified(self):
return {
"id": self.id,
"username": self.username,
"firstname": self.extra["firstname"] if self.extra else "",
}