from dataclasses import asdict, dataclass, field from app.config import UserConfig 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) 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()})