refactor
This commit is contained in:
parent
d6aed75fec
commit
0423cb3139
@ -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
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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")
|
||||
|
||||
@ -233,6 +228,16 @@ class Album(MainObject, MetadataAttribute):
|
||||
*[('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:
|
||||
@ -312,17 +311,13 @@ 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)
|
||||
|
||||
|
||||
"""
|
||||
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")
|
||||
|
||||
@ -382,6 +377,15 @@ class Artist(MainObject, MetadataAttribute):
|
||||
*[('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 ""
|
||||
plaintext_notes = self.notes.get_plaintext()
|
||||
@ -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",)
|
||||
|
||||
|
@ -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]]:
|
||||
|
Loading…
Reference in New Issue
Block a user