diff --git a/src/music_kraken/objects/collection.py b/src/music_kraken/objects/collection.py index 405b12c..0908bf9 100644 --- a/src/music_kraken/objects/collection.py +++ b/src/music_kraken/objects/collection.py @@ -84,7 +84,8 @@ class Collection: return self._data[item] - def copy(self) -> List: + @property + def shallow_list(self) -> List[DatabaseObject]: """ returns a shallow copy of the data list """ diff --git a/src/music_kraken/objects/lyrics.py b/src/music_kraken/objects/lyrics.py index afb9296..fcd9ea5 100644 --- a/src/music_kraken/objects/lyrics.py +++ b/src/music_kraken/objects/lyrics.py @@ -3,12 +3,12 @@ from typing import List import pycountry from .parents import DatabaseObject -from .source import SourceAttribute, Source -from .metadata import MetadataAttribute +from .source import Source, SourceCollection +from .metadata import Metadata from .formatted_text import FormattedText -class Lyrics(DatabaseObject, SourceAttribute, MetadataAttribute): +class Lyrics(DatabaseObject): def __init__( self, text: FormattedText, @@ -23,5 +23,4 @@ class Lyrics(DatabaseObject, SourceAttribute, MetadataAttribute): self.text: FormattedText = text self.language: pycountry.Languages = language - if source_list is not None: - self.source_list = source_list + self.source_collection: SourceCollection = SourceCollection(source_list) diff --git a/src/music_kraken/objects/metadata.py b/src/music_kraken/objects/metadata.py index 19298c8..1c7b294 100644 --- a/src/music_kraken/objects/metadata.py +++ b/src/music_kraken/objects/metadata.py @@ -1,7 +1,6 @@ from enum import Enum from typing import List, Dict, Tuple -import dateutil.tz from mutagen import id3 import datetime @@ -267,126 +266,113 @@ class ID3Timestamp: timeformat: str = property(fget=get_time_format) -class MetadataAttribute: - """ - This class shall be added to any object, which can return data for tagging - """ +class Metadata: + # it's a null byte for the later concatenation of text frames + NULL_BYTE: str = "\x00" + # this is pretty self-explanatory + # the key is an enum from Mapping + # the value is a list with each value + # the mutagen object for each frame will be generated dynamically + id3_dict: Dict[any, list] - class Metadata: - # it's a null byte for the later concatenation of text frames - NULL_BYTE: str = "\x00" - # this is pretty self-explanatory - # the key is an enum from Mapping - # the value is a list with each value - # the mutagen object for each frame will be generated dynamically - id3_dict: Dict[any, list] + def __init__(self, id3_dict: Dict[any, list] = None) -> None: + self.id3_dict = dict() + if id3_dict is not None: + self.add_metadata_dict(id3_dict) - def __init__(self, id3_dict: Dict[any, list] = None) -> None: - self.id3_dict = dict() - if id3_dict is not None: - self.add_metadata_dict(id3_dict) + def __setitem__(self, frame, value_list: list, override_existing: bool = True): + if type(value_list) != list: + raise ValueError(f"can only set attribute to list, not {type(value_list)}") - def __setitem__(self, frame, value_list: list, override_existing: bool = True): - if type(value_list) != list: - raise ValueError(f"can only set attribute to list, not {type(value_list)}") + new_val = [i for i in value_list if i not in {None, ''}] - new_val = [i for i in value_list if i not in {None, ''}] + if len(new_val) == 0: + return - if len(new_val) == 0: + if override_existing: + self.id3_dict[frame] = new_val + else: + if frame not in self.id3_dict: + self.id3_dict[frame] = new_val return - if override_existing: - self.id3_dict[frame] = new_val - else: - if frame not in self.id3_dict: - self.id3_dict[frame] = new_val - return + self.id3_dict[frame].extend(new_val) - self.id3_dict[frame].extend(new_val) + def __getitem__(self, key): + if key not in self.id3_dict: + return None + return self.id3_dict[key] - def __getitem__(self, key): - if key not in self.id3_dict: - return None - return self.id3_dict[key] + def delete_field(self, key: str): + if key in self.id3_dict: + return self.id3_dict.pop(key) - def delete_field(self, key: str): - if key in self.id3_dict: - return self.id3_dict.pop(key) + def add_metadata_dict(self, metadata_dict: dict, override_existing: bool = True): + for field_enum, value in metadata_dict.items(): + self.__setitem__(field_enum, value, override_existing=override_existing) - def add_metadata_dict(self, metadata_dict: dict, override_existing: bool = True): - for field_enum, value in metadata_dict.items(): - self.__setitem__(field_enum, value, override_existing=override_existing) - - def merge(self, other, override_existing: bool = False): - """ - adds the values of another metadata obj to this one - - other is a value of the type MetadataAttribute.Metadata - """ - - self.add_metadata_dict(other.id3_dict, override_existing=override_existing) - - def merge_many(self, many_other): - """ - adds the values of many other metadata objects to this one - """ - - for other in many_other: - self.merge(other) - - def get_id3_value(self, field): - if field not in self.id3_dict: - return None - - list_data = self.id3_dict[field] - - # convert for example the time objects to timestamps - for i, element in enumerate(list_data): - # for performance’s sake I don't do other checks if it is already the right type - if type(element) == str: - continue - - if type(element) in {int}: - list_data[i] = str(element) - - if type(element) == ID3Timestamp: - list_data[i] = element.timestamp - continue - - """ - Version 2.4 of the specification prescribes that all text fields (the fields that start with a T, except for TXXX) can contain multiple values separated by a null character. - Thus if above conditions are met, I concatenate the list, - else I take the first element - """ - if field.value[0].upper() == "T" and field.value.upper() != "TXXX": - return self.NULL_BYTE.join(list_data) - - return list_data[0] - - def get_mutagen_object(self, field): - return Mapping.get_mutagen_instance(field, self.get_id3_value(field)) - - def __str__(self) -> str: - rows = [] - for key, value in self.id3_dict.items(): - rows.append(f"{key} - {str(value)}") - return "\n".join(rows) - - def __iter__(self): - """ - returns a generator, you can iterate through, - to directly tagg a file with id3 container. - """ - # set the tagging timestamp to the current time - self.__setitem__(Mapping.TAGGING_TIME, [ID3Timestamp.now()]) - - for field in self.id3_dict: - yield self.get_mutagen_object(field) - - def get_metadata(self) -> Metadata: + def merge(self, other, override_existing: bool = False): """ - this is intendet to be overwritten by the child class - """ - return MetadataAttribute.Metadata() + adds the values of another metadata obj to this one - metadata = property(fget=lambda self: self.get_metadata()) + other is a value of the type MetadataAttribute.Metadata + """ + + self.add_metadata_dict(other.id3_dict, override_existing=override_existing) + + def merge_many(self, many_other): + """ + adds the values of many other metadata objects to this one + """ + + for other in many_other: + self.merge(other) + + def get_id3_value(self, field): + if field not in self.id3_dict: + return None + + list_data = self.id3_dict[field] + + # convert for example the time objects to timestamps + for i, element in enumerate(list_data): + # for performance’s sake I don't do other checks if it is already the right type + if type(element) == str: + continue + + if type(element) in {int}: + list_data[i] = str(element) + + if type(element) == ID3Timestamp: + list_data[i] = element.timestamp + continue + + """ + Version 2.4 of the specification prescribes that all text fields (the fields that start with a T, except for TXXX) can contain multiple values separated by a null character. + Thus if above conditions are met, I concatenate the list, + else I take the first element + """ + if field.value[0].upper() == "T" and field.value.upper() != "TXXX": + return self.NULL_BYTE.join(list_data) + + return list_data[0] + + def get_mutagen_object(self, field): + return Mapping.get_mutagen_instance(field, self.get_id3_value(field)) + + def __str__(self) -> str: + rows = [] + for key, value in self.id3_dict.items(): + rows.append(f"{key} - {str(value)}") + return "\n".join(rows) + + def __iter__(self): + """ + returns a generator, you can iterate through, + to directly tagg a file with id3 container. + """ + # set the tagging timestamp to the current time + self.__setitem__(Mapping.TAGGING_TIME, [ID3Timestamp.now()]) + + for field in self.id3_dict: + yield self.get_mutagen_object(field) diff --git a/src/music_kraken/objects/parents.py b/src/music_kraken/objects/parents.py index e194fcc..f7963d9 100644 --- a/src/music_kraken/objects/parents.py +++ b/src/music_kraken/objects/parents.py @@ -5,6 +5,7 @@ import uuid from ..utils.shared import ( SONG_LOGGER as LOGGER ) +from .metadata import Metadata class DatabaseObject: @@ -69,6 +70,10 @@ class DatabaseObject: if override or getattr(self, simple_attribute) is None: setattr(self, simple_attribute, getattr(other, simple_attribute)) + @property + def metadata(self) -> Metadata: + return Metadata() + class MainObject(DatabaseObject): """ diff --git a/src/music_kraken/objects/song.py b/src/music_kraken/objects/song.py index 84b6d41..13378a0 100644 --- a/src/music_kraken/objects/song.py +++ b/src/music_kraken/objects/song.py @@ -5,7 +5,7 @@ import pycountry from .metadata import ( Mapping as id3Mapping, ID3Timestamp, - MetadataAttribute + Metadata ) from ..utils.shared import ( MUSIC_DIR, @@ -36,12 +36,10 @@ All Objects dependent CountryTyping = type(list(pycountry.countries)[0]) -class Song(MainObject, MetadataAttribute): +class Song(MainObject): """ Class representing a song object, with attributes id, mb_id, title, album_name, isrc, length, tracksort, genre, source_list, target, lyrics_list, album, main_artist_list, and feature_artist_list. - - Inherits from DatabaseObject, SourceAttribute, and MetadataAttribute classes. """ COLLECTION_ATTRIBUTES = ("lyrics_collection", "album_collection", "main_artist_collection", "feature_artist_collection", "source_collection") @@ -65,9 +63,6 @@ class Song(MainObject, MetadataAttribute): feature_artist_list: List[Type['Artist']] = None, **kwargs ) -> None: - """ - Initializes the Song object with the following attributes: - """ MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) # attributes self.title: str = title @@ -90,6 +85,33 @@ class Song(MainObject, MetadataAttribute): self.feature_artist_collection = Collection(data=feature_artist_list, element_type=Artist) + @property + def indexing_values(self) -> List[Tuple[str, object]]: + return [ + ('id', self.id), + ('title', self.unified_title), + ('isrc', self.isrc.strip()), + *[('url', source.url) for source in self.source_list] + ] + + @property + def metadata(self) -> Metadata: + metadata = Metadata({ + id3Mapping.TITLE: [self.title], + id3Mapping.ISRC: [self.isrc], + id3Mapping.LENGTH: [self.length], + id3Mapping.GENRE: [self.genre], + id3Mapping.TRACKNUMBER: [self.tracksort_str] + }) + + metadata.merge_many([s.get_song_metadata() for s in self.source_list]) + metadata.merge_many([a.metadata for a in self.album_collection]) + metadata.merge_many([a.metadata for a in self.main_artist_collection]) + metadata.merge_many([a.metadata for a in self.feature_artist_collection]) + metadata.merge_many([lyrics.metadata for lyrics in self.lyrics_collection]) + + return metadata + def __eq__(self, other): if type(other) != type(self): return False @@ -114,39 +136,14 @@ class Song(MainObject, MetadataAttribute): def __repr__(self) -> str: return f"Song(\"{self.title}\")" - def get_tracksort_str(self): + @property + def tracksort_str(self) -> str: """ if the album tracklist is empty, it sets it length to 1, this song has to be in the Album :returns id3_tracksort: {song_position}/{album.length_of_tracklist} """ return f"{self.tracksort}/{len(self.album.tracklist) or 1}" - @property - def indexing_values(self) -> List[Tuple[str, object]]: - return [ - ('id', self.id), - ('title', self.unified_title), - ('isrc', self.isrc.strip()), - *[('url', source.url) for source in self.source_list] - ] - - def get_metadata(self) -> MetadataAttribute.Metadata: - metadata = MetadataAttribute.Metadata({ - id3Mapping.TITLE: [self.title], - id3Mapping.ISRC: [self.isrc], - id3Mapping.LENGTH: [self.length], - id3Mapping.GENRE: [self.genre], - id3Mapping.TRACKNUMBER: [self.tracksort_str] - }) - - metadata.merge_many([s.get_song_metadata() for s in self.source_list]) - metadata.merge_many([a.metadata for a in self.album_collection]) - metadata.merge_many([a.metadata for a in self.main_artist_collection]) - metadata.merge_many([a.metadata for a in self.feature_artist_collection]) - metadata.merge_many([lyrics.metadata for lyrics in self.lyrics_collection]) - - return metadata - def get_options(self) -> list: """ Return a list of related objects including the song object, album object, main artist objects, and feature artist objects. @@ -162,15 +159,13 @@ class Song(MainObject, MetadataAttribute): def get_option_string(self) -> str: return f"Song({self.title}) of Album({self.album.title}) from Artists({self.get_artist_credits()})" - tracksort_str: List[Type['Album']] = property(fget=get_tracksort_str) - """ All objects dependent on Album """ -class Album(MainObject, MetadataAttribute): +class Album(MainObject): COLLECTION_ATTRIBUTES = ("label_collection", "artist_collection", "song_collection") SIMPLE_ATTRIBUTES = ("title", "album_status", "album_type", "language", "date", "barcode", "albumsort") @@ -232,6 +227,16 @@ class Album(MainObject, MetadataAttribute): ('barcode', self.barcode), *[('url', source.url) for source in self.source_list] ] + + @property + def metadata(self) -> Metadata: + return Metadata({ + id3Mapping.ALBUM: [self.title], + id3Mapping.COPYRIGHT: [self.copyright], + id3Mapping.LANGUAGE: [self.iso_639_2_language], + id3Mapping.ALBUM_ARTIST: [a.name for a in self.artist_collection], + id3Mapping.DATE: [self.date.timestamp] + }) def __repr__(self): return f"Album(\"{self.title}\")" @@ -268,16 +273,10 @@ class Album(MainObject, MetadataAttribute): continue song.tracksort = i + 1 - def get_metadata(self) -> MetadataAttribute.Metadata: - return MetadataAttribute.Metadata({ - id3Mapping.ALBUM: [self.title], - id3Mapping.COPYRIGHT: [self.copyright], - id3Mapping.LANGUAGE: [self.iso_639_2_language], - id3Mapping.ALBUM_ARTIST: [a.name for a in self.artist_collection], - id3Mapping.DATE: [self.date.timestamp] - }) - def get_copyright(self) -> str: + + @property + def copyright(self) -> str: if self.date is None: return "" if self.date.has_year or len(self.label_collection) == 0: @@ -311,10 +310,6 @@ class Album(MainObject, MetadataAttribute): def get_option_string(self) -> str: return f"Album: {self.title}; Artists {', '.join([i.name for i in self.artist_collection])}" - - tracklist: List[Song] = property(fget=lambda self: self.song_collection.copy()) - - copyright = property(fget=get_copyright) """ @@ -322,7 +317,7 @@ All objects dependent on Artist """ -class Artist(MainObject, MetadataAttribute): +class Artist(MainObject): COLLECTION_ATTRIBUTES = ("feature_song_collection", "main_album_collection", "label_collection") SIMPLE_ATTRIBUTES = ("name", "name", "country", "formed_in", "notes", "lyrical_themes", "general_genre") @@ -381,6 +376,15 @@ class Artist(MainObject, MetadataAttribute): ('name', self.unified_name), *[('url', source.url) for source in self.source_list] ] + + @property + def metadata(self) -> Metadata: + metadata = Metadata({ + id3Mapping.ARTIST: [self.name] + }) + metadata.merge_many([s.get_artist_metadata() for s in self.source_list]) + + return metadata def __str__(self): string = self.name or "" @@ -425,14 +429,6 @@ class Artist(MainObject, MetadataAttribute): song_list=self.feature_song_collection.copy() ) - def get_metadata(self) -> MetadataAttribute.Metadata: - metadata = MetadataAttribute.Metadata({ - id3Mapping.ARTIST: [self.name] - }) - metadata.merge_many([s.get_artist_metadata() for s in self.source_list]) - - return metadata - def get_options(self) -> list: options = [self] options.extend(self.main_album_collection) @@ -466,7 +462,7 @@ Label """ -class Label(MainObject, SourceAttribute, MetadataAttribute): +class Label(MainObject, SourceAttribute): COLLECTION_ATTRIBUTES = ("album_collection", "current_artist_collection") SIMPLE_ATTRIBUTES = ("name",) diff --git a/src/music_kraken/objects/source.py b/src/music_kraken/objects/source.py index 9659e35..3b4a431 100644 --- a/src/music_kraken/objects/source.py +++ b/src/music_kraken/objects/source.py @@ -2,7 +2,7 @@ from collections import defaultdict from enum import Enum from typing import List, Dict, Tuple -from .metadata import Mapping, MetadataAttribute +from .metadata import Mapping, Metadata from .parents import DatabaseObject from .collection import Collection @@ -106,14 +106,15 @@ class Source(DatabaseObject, MetadataAttribute): Mapping.ARTIST_WEBPAGE_URL: [self.url] }) - def get_metadata(self) -> MetadataAttribute.Metadata: + @property + def metadata(self) -> Metadata: if self.type_enum == SourceTypes.SONG: return self.get_song_metadata() if self.type_enum == SourceTypes.ARTIST: return self.get_artist_metadata() - return super().get_metadata() + return super().metadata @property def indexing_values(self) -> List[Tuple[str, object]]: