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]
def copy(self) -> List:
@property
def shallow_list(self) -> List[DatabaseObject]:
"""
returns a shallow copy of the data list
"""

View File

@ -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)

View File

@ -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 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:
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 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 (
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):
"""

View File

@ -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",)

View File

@ -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]]: