implement artist split ingore list

+ move post processing of tags to the track model
+ rebuild stores on settings update via API
+ check files from the store instead of the db when streaming
+ remove deprecetated table columns
+misc
This commit is contained in:
cwilvx
2024-08-10 08:42:13 +03:00
parent 6d2aac084d
commit cd992419c5
12 changed files with 481 additions and 245 deletions
+2 -8
View File
@@ -33,17 +33,9 @@
# THE BIG ONE # THE BIG ONE
- Updating settings
- Cleaning out commented code
- Watchdog - Watchdog
- Periodic scans - Periodic scans
- Remove legacy db methods
- Review: We don't need server side image colors
- Clean up main db and userdata modules
- Move plugins to a config file
- What about our migrations? - What about our migrations?
- Add userid to queries
- Remove duplicates on artist page (test with Hanson)
- Test foreign keys on delete - Test foreign keys on delete
- Normalize playlists table: - Normalize playlists table:
- New table to hold playlist entries - New table to hold playlist entries
@@ -56,3 +48,5 @@
- Duplicates on search - Duplicates on search
- Audio stops on ending - Audio stops on ending
- Show users on account settings when logged in as admin and show users on login is disabled.
-
+14 -114
View File
@@ -7,11 +7,7 @@ from app.api.auth import admin_required
from app.db.userdata import PluginTable from app.db.userdata import PluginTable
from app.lib.index import index_everything from app.lib.index import index_everything
from app.logger import log
from app.settings import Info from app.settings import Info
from app.store.albums import AlbumStore
from app.store.artists import ArtistStore
from app.store.tracks import TrackStore
from app.config import UserConfig from app.config import UserConfig
bp_tag = Tag(name="Settings", description="Customize stuff") bp_tag = Tag(name="Settings", description="Customize stuff")
@@ -24,65 +20,6 @@ def get_child_dirs(parent: str, children: list[str]):
return [_dir for _dir in children if _dir.startswith(parent) and _dir != parent] return [_dir for _dir in children if _dir.startswith(parent) and _dir != parent]
def reload_everything(instance_key: str):
"""
Reloads all stores using the current database items
"""
try:
TrackStore.load_all_tracks(instance_key)
except Exception as e:
log.error(e)
try:
AlbumStore.load_albums(instance_key=instance_key)
except Exception as e:
log.error(e)
try:
ArtistStore.load_artists(instance_key)
except Exception as e:
log.error(e)
# CHECKPOINT: TEST SETTINGS API ENDPOINTS
# @background
# def rebuild_store(db_dirs: list[str]):
# """
# Restarts watchdog and rebuilds the music library.
# """
# instance_key = get_random_str()
# log.info("Rebuilding library...")
# trackdb.remove_tracks_not_in_folders(db_dirs)
# reload_everything(instance_key)
# try:
# populate.Populate(instance_key=instance_key)
# except populate.PopulateCancelledError as e:
# print(e)
# reload_everything(instance_key)
# return
# WatchDog().restart()
# log.info("Rebuilding library... ✅")
# # I freaking don't know what this function does anymore
# def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
# """
# Params:
# new_: will be added to the database
# removed_: will be removed from the database
# db_dirs_: will be used to remove tracks that
# are outside these directories from the database and store.
# """
# sdb.remove_root_dirs(removed_)
# sdb.add_root_dirs(new_)
# rebuild_store(db_dirs_)
class AddRootDirsBody(BaseModel): class AddRootDirsBody(BaseModel):
new_dirs: list[str] = Field( new_dirs: list[str] = Field(
description="The new directories to add", description="The new directories to add",
@@ -151,18 +88,6 @@ def get_root_dirs():
return {"dirs": UserConfig().rootDirs} return {"dirs": UserConfig().rootDirs}
# maps settings to their parser flags
# mapp = {
# "artist_separators": SessionVarKeys.ARTIST_SEPARATORS,
# "extract_feat": SessionVarKeys.EXTRACT_FEAT,
# "remove_prod": SessionVarKeys.REMOVE_PROD,
# "clean_album_title": SessionVarKeys.CLEAN_ALBUM_TITLE,
# "remove_remaster": SessionVarKeys.REMOVE_REMASTER_FROM_TRACK,
# "merge_albums": SessionVarKeys.MERGE_ALBUM_VERSIONS,
# "show_albums_as_singles": SessionVarKeys.SHOW_ALBUMS_AS_SINGLES,
# }
@api.get("") @api.get("")
def get_all_settings(): def get_all_settings():
""" """
@@ -176,11 +101,6 @@ def get_all_settings():
return config return config
# @background
# def reload_all_for_set_setting():
# reload_everything(get_random_str())
class SetSettingBody(BaseModel): class SetSettingBody(BaseModel):
key: str = Field( key: str = Field(
description="The setting key", description="The setting key",
@@ -192,39 +112,6 @@ class SetSettingBody(BaseModel):
) )
# @api.post("/set")
# @admin_required()
# def set_setting(body: SetSettingBody):
# """
# Set a setting.
# """
# key = body.key
# value = body.value
# if key is None or value is None or key == "root_dirs":
# return {"msg": "Invalid arguments!"}, 400
# root_dir = sdb.get_root_dirs()
# if not root_dir:
# return {"msg": "No root directories set!"}, 400
# if key not in mapp:
# return {"msg": "Invalid key!"}, 400
# if key == "artist_separators":
# value = str(value).split(",")
# value = set(value)
# reload_all_for_set_setting()
# # if value is a set, convert it to a string
# # (artist_separators)
# if type(value) == set:
# value = ",".join(value)
# return {"result": value}
@api.get("/trigger-scan") @api.get("/trigger-scan")
def trigger_scan(): def trigger_scan():
""" """
@@ -256,7 +143,20 @@ def update_config(body: UpdateConfigBody):
body.value = body.value.split(",") body.value = body.value.split(",")
setattr(config, body.key, body.value) setattr(config, body.key, body.value)
print(getattr(config, body.key))
# INFO: Rebuild stores when these settings are updated
reset_stores_lists = {
"artistSeparators",
"artistSplitIgnoreList",
"removeProdBy",
"removeRemasterInfo",
"mergeAlbums",
"cleanAlbumTitle",
"showAlbumsAsSingles",
}
if body.key in reset_stores_lists:
index_everything()
return { return {
"msg": "Config updated!", "msg": "Config updated!",
+38 -8
View File
@@ -10,8 +10,7 @@ from pydantic import BaseModel, Field
from app.api.apischemas import TrackHashSchema from app.api.apischemas import TrackHashSchema
from app.lib.trackslib import get_silence_paddings from app.lib.trackslib import get_silence_paddings
# from app.store.tracks import TrackStore from app.store.tracks import TrackStore
from app.db.libdata import TrackTable
from app.utils.files import guess_mime_type from app.utils.files import guess_mime_type
bp_tag = Tag(name="File", description="Audio files") bp_tag = Tag(name="File", description="Audio files")
@@ -35,10 +34,26 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
filepath = query.filepath filepath = query.filepath
msg = {"msg": "File Not Found"} msg = {"msg": "File Not Found"}
track = TrackTable.get_track_by_trackhash(trackhash, filepath) track = None
track_exists = track is not None and os.path.exists(track.filepath) tracks = TrackStore.get_tracks_by_filepaths([filepath])
if track_exists:
if len(tracks) > 0 and os.path.exists(filepath):
track = tracks[0]
else:
res = TrackStore.trackhashmap.get(trackhash)
# When finding by trackhash, sort by bitrate
# and get the first track that exists
if res is not None:
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
for t in tracks:
if os.path.exists(t.filepath):
track = t
break
if track is not None:
audio_type = guess_mime_type(filepath) audio_type = guess_mime_type(filepath)
return send_file(filepath, mimetype=audio_type, conditional=True) return send_file(filepath, mimetype=audio_type, conditional=True)
@@ -57,10 +72,25 @@ def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
msg = {"msg": "File Not Found"} msg = {"msg": "File Not Found"}
# If filepath is provided, try to send that # If filepath is provided, try to send that
track = TrackTable.get_track_by_trackhash(trackhash, filepath) track = None
track_exists = track is not None and os.path.exists(track.filepath) tracks = TrackStore.get_tracks_by_filepaths([filepath])
if track_exists: if len(tracks) > 0 and os.path.exists(filepath):
track = tracks[0]
else:
res = TrackStore.trackhashmap.get(trackhash)
# When finding by trackhash, sort by bitrate
# and get the first track that exists
if res is not None:
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
for t in tracks:
if os.path.exists(t.filepath):
track = t
break
if track is not None:
audio_type = guess_mime_type(filepath) audio_type = guess_mime_type(filepath)
return send_file_as_chunks(track.filepath, audio_type) return send_file_as_chunks(track.filepath, audio_type)
+7
View File
@@ -22,6 +22,13 @@ class UserConfig:
rootDirs: list[str] = field(default_factory=list) rootDirs: list[str] = field(default_factory=list)
excludeDirs: list[str] = field(default_factory=list) excludeDirs: list[str] = field(default_factory=list)
artistSeparators: set[str] = field(default_factory=lambda: {";", "/"}) artistSeparators: set[str] = field(default_factory=lambda: {";", "/"})
artistSplitIgnoreList: set[str] = field(
default_factory=lambda: {
"AC/DC",
"Bob marley & the wailers",
"Crosby, Stills, Nash & Young",
}
)
genreSeparators: set[str] = field(default_factory=lambda: {"/", ";", "&"}) genreSeparators: set[str] = field(default_factory=lambda: {"/", ";", "&"})
# tracks # tracks
+20 -7
View File
@@ -109,10 +109,10 @@ class TrackTable(Base):
id: Mapped[int] = mapped_column(init=False, primary_key=True) id: Mapped[int] = mapped_column(init=False, primary_key=True)
album: Mapped[str] = mapped_column(String()) album: Mapped[str] = mapped_column(String())
albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON()) albumartists: Mapped[str] = mapped_column(String())
albumhash: Mapped[str] = mapped_column(String(), index=True) albumhash: Mapped[str] = mapped_column(String(), index=True)
artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True) # artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
artists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True) artists: Mapped[str] = mapped_column(String())
bitrate: Mapped[int] = mapped_column(Integer()) bitrate: Mapped[int] = mapped_column(Integer())
copyright: Mapped[Optional[str]] = mapped_column(String()) copyright: Mapped[Optional[str]] = mapped_column(String())
date: Mapped[int] = mapped_column(Integer(), nullable=True) date: Mapped[int] = mapped_column(Integer(), nullable=True)
@@ -120,11 +120,11 @@ class TrackTable(Base):
duration: Mapped[int] = mapped_column(Integer()) duration: Mapped[int] = mapped_column(Integer())
filepath: Mapped[str] = mapped_column(String(), index=True, unique=True) filepath: Mapped[str] = mapped_column(String(), index=True, unique=True)
folder: Mapped[str] = mapped_column(String(), index=True) folder: Mapped[str] = mapped_column(String(), index=True)
genrehashes: Mapped[list[str]] = mapped_column(JSON(), index=True) # genrehashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
genres: Mapped[Optional[list[dict[str, str]]]] = mapped_column(JSON()) genres: Mapped[Optional[str]] = mapped_column(String())
last_mod: Mapped[float] = mapped_column(Integer()) last_mod: Mapped[float] = mapped_column(Integer())
og_album: Mapped[str] = mapped_column(String()) # og_album: Mapped[str] = mapped_column(String())
og_title: Mapped[str] = mapped_column(String()) # og_title: Mapped[str] = mapped_column(String())
title: Mapped[str] = mapped_column(String()) title: Mapped[str] = mapped_column(String())
track: Mapped[int] = mapped_column(Integer()) track: Mapped[int] = mapped_column(Integer())
trackhash: Mapped[str] = mapped_column(String(), index=True) trackhash: Mapped[str] = mapped_column(String(), index=True)
@@ -250,6 +250,19 @@ class TrackTable(Base):
TrackTable, TrackTable.trackhash, trackhash, duration, timestamp TrackTable, TrackTable.trackhash, trackhash, duration, timestamp
) )
# @classmethod
# def update_artist_separators(cls, separators: set[str]):
# tracks = cls.get_all()
# with DbEngine.manager(commit=True) as conn:
# for track in tracks:
# track.split_artists(separators)
# conn.execute(
# update(cls)
# .where(cls.trackhash == track.trackhash)
# .values(artists=track.artists, artisthashes=track.artisthashes)
# )
class AlbumTable(Base): class AlbumTable(Base):
__tablename__ = "album" __tablename__ = "album"
+4 -3
View File
@@ -1,5 +1,6 @@
from typing import Any from typing import Any
from app.config import UserConfig
from app.models import Album as AlbumModel, Artist as ArtistModel, Track as TrackModel from app.models import Album as AlbumModel, Artist as ArtistModel, Track as TrackModel
from app.models.favorite import Favorite from app.models.favorite import Favorite
from app.models.lastfm import SimilarArtist from app.models.lastfm import SimilarArtist
@@ -9,12 +10,12 @@ from app.models.plugins import Plugin
from app.models.user import User from app.models.user import User
def track_to_dataclass(track: Any): def track_to_dataclass(track: Any, config: UserConfig):
return TrackModel(**track._asdict()) return TrackModel(**track._asdict(), config=config)
def tracks_to_dataclasses(tracks: Any): def tracks_to_dataclasses(tracks: Any):
return [track_to_dataclass(track) for track in tracks] return [track_to_dataclass(track, UserConfig()) for track in tracks]
def album_to_dataclass(album: Any): def album_to_dataclass(album: Any):
+1 -1
View File
@@ -124,7 +124,7 @@ class IndexTracks:
log.warning("'Populate.tag_untagged': Populate key changed") log.warning("'Populate.tag_untagged': Populate key changed")
return return
tags = get_tags(file, artist_separators=config.artistSeparators) tags = get_tags(file, config=config)
if tags is not None: if tags is not None:
TrackTable.insert_one(tags) TrackTable.insert_one(tags)
+89 -88
View File
@@ -14,13 +14,7 @@ from tinytag import TinyTag
from app.config import UserConfig from app.config import UserConfig
from app.settings import Defaults, Paths from app.settings import Defaults, Paths
from app.utils.hashing import create_hash from app.utils.hashing import create_hash
from app.utils.parsers import ( from app.utils.parsers import split_artists
clean_title,
get_base_title_and_versions,
parse_feat_from_title,
remove_prod,
split_artists,
)
from app.utils.wintools import win_replace_slash from app.utils.wintools import win_replace_slash
@@ -109,13 +103,13 @@ def clean_filename(filename: str):
class ParseData: class ParseData:
artist: str artist: str
title: str title: str
artist_separators: set[str] config: UserConfig
def __post_init__(self): def __post_init__(self):
self.artist = split_artists(self.artist, self.artist_separators) self.artist = split_artists(self.artist, self.config)
def extract_artist_title(filename: str, artist_separators: set[str]): def extract_artist_title(filename: str, config: UserConfig):
path = Path(filename).with_suffix("") path = Path(filename).with_suffix("")
path = clean_filename(str(path)) path = clean_filename(str(path))
@@ -123,24 +117,30 @@ def extract_artist_title(filename: str, artist_separators: set[str]):
split_result = [x.strip() for x in split_result] split_result = [x.strip() for x in split_result]
if len(split_result) == 1: if len(split_result) == 1:
return ParseData("", split_result[0], artist_separators) return ParseData(
"",
split_result[0],
config,
)
if len(split_result) > 2: if len(split_result) > 2:
try: try:
int(split_result[0]) int(split_result[0])
return ParseData( return ParseData(
split_result[1], " - ".join(split_result[2:]), artist_separators split_result[1],
" - ".join(split_result[2:]),
config,
) )
except ValueError: except ValueError:
pass pass
artist = split_result[0] artist = split_result[0]
title = split_result[1] title = split_result[1]
return ParseData(artist, title, artist_separators) return ParseData(artist, title, config)
def get_tags(filepath: str, artist_separators: set[str]): def get_tags(filepath: str, config: UserConfig):
""" """
Returns the tags for a given audio file. Returns the tags for a given audio file.
""" """
@@ -173,17 +173,20 @@ def get_tags(filepath: str, artist_separators: set[str]):
for tag in to_filename: for tag in to_filename:
p = getattr(tags, tag) p = getattr(tags, tag)
if p == "" or p is None: if p == "" or p is None:
parse_data = extract_artist_title(filename, artist_separators) parse_data = extract_artist_title(filename, config)
title = parse_data.title title = parse_data.title.replace("_", " ")
setattr(tags, tag, title) setattr(tags, tag, title)
# tags.title = tags.title.replace("_", " ")
# tags.album = tags.album.replace("_", " ")
parse = ["artist", "albumartist"] parse = ["artist", "albumartist"]
for tag in parse: for tag in parse:
p = getattr(tags, tag) p = getattr(tags, tag)
if p == "" or p is None: if p == "" or p is None:
if not parse_data: if not parse_data:
parse_data = extract_artist_title(filename, artist_separators) parse_data = extract_artist_title(filename, config)
artist = parse_data.artist artist = parse_data.artist
@@ -229,112 +232,110 @@ def get_tags(filepath: str, artist_separators: set[str]):
tags.artists = tags.artist tags.artists = tags.artist
tags.albumartists = tags.albumartist tags.albumartists = tags.albumartist
split_artist = split_artists(tags.artist, separators=artist_separators) # split_artist = split_artists(tags.artist, separators=config.artistSeparators)
split_albumartists = split_artists(tags.albumartist, separators=artist_separators) # split_albumartists = split_artists(tags.albumartist, separators=config.artistSeparators)
new_title = tags.title # new_title = tags.title
# TODO: Figure out which is the best spot to create these hashes # TODO: Figure out which is the best spot to create these hashes
# create albumhash using og_album # create albumhash using og_album
tags.albumhash = create_hash(tags.album or "", tags.albumartist) tags.albumhash = create_hash(tags.album or "", tags.albumartist)
config = UserConfig()
# extract featured artists # extract featured artists
if config.extractFeaturedArtists: # if config.extractFeaturedArtists:
feat, new_title = parse_feat_from_title( # feat, new_title = parse_feat_from_title(
tags.title, separators=artist_separators # tags.title, separators=config.artistSeparators
) # )
original_lower = "-".join([create_hash(a) for a in split_artist]) # original_lower = "-".join([create_hash(a) for a in split_artist])
split_artist.extend(a for a in feat if create_hash(a) not in original_lower) # split_artist.extend(a for a in feat if create_hash(a) not in original_lower)
# if no albumartist, assign to the first artist # if no albumartist, assign to the first artist
if not tags.albumartist: if not tags.albumartist:
tags.albumartist = split_artist[:1] tags.albumartist = split_artists(tags.artist, config)[:1]
# create json objects for artists and albumartists # create json objects for artists and albumartists
tags.artists = [ # tags.artists = [
{ # {
"artisthash": create_hash(a, decode=True), # "artisthash": create_hash(a, decode=True),
"name": a, # "name": a,
} # }
for a in split_artist # for a in split_artist
] # ]
tags.albumartists = [ # tags.albumartists = [
{ # {
"artisthash": create_hash(a, decode=True), # "artisthash": create_hash(a, decode=True),
"name": a, # "name": a,
} # }
for a in split_albumartists # for a in split_albumartists
] # ]
tags.artisthashes = list( # tags.artisthashes = list(
{a["artisthash"] for a in tags.artists} # {a["artisthash"] for a in tags.artists}
) # )
# remove prod by # remove prod by
if config.removeProdBy: # if config.removeProdBy:
new_title = remove_prod(new_title) # new_title = remove_prod(new_title)
# if track is a single, ie. # if track is a single, ie.
# if og_title == album, rename album to new_title # if og_title == album, rename album to new_title
if tags.title == tags.album: # if tags.title == tags.album:
tags.album = new_title # tags.album = new_title
# remove remaster from track title # remove remaster from track title
if config.removeRemasterInfo: # if config.removeRemasterInfo:
new_title = clean_title(new_title) # new_title = clean_title(new_title)
# save final title # save final title
tags.og_title = tags.title # tags.og_title = tags.title
tags.title = new_title # tags.title = new_title
tags.og_album = tags.album # tags.og_album = tags.album
# clean album title # clean album title
if config.cleanAlbumTitle: # if config.cleanAlbumTitle:
tags.album, _ = get_base_title_and_versions(tags.album, get_versions=False) # tags.album, _ = get_base_title_and_versions(tags.album, get_versions=False)
# merge album versions # merge album versions
if config.mergeAlbums: # if config.mergeAlbums:
tags.albumhash = create_hash( # tags.albumhash = create_hash(
tags.album, *(a["name"] for a in tags.albumartists) # tags.album, *(a["name"] for a in tags.albumartists)
) # )
# process genres # process genres
if tags.genre: # if tags.genre:
src_genres: str = tags.genre # src_genres: str = tags.genre
src_genres = src_genres.lower() # src_genres = src_genres.lower()
# separators = {"/", ";", "&"} # # separators = {"/", ";", "&"}
separators = set(config.genreSeparators) # separators = set(config.genreSeparators)
contains_rnb = "r&b" in src_genres # contains_rnb = "r&b" in src_genres
contains_rock = "rock & roll" in src_genres # contains_rock = "rock & roll" in src_genres
if contains_rnb: # if contains_rnb:
src_genres = src_genres.replace("r&b", "RnB") # src_genres = src_genres.replace("r&b", "RnB")
if contains_rock: # if contains_rock:
src_genres = src_genres.replace("rock & roll", "rock") # src_genres = src_genres.replace("rock & roll", "rock")
for s in separators: # for s in separators:
src_genres = src_genres.replace(s, ",") # src_genres = src_genres.replace(s, ",")
genres_list: list[str] = src_genres.split(",") # genres_list: list[str] = src_genres.split(",")
tags.genres = [ # tags.genres = [
{"name": g.strip(), "genrehash": create_hash(g.strip())} # {"name": g.strip(), "genrehash": create_hash(g.strip())}
for g in genres_list # for g in genres_list
] # ]
tags.genrehashes = [g["genrehash"] for g in tags.genres] # tags.genrehashes = [g["genrehash"] for g in tags.genres]
else: # else:
tags.genres = [] # tags.genres = []
tags.genrehashes = [] # tags.genrehashes = []
tags.genres = tags.genre
# sub underscore with space # sub underscore with space
tags.title = tags.title.replace("_", " ") # tags.title = tags.title.replace("_", " ")
tags.album = tags.album.replace("_", " ") # tags.album = tags.album.replace("_", " ")
tags.trackhash = create_hash( tags.trackhash = create_hash(tags.artists, tags.album, tags.title)
*[a["name"] for a in tags.artists], tags.album, tags.title
)
more_extra = { more_extra = {
"audio_offset": tags.audio_offset, "audio_offset": tags.audio_offset,
+125 -5
View File
@@ -1,6 +1,15 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from app.config import UserConfig
from app.utils.auth import get_current_userid from app.utils.auth import get_current_userid
from app.utils.hashing import create_hash
from app.utils.parsers import (
clean_title,
get_base_title_and_versions,
parse_feat_from_title,
remove_prod,
split_artists,
)
@dataclass(slots=True) @dataclass(slots=True)
@@ -13,7 +22,6 @@ class Track:
album: str album: str
albumartists: list[dict[str, str]] albumartists: list[dict[str, str]]
albumhash: str albumhash: str
artisthashes: list[str]
artists: list[dict[str, str]] artists: list[dict[str, str]]
bitrate: int bitrate: int
copyright: str copyright: str
@@ -22,11 +30,8 @@ class Track:
duration: int duration: int
filepath: str filepath: str
folder: str folder: str
genres: list[dict[str, str]] genres: str | list[dict[str, str]]
genrehashes: list[str]
last_mod: int last_mod: int
og_album: str
og_title: str
title: str title: str
track: int track: int
trackhash: str trackhash: str
@@ -35,6 +40,12 @@ class Track:
playcount: int playcount: int
playduration: 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)
_pos: int = 0 _pos: int = 0
_ati: str = "" _ati: str = ""
image: str = "" image: str = ""
@@ -55,9 +66,118 @@ class Track:
self.fav_userids.append(userid) self.fav_userids.append(userid)
def __post_init__(self): def __post_init__(self):
self.og_title = self.title
self.og_album = self.album
self.image = self.albumhash + ".webp" self.image = self.albumhash + ".webp"
self.extra = { self.extra = {
"disc_total": self.extra.get("disc_total", 0), "disc_total": self.extra.get("disc_total", 0),
"track_total": self.extra.get("track_total", 0), "track_total": self.extra.get("track_total", 0),
"samplerate": self.extra.get("samplerate", -1), "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.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):
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):
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]
-2
View File
@@ -215,8 +215,6 @@ class TrackStore:
def get_tracks_by_filepaths(cls, paths: list[str]) -> list[Track]: def get_tracks_by_filepaths(cls, paths: list[str]) -> list[Track]:
""" """
Returns all tracks matching the given paths. Returns all tracks matching the given paths.
⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔
""" """
# tracks = sorted(cls.trackhashmap, key=lambda x: x.filepath) # tracks = sorted(cls.trackhashmap, key=lambda x: x.filepath)
# tracks = use_bisection(tracks, "filepath", paths) # tracks = use_bisection(tracks, "filepath", paths)
+44 -9
View File
@@ -1,19 +1,54 @@
import re import re
from app.config import UserConfig
from app.enums.album_versions import AlbumVersionEnum, get_all_keywords from app.enums.album_versions import AlbumVersionEnum, get_all_keywords
def split_artists(src: str, separators: set[str]): def split_artists(src: str, config: UserConfig):
""" """
Splits a string of artists into a list of artists. Splits a string of artists into a list of artists, preserving those in ignoreList.
Case-insensitive matching is used for the ignoreList.
""" """
for sep in separators: result = []
src = src.replace(sep, ",") current = ""
i = 0
artists = src.split(",") while i < len(src):
artists = [a.strip() for a in artists] # Check if any ignored artist starts at this position (case-insensitive)
ignored_match = next(
(
src[i : i + len(ignored)]
for ignored in config.artistSplitIgnoreList
if src.lower().startswith(ignored.lower(), i)
),
None,
)
return [a for a in artists if a] if ignored_match:
# If we have accumulated any current string, add it to result
if current.strip():
result.extend([a.strip() for a in current.split(",") if a.strip()])
current = ""
# Add the ignored artist to the result (preserving original case)
result.append(ignored_match)
# Move past the ignored artist
i += len(ignored_match)
elif src[i] in config.artistSeparators:
# If we encounter a separator, process the current string
if current.strip():
result.extend([a.strip() for a in current.split(",") if a.strip()])
current = ""
i += 1
else:
# If it's not an ignored artist or a separator, add to current
current += src[i]
i += 1
# Process any remaining current string
if current.strip():
result.extend([a.strip() for a in current.split(",") if a.strip()])
return result
def remove_prod(title: str) -> str: def remove_prod(title: str) -> str:
@@ -36,7 +71,7 @@ def remove_prod(title: str) -> str:
return title.strip() return title.strip()
def parse_feat_from_title(title: str, separators: set[str]) -> tuple[list[str], str]: def parse_feat_from_title(title: str, config: UserConfig) -> tuple[list[str], str]:
""" """
Extracts featured artists from a song title using regex. Extracts featured artists from a song title using regex.
""" """
@@ -54,7 +89,7 @@ def parse_feat_from_title(title: str, separators: set[str]) -> tuple[list[str],
return [], title return [], title
artists = match.group(1) artists = match.group(1)
artists = split_artists(artists, separators) artists = split_artists(artists, config)
# remove "feat" group from title # remove "feat" group from title
new_title = re.sub(regex, "", title, flags=re.IGNORECASE) new_title = re.sub(regex, "", title, flags=re.IGNORECASE)
+137
View File
@@ -0,0 +1,137 @@
import unittest
def split_artists(src: str, separators: set[str], ignoreList: set[str] = set()):
"""
Splits a string of artists into a list of artists, preserving those in ignoreList.
Case-insensitive matching is used for the ignoreList.
"""
result = []
current = ""
i = 0
# Convert ignoreList to lowercase for case-insensitive matching
ignore_lower = {artist.lower() for artist in ignoreList}
while i < len(src):
# Check if any ignored artist starts at this position (case-insensitive)
ignored_match = next(
(
src[i:i+len(ignored)]
for ignored in ignoreList
if src.lower().startswith(ignored.lower(), i)
),
None
)
if ignored_match:
# If we have accumulated any current string, add it to result
if current.strip():
result.extend([a.strip() for a in current.split(',') if a.strip()])
current = ""
# Add the ignored artist to the result (preserving original case)
result.append(ignored_match)
# Move past the ignored artist
i += len(ignored_match)
elif src[i] in separators:
# If we encounter a separator, process the current string
if current.strip():
result.extend([a.strip() for a in current.split(',') if a.strip()])
current = ""
i += 1
else:
# If it's not an ignored artist or a separator, add to current
current += src[i]
i += 1
# Process any remaining current string
if current.strip():
result.extend([a.strip() for a in current.split(',') if a.strip()])
return result
class TestSplitArtists(unittest.TestCase):
def test_basic_splitting(self):
self.assertEqual(
split_artists("Beatles, Queen; Rolling Stones", {";"}),
["Beatles", "Queen", "Rolling Stones"],
)
def test_multiple_separators(self):
self.assertEqual(
split_artists("Beatles; Queen & Rolling Stones | ABBA", {";", "&", "|"}),
["Beatles", "Queen", "Rolling Stones", "ABBA"],
)
def test_ignore_list(self):
self.assertEqual(
split_artists(
"Beatles; Earth, Wind & Fire; Queen", {";", "&"}, {"Earth, Wind & Fire"}
),
["Beatles", "Earth, Wind & Fire", "Queen"],
)
def test_empty_string(self):
self.assertEqual(split_artists("", {";"}), [])
def test_only_separators(self):
self.assertEqual(split_artists(";;;", {";"}), [])
def test_extra_spaces(self):
self.assertEqual(
split_artists(" Beatles ; Queen ", {";"}), ["Beatles", "Queen"]
)
def test_comma_splitting(self):
self.assertEqual(
split_artists("Beatles, Queen; Rolling Stones, ABBA", {";"}),
["Beatles", "Queen", "Rolling Stones", "ABBA"],
)
def test_ignore_list_with_comma(self):
self.assertEqual(
split_artists(
"Beatles; Earth, Wind & Fire, Queen", {";"}, {"Earth, Wind & Fire"}
),
["Beatles", "Earth, Wind & Fire", "Queen"],
)
def test_ignore_list_with_separator(self):
self.assertEqual(
split_artists("Beatles; AC/DC", {"/", ";"}, {"AC/DC"}), ["Beatles", "AC/DC"]
)
def test_ignore_list_at_start(self):
self.assertEqual(
split_artists("AC/DC; Beatles", {"/", ";"}, {"AC/DC"}), ["AC/DC", "Beatles"]
)
def test_ignore_list_at_end(self):
self.assertEqual(
split_artists("Beatles; AC/DC", {"/", ";"}, {"AC/DC"}), ["Beatles", "AC/DC"]
)
def test_multiple_ignored_artists(self):
self.assertEqual(
split_artists(
"Beatles; AC/DC; Guns N' Roses; Queen",
{"/", ";", "'"},
{"AC/DC", "Guns N' Roses"},
),
["Beatles", "AC/DC", "Guns N' Roses", "Queen"],
)
def test_bob_marley(self):
self.assertEqual(
split_artists(
"Bob marley & The wailers; Beatles",
{";", "&"},
{"Bob marley & the wailers"},
),
["Bob marley & The wailers", "Beatles"],
)
if __name__ == "__main__":
unittest.main()