From bc69cac27cd5caa9ee1fc51dbe04b78052292cbf Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Fri, 27 Jan 2023 12:56:59 +0100 Subject: [PATCH] metadata --- src/music_kraken/database/objects/metadata.py | 177 ++++++++++-------- src/music_kraken/database/objects/song.py | 71 +++---- src/music_kraken/database/objects/source.py | 12 +- 3 files changed, 135 insertions(+), 125 deletions(-) diff --git a/src/music_kraken/database/objects/metadata.py b/src/music_kraken/database/objects/metadata.py index 462de15..04b2d73 100644 --- a/src/music_kraken/database/objects/metadata.py +++ b/src/music_kraken/database/objects/metadata.py @@ -247,100 +247,123 @@ class ID3Timestamp: timestamp: str = property(fget=get_timestamp) -class Metadata: +class MetadataAttribute: """ - Shall only be read or edited via the Song object. - call it like a dict to read/write values + This class shall be added to any object, which can return data for tagging """ - def __init__(self, id3_dict: dict = None) -> None: - # this is pretty self-explanatory - # the key is a 4 letter key from the id3 standards like TITL - - self.id3_attributes: Dict[str, list] = {} - if id3_dict is not None: - self.add_metadata_dict(id3_dict) - + class Metadata: # its a null byte for the later concatenation of text frames - self.null_byte = "\x00" + 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] = dict() - def get_all_metadata(self): - return list(self.id3_attributes.items()) - def __setitem__(self, key: str, value: list, override_existing: bool = True): - if len(value) == 0: - return - if type(value) != list: - raise ValueError(f"can only set attribute to list, not {type(value)}") + def __init__(self, id3_dict: Dict[any, list] = None) -> None: + if id3_dict is not None: + self.add_metadata_dict(id3_dict) - if override_existing: - new_val = [] - for elem in value: - if elem is not None: - new_val.append(elem) - if len(new_val) > 0: - self.id3_attributes[key] = new_val - else: - if key not in self.id3_attributes: - self.id3_attributes[key] = value + def __setitem__(self, frame, value_list: list, override_existing: bool = True): + if len(value_list) == 0: return - self.id3_attributes[key].extend(value) + 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 is not None] + + if len(new_val) == 0: + 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 - def __getitem__(self, key): - if key not in self.id3_attributes: - return None - return self.id3_attributes[key] + self.id3_attributes[frame].extend(new_val) - 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, value, override_existing=override_existing) + def __getitem__(self, key): + if key not in self.id3_dict: + return None + return self.id3_dict[key] - def add_many_metadata_dict(self, id3_metadata_list: List[dict], override_existing: bool = False): - for metadata_dict in id3_metadata_list: - self.add_metadata_dict(metadata_dict, override_existing=override_existing) - def delete_item(self, key: str): - if key in self.id3_attributes: - return self.id3_attributes.pop(key) + def delete_field(self, key: str): + if key in self.id3_attributes: + return self.id3_attributes.pop(key) - def get_id3_value(self, key: str): - if key not in self.id3_attributes: - return None + 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, value, override_existing=override_existing) - list_data = self.id3_attributes[key] + def merge(self, other, override_existing: bool = False): + """ + adds the values of another metadata obj to this one + """ + 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) - # 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 + def get_id3_value(self, field): + if field not in self.id3_attributes: + return None - if type(element) == ID3Timestamp: - list_data[i] = element.timestamp - continue + 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) == 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_attributes.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.value, [ID3Timestamp.now()]) + + for field in self.id3_attributes: + yield self.get_mutagen_object(field) + + def get_metadata(self) -> Metadata: """ - 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 + this is intendet to be overwritten by the child class """ - if key[0].upper() == "T" and key.upper() != "TXXX": - return self.null_byte.join(list_data) + return self.Metadata() - return list_data[0] - - def get_mutagen_object(self, key: str): - return Mapping.get_mutagen_instance(Mapping(key), self.get_id3_value(key)) - - def __iter__(self): - # set the tagging timestamp to the current time - self.__setitem__(Mapping.TAGGING_TIME.value, [ID3Timestamp.now()]) - - for key in self.id3_attributes: - yield key, self.get_mutagen_object(key) - - def __str__(self) -> str: - rows = [] - for key, value in self.id3_attributes.items(): - rows.append(f"{key} - {str(value)}") - return "\n".join(rows) + metadata = property(fget=get_metadata) diff --git a/src/music_kraken/database/objects/song.py b/src/music_kraken/database/objects/song.py index 3d85247..154d0a1 100644 --- a/src/music_kraken/database/objects/song.py +++ b/src/music_kraken/database/objects/song.py @@ -21,7 +21,8 @@ from .parents import ( from .source import ( Source, SourceTypes, - SourcePages + SourcePages, + SourceAttribute ) """ @@ -79,15 +80,24 @@ class Target(DatabaseObject, SongAttribute): exists_on_disc = property(fget=get_exists_on_disc) -class Lyrics(DatabaseObject, SongAttribute): - def __init__(self, text: str, language: str, id_: str = None) -> None: +class Lyrics(DatabaseObject, SongAttribute, SourceAttribute): + def __init__( + self, + text: str, + language: str, + id_: str = None, + source_list: List[Source] = None + ) -> None: DatabaseObject.__init__(self, id_=id_) SongAttribute.__init__(self) self.text = text self.language = language + if source_list is not None: + self.source_list = source_list -class Song(DatabaseObject, ID3Metadata): + +class Song(DatabaseObject, ID3Metadata, SourceAttribute): def __init__( self, id_: str = None, @@ -98,7 +108,7 @@ class Song(DatabaseObject, ID3Metadata): length: int = None, tracksort: int = None, genre: str = None, - sources: List[Source] = None, + source_list: List[Source] = None, target: Target = None, lyrics: List[Lyrics] = None, album=None, @@ -122,8 +132,8 @@ class Song(DatabaseObject, ID3Metadata): self.tracksort: int | None = tracksort self.genre: str = genre - self._sources: List[Source] = [] - self.sources = sources + if source_list: + self.source_list = source_list self.album = album @@ -187,7 +197,7 @@ class Song(DatabaseObject, ID3Metadata): def get_metadata(self) -> Metadata: metadata = Metadata(self.get_id3_dict()) - metadata.add_many_metadata_dict([source.get_id3_dict() for source in self.sources]) + metadata.add_many_metadata_dict([source.get_id3_dict() for source in self.source_list]) if self.album is not None: metadata.add_metadata_dict(self.album.get_id3_dict()) metadata.add_many_metadata_dict([artist.get_id3_dict() for artist in self.main_artist_list]) @@ -195,15 +205,6 @@ class Song(DatabaseObject, ID3Metadata): return metadata - def set_sources(self, source_list: List[Source]): - if source_list is None: - return - - self._sources = source_list - for source in self._sources: - source.type_enum = SourceTypes.SONG - - sources: List[Source] = property(fget=lambda self: self._sources, fset=set_sources) metadata = property(fget=get_metadata) @@ -212,7 +213,7 @@ All objects dependent on Album """ -class Album(DatabaseObject, ID3Metadata): +class Album(DatabaseObject, ID3Metadata, SourceAttribute): """ -------DB-FIELDS------- title TEXT, @@ -240,7 +241,7 @@ class Album(DatabaseObject, ID3Metadata): is_split: bool = False, albumsort: int = None, dynamic: bool = False, - sources: List[Source] = None, + source_list: List[Source] = None, artists: list = None ) -> None: DatabaseObject.__init__(self, id_=id_, dynamic=dynamic) @@ -261,8 +262,8 @@ class Album(DatabaseObject, ID3Metadata): self.tracklist: List[Song] = [] - self._sources = [] - self.sources = sources + if source_list is not None: + self.source_list = source_list self.artists = [] if artists is not None: @@ -312,16 +313,6 @@ class Album(DatabaseObject, ID3Metadata): return self.language.alpha_3 - def set_sources(self, source_list: List[Source]): - if source_list is None: - return - - self._sources = source_list - for source in self._sources: - source.add_song(self) - source.type_enum = SourceTypes.ALBUM - - sources: List[Source] = property(fget=lambda self: self._sources, fset=set_sources) copyright = property(fget=get_copyright) iso_639_2_language = property(fget=get_iso_639_2_lang) @@ -332,7 +323,7 @@ All objects dependent on Artist """ -class Artist(DatabaseObject, ID3Metadata): +class Artist(DatabaseObject, ID3Metadata, SourceAttribute): """ main_songs feature_song @@ -344,7 +335,7 @@ class Artist(DatabaseObject, ID3Metadata): self, id_: str = None, name: str = None, - sources: List[Source] = None, + source_list: List[Source] = None, main_songs: List[Song] = None, feature_songs: List[Song] = None, main_albums: List[Album] = None, @@ -368,9 +359,8 @@ class Artist(DatabaseObject, ID3Metadata): self.main_albums = main_albums - self._sources = [] - self.sources = sources - + if source_list is not None: + self.source_list = source_list def __str__(self): return self.name or "" @@ -427,16 +417,7 @@ class Artist(DatabaseObject, ID3Metadata): return id3_dict - def set_sources(self, source_list: List[Source]): - if source_list is None: - return - self._sources = source_list - for source in self._sources: - source.add_song(self) - source.type_enum = SourceTypes.ARTIST - - sources: List[Source] = property(fget=lambda self: self._sources, fset=set_sources) discography: List[Album] = property(fget=get_discography) features: Album = property(fget=get_features) songs: Album = property(fget=get_songs) diff --git a/src/music_kraken/database/objects/source.py b/src/music_kraken/database/objects/source.py index d1432b4..b0a3309 100644 --- a/src/music_kraken/database/objects/source.py +++ b/src/music_kraken/database/objects/source.py @@ -86,7 +86,7 @@ class SourceAttribute: """ adds a new Source to the sources """ - pass + self._source_dict[source.page_enum].append(source) def get_sources_from_page(self, page_enum) -> List[Source]: """ @@ -99,7 +99,13 @@ class SourceAttribute: """ gets all sources """ - return [] + return [item for _, item in self._source_dict.items()] + + def set_source_list(self, source_list: List[Source]): + self._source_dict = {page_enum: list() for page_enum in SourcePages} + + for source in source_list: + self.add_source(source) def get_source_dict(self) -> Dict[any: List[Source]]: """ @@ -109,5 +115,5 @@ class SourceAttribute: """ return self._source_dict - source_list: List[Source] = property(fget=get_source_list) + source_list: List[Source] = property(fget=get_source_list, fset=set_source_list) source_dict: Dict[any: List[Source]] = property(fget=get_source_dict)