This commit is contained in:
Hellow2 2023-03-10 09:09:35 +01:00
parent d6aed75fec
commit 0423cb3139
6 changed files with 168 additions and 180 deletions

View File

@ -84,7 +84,8 @@ class Collection:
return self._data[item] return self._data[item]
def copy(self) -> List: @property
def shallow_list(self) -> List[DatabaseObject]:
""" """
returns a shallow copy of the data list returns a shallow copy of the data list
""" """

View File

@ -3,12 +3,12 @@ from typing import List
import pycountry import pycountry
from .parents import DatabaseObject from .parents import DatabaseObject
from .source import SourceAttribute, Source from .source import Source, SourceCollection
from .metadata import MetadataAttribute from .metadata import Metadata
from .formatted_text import FormattedText from .formatted_text import FormattedText
class Lyrics(DatabaseObject, SourceAttribute, MetadataAttribute): class Lyrics(DatabaseObject):
def __init__( def __init__(
self, self,
text: FormattedText, text: FormattedText,
@ -23,5 +23,4 @@ class Lyrics(DatabaseObject, SourceAttribute, MetadataAttribute):
self.text: FormattedText = text self.text: FormattedText = text
self.language: pycountry.Languages = language self.language: pycountry.Languages = language
if source_list is not None: self.source_collection: SourceCollection = SourceCollection(source_list)
self.source_list = source_list

View File

@ -1,7 +1,6 @@
from enum import Enum from enum import Enum
from typing import List, Dict, Tuple from typing import List, Dict, Tuple
import dateutil.tz
from mutagen import id3 from mutagen import id3
import datetime import datetime
@ -267,126 +266,113 @@ class ID3Timestamp:
timeformat: str = property(fget=get_time_format) timeformat: str = property(fget=get_time_format)
class MetadataAttribute: class Metadata:
""" # it's a null byte for the later concatenation of text frames
This class shall be added to any object, which can return data for tagging 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: def __init__(self, id3_dict: Dict[any, list] = None) -> None:
# it's a null byte for the later concatenation of text frames self.id3_dict = dict()
NULL_BYTE: str = "\x00" if id3_dict is not None:
# this is pretty self-explanatory self.add_metadata_dict(id3_dict)
# 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: def __setitem__(self, frame, value_list: list, override_existing: bool = True):
self.id3_dict = dict() if type(value_list) != list:
if id3_dict is not None: raise ValueError(f"can only set attribute to list, not {type(value_list)}")
self.add_metadata_dict(id3_dict)
def __setitem__(self, frame, value_list: list, override_existing: bool = True): new_val = [i for i in value_list if i not in {None, ''}]
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, ''}] 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 return
if override_existing: self.id3_dict[frame].extend(new_val)
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) def __getitem__(self, key):
if key not in self.id3_dict:
return None
return self.id3_dict[key]
def __getitem__(self, key): def delete_field(self, key: str):
if key not in self.id3_dict: if key in self.id3_dict:
return None return self.id3_dict.pop(key)
return self.id3_dict[key]
def delete_field(self, key: str): def add_metadata_dict(self, metadata_dict: dict, override_existing: bool = True):
if key in self.id3_dict: for field_enum, value in metadata_dict.items():
return self.id3_dict.pop(key) self.__setitem__(field_enum, value, override_existing=override_existing)
def add_metadata_dict(self, metadata_dict: dict, override_existing: bool = True): def merge(self, other, override_existing: bool = False):
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 performances 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:
""" """
this is intendet to be overwritten by the child class adds the values of another metadata obj to this one
"""
return MetadataAttribute.Metadata()
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 performances 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)

View File

@ -5,6 +5,7 @@ import uuid
from ..utils.shared import ( from ..utils.shared import (
SONG_LOGGER as LOGGER SONG_LOGGER as LOGGER
) )
from .metadata import Metadata
class DatabaseObject: class DatabaseObject:
@ -69,6 +70,10 @@ class DatabaseObject:
if override or getattr(self, simple_attribute) is None: if override or getattr(self, simple_attribute) is None:
setattr(self, simple_attribute, getattr(other, simple_attribute)) setattr(self, simple_attribute, getattr(other, simple_attribute))
@property
def metadata(self) -> Metadata:
return Metadata()
class MainObject(DatabaseObject): class MainObject(DatabaseObject):
""" """

View File

@ -5,7 +5,7 @@ import pycountry
from .metadata import ( from .metadata import (
Mapping as id3Mapping, Mapping as id3Mapping,
ID3Timestamp, ID3Timestamp,
MetadataAttribute Metadata
) )
from ..utils.shared import ( from ..utils.shared import (
MUSIC_DIR, MUSIC_DIR,
@ -36,12 +36,10 @@ All Objects dependent
CountryTyping = type(list(pycountry.countries)[0]) 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, 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. 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") 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, feature_artist_list: List[Type['Artist']] = None,
**kwargs **kwargs
) -> None: ) -> None:
"""
Initializes the Song object with the following attributes:
"""
MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs)
# attributes # attributes
self.title: str = title self.title: str = title
@ -90,6 +85,33 @@ class Song(MainObject, MetadataAttribute):
self.feature_artist_collection = Collection(data=feature_artist_list, element_type=Artist) 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): def __eq__(self, other):
if type(other) != type(self): if type(other) != type(self):
return False return False
@ -114,39 +136,14 @@ class Song(MainObject, MetadataAttribute):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Song(\"{self.title}\")" 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 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} :returns id3_tracksort: {song_position}/{album.length_of_tracklist}
""" """
return f"{self.tracksort}/{len(self.album.tracklist) or 1}" 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: def get_options(self) -> list:
""" """
Return a list of related objects including the song object, album object, main artist objects, and feature artist objects. 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: def get_option_string(self) -> str:
return f"Song({self.title}) of Album({self.album.title}) from Artists({self.get_artist_credits()})" 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 All objects dependent on Album
""" """
class Album(MainObject, MetadataAttribute): class Album(MainObject):
COLLECTION_ATTRIBUTES = ("label_collection", "artist_collection", "song_collection") COLLECTION_ATTRIBUTES = ("label_collection", "artist_collection", "song_collection")
SIMPLE_ATTRIBUTES = ("title", "album_status", "album_type", "language", "date", "barcode", "albumsort") SIMPLE_ATTRIBUTES = ("title", "album_status", "album_type", "language", "date", "barcode", "albumsort")
@ -233,6 +228,16 @@ class Album(MainObject, MetadataAttribute):
*[('url', source.url) for source in self.source_list] *[('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): def __repr__(self):
return f"Album(\"{self.title}\")" return f"Album(\"{self.title}\")"
@ -268,16 +273,10 @@ class Album(MainObject, MetadataAttribute):
continue continue
song.tracksort = i + 1 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: if self.date is None:
return "" return ""
if self.date.has_year or len(self.label_collection) == 0: if self.date.has_year or len(self.label_collection) == 0:
@ -312,17 +311,13 @@ class Album(MainObject, MetadataAttribute):
def get_option_string(self) -> str: def get_option_string(self) -> str:
return f"Album: {self.title}; Artists {', '.join([i.name for i in self.artist_collection])}" 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)
""" """
All objects dependent on Artist All objects dependent on Artist
""" """
class Artist(MainObject, MetadataAttribute): class Artist(MainObject):
COLLECTION_ATTRIBUTES = ("feature_song_collection", "main_album_collection", "label_collection") COLLECTION_ATTRIBUTES = ("feature_song_collection", "main_album_collection", "label_collection")
SIMPLE_ATTRIBUTES = ("name", "name", "country", "formed_in", "notes", "lyrical_themes", "general_genre") SIMPLE_ATTRIBUTES = ("name", "name", "country", "formed_in", "notes", "lyrical_themes", "general_genre")
@ -382,6 +377,15 @@ class Artist(MainObject, MetadataAttribute):
*[('url', source.url) for source in self.source_list] *[('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): def __str__(self):
string = self.name or "" string = self.name or ""
plaintext_notes = self.notes.get_plaintext() plaintext_notes = self.notes.get_plaintext()
@ -425,14 +429,6 @@ class Artist(MainObject, MetadataAttribute):
song_list=self.feature_song_collection.copy() 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: def get_options(self) -> list:
options = [self] options = [self]
options.extend(self.main_album_collection) 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") COLLECTION_ATTRIBUTES = ("album_collection", "current_artist_collection")
SIMPLE_ATTRIBUTES = ("name",) SIMPLE_ATTRIBUTES = ("name",)

View File

@ -2,7 +2,7 @@ from collections import defaultdict
from enum import Enum from enum import Enum
from typing import List, Dict, Tuple from typing import List, Dict, Tuple
from .metadata import Mapping, MetadataAttribute from .metadata import Mapping, Metadata
from .parents import DatabaseObject from .parents import DatabaseObject
from .collection import Collection from .collection import Collection
@ -106,14 +106,15 @@ class Source(DatabaseObject, MetadataAttribute):
Mapping.ARTIST_WEBPAGE_URL: [self.url] Mapping.ARTIST_WEBPAGE_URL: [self.url]
}) })
def get_metadata(self) -> MetadataAttribute.Metadata: @property
def metadata(self) -> Metadata:
if self.type_enum == SourceTypes.SONG: if self.type_enum == SourceTypes.SONG:
return self.get_song_metadata() return self.get_song_metadata()
if self.type_enum == SourceTypes.ARTIST: if self.type_enum == SourceTypes.ARTIST:
return self.get_artist_metadata() return self.get_artist_metadata()
return super().get_metadata() return super().metadata
@property @property
def indexing_values(self) -> List[Tuple[str, object]]: def indexing_values(self) -> List[Tuple[str, object]]: