STARTED IMPLEMENTING DB
STARTED IMPLEMENTING DB
This commit is contained in:
26
src/music_kraken/objects/__init__.py
Normal file
26
src/music_kraken/objects/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from . import (
|
||||
song,
|
||||
metadata,
|
||||
source,
|
||||
parents,
|
||||
formatted_text
|
||||
)
|
||||
|
||||
MusicObject = parents.DatabaseObject
|
||||
|
||||
ID3Mapping = metadata.Mapping
|
||||
ID3Timestamp = metadata.ID3Timestamp
|
||||
|
||||
SourceTypes = source.SourceTypes
|
||||
SourcePages = source.SourcePages
|
||||
SourceAttribute = source.SourceAttribute
|
||||
|
||||
Song = song.Song
|
||||
Artist = song.Artist
|
||||
Source = source.Source
|
||||
Target = song.Target
|
||||
Lyrics = song.Lyrics
|
||||
|
||||
Album = song.Album
|
||||
|
||||
FormattedText = formatted_text.FormattedText
|
||||
22
src/music_kraken/objects/artist.py
Normal file
22
src/music_kraken/objects/artist.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from src.music_kraken.utils.shared import (
|
||||
DATABASE_LOGGER as logger
|
||||
)
|
||||
from .parents import (
|
||||
DatabaseObject,
|
||||
Reference
|
||||
)
|
||||
|
||||
|
||||
class Artist(DatabaseObject):
|
||||
def __init__(self, id_: str = None, mb_id: str = None, name: str = None) -> None:
|
||||
super().__init__(id_=id_)
|
||||
self.mb_id = mb_id
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
if type(__o) != type(self):
|
||||
return False
|
||||
return self.id == __o.id
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
84
src/music_kraken/objects/collection.py
Normal file
84
src/music_kraken/objects/collection.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from typing import List
|
||||
|
||||
from .source import SourceAttribute
|
||||
from src.music_kraken.utils import string_processing
|
||||
|
||||
class Collection:
|
||||
"""
|
||||
This an class for the iterables
|
||||
like tracklist or discography
|
||||
"""
|
||||
_data: List[SourceAttribute]
|
||||
|
||||
_by_url: dict
|
||||
_by_attribute: dict
|
||||
|
||||
|
||||
def __init__(self, data: list = None, map_attributes: list = None, element_type=None) -> None:
|
||||
"""
|
||||
Attribute needs to point to
|
||||
"""
|
||||
self._by_url = dict()
|
||||
|
||||
|
||||
self.map_attributes = map_attributes or []
|
||||
self.element_type = element_type
|
||||
self._by_attribute = {attr: dict() for attr in map_attributes}
|
||||
|
||||
self._data = data or []
|
||||
|
||||
for element in self._data:
|
||||
self.map_element(element=element)
|
||||
|
||||
def map_element(self, element: SourceAttribute):
|
||||
for source_url in element.source_url_map:
|
||||
self._by_url[source_url] = element
|
||||
|
||||
for attr in self.map_attributes:
|
||||
value = element.__getattribute__(attr)
|
||||
if type(value) != str:
|
||||
# this also throws out all none values
|
||||
continue
|
||||
|
||||
self._by_attribute[attr][string_processing.unify(value)] = element
|
||||
|
||||
|
||||
def get_object_with_source(self, url: str) -> any:
|
||||
"""
|
||||
Returns either None, or the object, that has a source
|
||||
matching the url.
|
||||
"""
|
||||
if url in self._by_url:
|
||||
return self._by_url[url]
|
||||
|
||||
def get_object_with_attribute(self, name: str, value: str):
|
||||
if name not in self.map_attributes:
|
||||
raise ValueError(f"didn't map the attribute {name}")
|
||||
|
||||
unified = string_processing.unify(value)
|
||||
if unified in self._by_attribute[name]:
|
||||
return self._by_attribute[name][unified]
|
||||
|
||||
def append(self, element: SourceAttribute):
|
||||
if type(element) is not self.element_type and self.element_type is not None:
|
||||
|
||||
raise TypeError(f"{type(element)} is not the set type {self.element_type}")
|
||||
|
||||
self._data.append(element)
|
||||
self.map_element(element)
|
||||
|
||||
def __iter__(self):
|
||||
for element in self._data:
|
||||
yield element
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join([f"{str(j).zfill(2)}: {i}" for j, i in enumerate(self._data)])
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def copy(self) -> List:
|
||||
"""
|
||||
returns a shallow copy of the data list
|
||||
"""
|
||||
return self._data.copy()
|
||||
123
src/music_kraken/objects/formatted_text.py
Normal file
123
src/music_kraken/objects/formatted_text.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import pandoc
|
||||
|
||||
"""
|
||||
TODO
|
||||
implement in setup.py a skript to install pandocs
|
||||
https://pandoc.org/installing.html
|
||||
|
||||
!!!!!!!!!!!!!!!!!!IMPORTANT!!!!!!!!!!!!!!!!!!
|
||||
"""
|
||||
|
||||
|
||||
class FormattedText:
|
||||
doc = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plaintext: str = None,
|
||||
markdown: str = None,
|
||||
html: str = None
|
||||
) -> None:
|
||||
self.set_plaintext(plaintext)
|
||||
self.set_markdown(markdown)
|
||||
self.set_html(html)
|
||||
|
||||
def set_plaintext(self, plaintext: str):
|
||||
if plaintext is None:
|
||||
return
|
||||
self.doc = pandoc.read(plaintext)
|
||||
|
||||
def set_markdown(self, markdown: str):
|
||||
if markdown is None:
|
||||
return
|
||||
self.doc = pandoc.read(markdown, format="markdown")
|
||||
|
||||
def set_html(self, html: str):
|
||||
if html is None:
|
||||
return
|
||||
self.doc = pandoc.read(html, format="html")
|
||||
|
||||
def get_markdown(self) -> str:
|
||||
if self.doc is None:
|
||||
return None
|
||||
return pandoc.write(self.doc, format="markdown").strip()
|
||||
|
||||
def get_html(self) -> str:
|
||||
if self.doc is None:
|
||||
return None
|
||||
return pandoc.write(self.doc, format="html").strip()
|
||||
|
||||
def get_plaintext(self) -> str:
|
||||
if self.doc is None:
|
||||
return None
|
||||
return pandoc.write(self.doc, format="plain").strip()
|
||||
|
||||
plaintext = property(fget=get_plaintext, fset=set_plaintext)
|
||||
markdown = property(fget=get_markdown, fset=set_markdown)
|
||||
html = property(fget=get_html, fset=set_html)
|
||||
|
||||
|
||||
class NotesAttributes:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_plaintext = """
|
||||
World of Work
|
||||
|
||||
1. The right to help out society, and being paied for it
|
||||
2. The right to get paied, so you can get along well.
|
||||
3. The right for every individual to sell their products to provide for
|
||||
themselfes or for others
|
||||
4. The right of fair competitions, meaning eg. no monopoles.
|
||||
5. The right for a home.
|
||||
6. The right to good healthcare
|
||||
7. The right of protections against tragedies, be it personal ones, or
|
||||
global ones.
|
||||
8. The right to be educated in a way that enables you to work.
|
||||
|
||||
3 most important ones
|
||||
|
||||
1. The right to get paied, so you can get along well.
|
||||
2. The right for a home.
|
||||
3. The right for a good healthcare.
|
||||
"""
|
||||
_markdown = """
|
||||
# World of Work
|
||||
|
||||
1. The right to help out society, and being paied for it
|
||||
2. **The right to get paied, so you can get along well.**
|
||||
3. The right for every individual to sell their products to provide for themselfes or for others
|
||||
4. The right of fair competitions, meaning eg. no monopoles.
|
||||
5. **The right for a home.**
|
||||
6. **The right to good healthcare**
|
||||
7. The right of protections against tragedies, be it personal ones, or global ones.
|
||||
8. The right to be educated in a way that enables you to work.
|
||||
|
||||
## 3 most important ones
|
||||
|
||||
1. The right to get paied, so you can get along well.
|
||||
2. The right for a home.
|
||||
3. The right for a good healthcare.
|
||||
"""
|
||||
_html = """
|
||||
<b>Contact:</b> <a href="mailto:ghostbath@live.com">ghostbath@live.com</a><br />
|
||||
<br />
|
||||
Although the band originally claimed that they were from Chongqing, China, it has been revealed in a 2015 interview with <b>Noisey</b> that they're an American band based in Minot, North Dakota.<br />
|
||||
<br />
|
||||
According to the band, "Ghost Bath" refers to "the act of committing suicide by submerging in a body of water."<br />
|
||||
<br />
|
||||
<b>Compilation appearance(s):</b><br />
|
||||
- "Luminescence" on <i>Jericho Vol.36 - Nyctophobia</i> (2018) []
|
||||
"""
|
||||
|
||||
# notes = FormattedText(html=html)
|
||||
# notes = FormattedText(markdown=_markdown)
|
||||
notes = FormattedText(plaintext=_plaintext)
|
||||
|
||||
# print(notes.get_html())
|
||||
# print("-"*30)
|
||||
# print(notes.get_markdown())
|
||||
|
||||
print(notes.get_markdown())
|
||||
379
src/music_kraken/objects/metadata.py
Normal file
379
src/music_kraken/objects/metadata.py
Normal file
@@ -0,0 +1,379 @@
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
import dateutil.tz
|
||||
from mutagen import id3
|
||||
import datetime
|
||||
|
||||
|
||||
class Mapping(Enum):
|
||||
"""
|
||||
These frames belong to the id3 standart
|
||||
https://web.archive.org/web/20220830091059/https://id3.org/id3v2.4.0-frames
|
||||
https://id3lib.sourceforge.net/id3/id3v2com-00.html
|
||||
https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-frames.html
|
||||
"""
|
||||
# Textframes
|
||||
TITLE = "TIT2"
|
||||
ISRC = "TSRC"
|
||||
LENGTH = "TLEN" # in milliseconds
|
||||
DATE = "TYER"
|
||||
TRACKNUMBER = "TRCK"
|
||||
TOTALTRACKS = "TRCK" # Stored in the same frame with TRACKNUMBER, separated by '/': e.g. '4/9'.
|
||||
TITLESORTORDER = "TSOT"
|
||||
ENCODING_SETTINGS = "TSSE"
|
||||
SUBTITLE = "TIT3"
|
||||
SET_SUBTITLE = "TSST"
|
||||
RELEASE_DATE = "TDRL"
|
||||
RECORDING_DATES = "TXXX"
|
||||
PUBLISHER_URL = "WPUB"
|
||||
PUBLISHER = "TPUB"
|
||||
RATING = "POPM"
|
||||
DISCNUMBER = "TPOS"
|
||||
MOVEMENT_COUNT = "MVIN"
|
||||
TOTALDISCS = "TPOS"
|
||||
ORIGINAL_RELEASE_DATE = "TDOR"
|
||||
ORIGINAL_ARTIST = "TOPE"
|
||||
ORIGINAL_ALBUM = "TOAL"
|
||||
MEDIA_TYPE = "TMED"
|
||||
LYRICIST = "TEXT"
|
||||
WRITER = "TEXT"
|
||||
ARTIST = "TPE1"
|
||||
LANGUAGE = "TLAN" # https://en.wikipedia.org/wiki/ISO_639-2
|
||||
ITUNESCOMPILATION = "TCMP"
|
||||
REMIXED_BY = "TPE4"
|
||||
RADIO_STATION_OWNER = "TRSO"
|
||||
RADIO_STATION = "TRSN"
|
||||
INITIAL_KEY = "TKEY"
|
||||
OWNER = "TOWN"
|
||||
ENCODED_BY = "TENC"
|
||||
COPYRIGHT = "TCOP"
|
||||
GENRE = "TCON"
|
||||
GROUPING = "TIT1"
|
||||
CONDUCTOR = "TPE3"
|
||||
COMPOSERSORTORDER = "TSOC"
|
||||
COMPOSER = "TCOM"
|
||||
BPM = "TBPM"
|
||||
ALBUM_ARTIST = "TPE2"
|
||||
BAND = "TPE2"
|
||||
ARTISTSORTORDER = "TSOP"
|
||||
ALBUM = "TALB"
|
||||
ALBUMSORTORDER = "TSOA"
|
||||
ALBUMARTISTSORTORDER = "TSO2"
|
||||
TAGGING_TIME = "TDTG"
|
||||
|
||||
SOURCE_WEBPAGE_URL = "WOAS"
|
||||
FILE_WEBPAGE_URL = "WOAF"
|
||||
INTERNET_RADIO_WEBPAGE_URL = "WORS"
|
||||
ARTIST_WEBPAGE_URL = "WOAR"
|
||||
COPYRIGHT_URL = "WCOP"
|
||||
COMMERCIAL_INFORMATION_URL = "WCOM"
|
||||
PAYMEMT_URL = "WPAY"
|
||||
|
||||
MOVEMENT_INDEX = "MVIN"
|
||||
MOVEMENT_NAME = "MVNM"
|
||||
|
||||
UNSYNCED_LYRICS = "USLT"
|
||||
COMMENT = "COMM"
|
||||
|
||||
@classmethod
|
||||
def get_text_instance(cls, key: str, value: str):
|
||||
return id3.Frames[key](encoding=3, text=value)
|
||||
|
||||
@classmethod
|
||||
def get_url_instance(cls, key: str, url: str):
|
||||
return id3.Frames[key](encoding=3, url=url)
|
||||
|
||||
@classmethod
|
||||
def get_mutagen_instance(cls, attribute, value):
|
||||
key = attribute.value
|
||||
|
||||
if key[0] == 'T':
|
||||
# a text fiel
|
||||
return cls.get_text_instance(key, value)
|
||||
if key[0] == "W":
|
||||
# an url field
|
||||
return cls.get_url_instance(key, value)
|
||||
|
||||
|
||||
class ID3Timestamp:
|
||||
def __init__(
|
||||
self,
|
||||
year: int = None,
|
||||
month: int = None,
|
||||
day: int = None,
|
||||
hour: int = None,
|
||||
minute: int = None,
|
||||
second: int = None
|
||||
):
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
|
||||
self.has_year = year is not None
|
||||
self.has_month = month is not None
|
||||
self.has_day = day is not None
|
||||
self.has_hour = hour is not None
|
||||
self.has_minute = minute is not None
|
||||
self.has_second = second is not None
|
||||
|
||||
if not self.has_year:
|
||||
year = 1
|
||||
if not self.has_month:
|
||||
month = 1
|
||||
if not self.has_day:
|
||||
day = 1
|
||||
if not self.has_hour:
|
||||
hour = 1
|
||||
if not self.has_minute:
|
||||
minute = 1
|
||||
if not self.has_second:
|
||||
second = 1
|
||||
|
||||
self.date_obj = datetime.datetime(
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
second=second
|
||||
)
|
||||
|
||||
def get_time_format(self) -> str:
|
||||
"""
|
||||
https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-structure.html
|
||||
|
||||
The timestamp fields are based on a subset of ISO 8601. When being as precise as possible the format of a
|
||||
time string is
|
||||
- yyyy-MM-ddTHH:mm:ss
|
||||
- (year[%Y], “-”, month[%m], “-”, day[%d], “T”, hour (out of 24)[%H], ”:”, minutes[%M], ”:”, seconds[%S])
|
||||
- %Y-%m-%dT%H:%M:%S
|
||||
but the precision may be reduced by removing as many time indicators as wanted. Hence valid timestamps are
|
||||
- yyyy
|
||||
- yyyy-MM
|
||||
- yyyy-MM-dd
|
||||
- yyyy-MM-ddTHH
|
||||
- yyyy-MM-ddTHH:mm
|
||||
- yyyy-MM-ddTHH:mm:ss
|
||||
All time stamps are UTC. For durations, use the slash character as described in 8601,
|
||||
and for multiple non-contiguous dates, use multiple strings, if allowed by the frame definition.
|
||||
|
||||
:return timestamp: as timestamp in the format of the id3 time as above described
|
||||
"""
|
||||
|
||||
if self.has_year and self.has_month and self.has_day and self.has_hour and self.has_minute and self.has_second:
|
||||
return "%Y-%m-%dT%H:%M:%S"
|
||||
if self.has_year and self.has_month and self.has_day and self.has_hour and self.has_minute:
|
||||
return "%Y-%m-%dT%H:%M"
|
||||
if self.has_year and self.has_month and self.has_day and self.has_hour:
|
||||
return "%Y-%m-%dT%H"
|
||||
if self.has_year and self.has_month and self.has_day:
|
||||
return "%Y-%m-%d"
|
||||
if self.has_year and self.has_month:
|
||||
return "%Y-%m"
|
||||
if self.has_year:
|
||||
return "%Y"
|
||||
return ""
|
||||
|
||||
|
||||
def get_timestamp(self) -> str:
|
||||
time_format = self.get_time_format()
|
||||
return self.date_obj.strftime(time_format)
|
||||
|
||||
|
||||
def get_timestamp_w_format(self) -> Tuple[str, str]:
|
||||
time_format = self.get_time_format()
|
||||
return time_format, self.date_obj.strftime(time_format)
|
||||
|
||||
|
||||
@classmethod
|
||||
def strptime(cls, time_stamp: str, format: str):
|
||||
"""
|
||||
day: "%d"
|
||||
month: "%b", "%B", "%m"
|
||||
year: "%y", "%Y"
|
||||
hour: "%H", "%I"
|
||||
minute: "%M"
|
||||
second: "%S"
|
||||
"""
|
||||
date_obj = datetime.datetime.strptime(time_stamp, format)
|
||||
|
||||
day = None
|
||||
if "%d" in format:
|
||||
day = date_obj.day
|
||||
month = None
|
||||
if any([i in format for i in ("%b", "%B", "%m")]):
|
||||
month = date_obj.month
|
||||
year = None
|
||||
if any([i in format for i in ("%y", "%Y")]):
|
||||
year = date_obj.year
|
||||
hour = None
|
||||
if any([i in format for i in ("%H", "%I")]):
|
||||
hour = date_obj.hour
|
||||
minute = None
|
||||
if "%M" in format:
|
||||
minute = date_obj.minute
|
||||
second = None
|
||||
if "%S" in format:
|
||||
second = date_obj.second
|
||||
|
||||
return cls(
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
second=second
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def now(cls):
|
||||
date_obj = datetime.datetime.now()
|
||||
|
||||
return cls(
|
||||
year=date_obj.year,
|
||||
month=date_obj.month,
|
||||
day=date_obj.day,
|
||||
hour=date_obj.hour,
|
||||
minute=date_obj.minute,
|
||||
second=date_obj.second
|
||||
)
|
||||
|
||||
def strftime(self, format: str) -> str:
|
||||
return self.date_obj.strftime(format)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.timestamp
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.timestamp
|
||||
|
||||
timestamp: str = property(fget=get_timestamp)
|
||||
|
||||
|
||||
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]
|
||||
|
||||
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)}")
|
||||
|
||||
new_val = [i for i in value_list if i not in {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
|
||||
|
||||
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 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 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:
|
||||
"""
|
||||
this is intendet to be overwritten by the child class
|
||||
"""
|
||||
return MetadataAttribute.Metadata()
|
||||
|
||||
metadata = property(fget=lambda self: self.get_metadata())
|
||||
96
src/music_kraken/objects/parents.py
Normal file
96
src/music_kraken/objects/parents.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import uuid
|
||||
|
||||
from src.music_kraken.utils.shared import (
|
||||
SONG_LOGGER as logger
|
||||
)
|
||||
|
||||
|
||||
class Reference:
|
||||
def __init__(self, id_: str) -> None:
|
||||
self.id = id_
|
||||
|
||||
def __str__(self):
|
||||
return f"references to an object with the id: {self.id}"
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
if type(__o) != type(self):
|
||||
return False
|
||||
return self.id == __o.id
|
||||
|
||||
|
||||
class DatabaseObject:
|
||||
empty: bool
|
||||
|
||||
def __init__(self, id_: str = None, dynamic: bool = False, empty: bool = False, **kwargs) -> None:
|
||||
"""
|
||||
empty means it is an placeholder.
|
||||
it makes the object perform the same, it is just the same
|
||||
"""
|
||||
self.id_: str | None = id_
|
||||
self.dynamic = dynamic
|
||||
self.empty = empty
|
||||
|
||||
def get_id(self) -> str:
|
||||
"""
|
||||
returns the id if it is set, else
|
||||
it returns a randomly generated UUID
|
||||
https://docs.python.org/3/library/uuid.html
|
||||
|
||||
if the object is empty, it returns None
|
||||
if the object is dynamic, it raises an error
|
||||
"""
|
||||
if self.empty:
|
||||
return None
|
||||
if self.dynamic:
|
||||
raise ValueError("Dynamic objects have no idea, because they are not in the database")
|
||||
|
||||
if self.id_ is None:
|
||||
self.id_ = str(uuid.uuid4())
|
||||
logger.info(f"id for {self.__str__()} isn't set. Setting to {self.id_}")
|
||||
|
||||
return self.id_
|
||||
|
||||
def get_reference(self) -> Reference:
|
||||
return Reference(self.id)
|
||||
|
||||
def get_options(self) -> list:
|
||||
"""
|
||||
makes only sense in
|
||||
- artist
|
||||
- song
|
||||
- album
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_option_string(self) -> str:
|
||||
"""
|
||||
makes only sense in
|
||||
- artist
|
||||
- song
|
||||
- album
|
||||
"""
|
||||
return ""
|
||||
|
||||
id = property(fget=get_id)
|
||||
reference = property(fget=get_reference)
|
||||
options = property(fget=get_options)
|
||||
options_str = property(fget=get_option_string)
|
||||
|
||||
|
||||
class SongAttribute:
|
||||
def __init__(self, song=None):
|
||||
# the reference to the song the lyrics belong to
|
||||
self.song = song
|
||||
|
||||
def add_song(self, song):
|
||||
self.song = song
|
||||
|
||||
def get_ref_song_id(self):
|
||||
if self.song is None:
|
||||
return None
|
||||
return self.song.reference.id
|
||||
|
||||
def set_ref_song_id(self, song_id):
|
||||
self.song_ref = Reference(song_id)
|
||||
|
||||
song_ref_id = property(fget=get_ref_song_id, fset=set_ref_song_id)
|
||||
484
src/music_kraken/objects/song.py
Normal file
484
src/music_kraken/objects/song.py
Normal file
@@ -0,0 +1,484 @@
|
||||
import os
|
||||
from typing import List
|
||||
import pycountry
|
||||
import copy
|
||||
|
||||
from .metadata import (
|
||||
Mapping as id3Mapping,
|
||||
ID3Timestamp,
|
||||
MetadataAttribute
|
||||
)
|
||||
from src.music_kraken.utils.shared import (
|
||||
MUSIC_DIR,
|
||||
DATABASE_LOGGER as logger
|
||||
)
|
||||
from .parents import (
|
||||
DatabaseObject,
|
||||
Reference,
|
||||
SongAttribute
|
||||
)
|
||||
from .source import (
|
||||
Source,
|
||||
SourceTypes,
|
||||
SourcePages,
|
||||
SourceAttribute
|
||||
)
|
||||
from .formatted_text import FormattedText
|
||||
from .collection import Collection
|
||||
|
||||
"""
|
||||
All Objects dependent
|
||||
"""
|
||||
|
||||
|
||||
class Target(DatabaseObject, SongAttribute):
|
||||
"""
|
||||
create somehow like that
|
||||
```python
|
||||
# I know path is pointless, and I will change that (don't worry about backwards compatibility there)
|
||||
Target(file="~/Music/genre/artist/album/song.mp3", path="~/Music/genre/artist/album")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, id_: str = None, file: str = None, path: str = None) -> None:
|
||||
DatabaseObject.__init__(self, id_=id_)
|
||||
SongAttribute.__init__(self)
|
||||
self._file = file
|
||||
self._path = path
|
||||
|
||||
def set_file(self, _file: str):
|
||||
self._file = _file
|
||||
|
||||
def get_file(self) -> str | None:
|
||||
if self._file is None:
|
||||
return None
|
||||
return os.path.join(MUSIC_DIR, self._file)
|
||||
|
||||
def set_path(self, _path: str):
|
||||
self._path = _path
|
||||
|
||||
def get_path(self) -> str | None:
|
||||
if self._path is None:
|
||||
return None
|
||||
return os.path.join(MUSIC_DIR, self._path)
|
||||
|
||||
def get_exists_on_disc(self) -> bool:
|
||||
"""
|
||||
returns True when file can be found on disc
|
||||
returns False when file can't be found on disc or no filepath is set
|
||||
"""
|
||||
if not self.is_set():
|
||||
return False
|
||||
|
||||
return os.path.exists(self.file)
|
||||
|
||||
def is_set(self) -> bool:
|
||||
return not (self._file is None or self._path is None)
|
||||
|
||||
file = property(fget=get_file, fset=set_file)
|
||||
path = property(fget=get_path, fset=set_path)
|
||||
|
||||
exists_on_disc = property(fget=get_exists_on_disc)
|
||||
|
||||
|
||||
class Lyrics(DatabaseObject, SongAttribute, SourceAttribute, MetadataAttribute):
|
||||
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
|
||||
|
||||
def get_metadata(self) -> MetadataAttribute.Metadata:
|
||||
return super().get_metadata()
|
||||
|
||||
|
||||
class Song(DatabaseObject, SourceAttribute, MetadataAttribute):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id_: str = None,
|
||||
mb_id: str = None,
|
||||
title: str = None,
|
||||
isrc: str = None,
|
||||
length: int = None,
|
||||
tracksort: int = None,
|
||||
genre: str = None,
|
||||
source_list: List[Source] = None,
|
||||
target: Target = None,
|
||||
lyrics_list: List[Lyrics] = None,
|
||||
album=None,
|
||||
main_artist_list: list = None,
|
||||
feature_artist_list: list = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the Song object with the following attributes:
|
||||
"""
|
||||
super().__init__(id_=id_, **kwargs)
|
||||
# attributes
|
||||
# *private* attributes
|
||||
self.title: str = title
|
||||
self.isrc: str = isrc
|
||||
self.length: int = length
|
||||
self.mb_id: str | None = mb_id
|
||||
self.tracksort: int = tracksort or 0
|
||||
self.genre: str = genre
|
||||
|
||||
self.source_list = source_list or []
|
||||
|
||||
self.target = target or Target()
|
||||
self.lyrics_list = lyrics_list or []
|
||||
|
||||
# initialize with either a passed in album, or an empty one,
|
||||
# so it can at least properly generate dynamic attributes
|
||||
self._album = album or Album(empty=True)
|
||||
self.album = album
|
||||
|
||||
self.main_artist_collection = Collection(
|
||||
data=main_artist_list or [],
|
||||
map_attributes=["title"],
|
||||
element_type=Artist
|
||||
)
|
||||
self.feature_artist_collection = Collection(
|
||||
data=feature_artist_list or [],
|
||||
map_attributes=["title"],
|
||||
element_type=Artist
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) != type(self):
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
def get_artist_credits(self) -> str:
|
||||
main_artists = ", ".join([artist.name for artist in self.main_artist_collection])
|
||||
feature_artists = ", ".join([artist.name for artist in self.feature_artist_collection])
|
||||
|
||||
if len(feature_artists) == 0:
|
||||
return main_artists
|
||||
return f"{main_artists} feat. {feature_artists}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
artist_credit_str = ""
|
||||
artist_credits = self.get_artist_credits()
|
||||
if artist_credits != "":
|
||||
artist_credit_str = f" by {artist_credits}"
|
||||
|
||||
return f"\"{self.title}\"{artist_credit_str}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Song(\"{self.title}\")"
|
||||
|
||||
def get_tracksort_str(self):
|
||||
"""
|
||||
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}"
|
||||
|
||||
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])
|
||||
if not self.album.empty:
|
||||
metadata.merge(self.album.metadata)
|
||||
metadata.merge_many([a.metadata for a in self.main_artist_list])
|
||||
metadata.merge_many([a.metadata for a in self.feature_artist_list])
|
||||
metadata.merge_many([l.metadata for l in self.lyrics])
|
||||
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.
|
||||
|
||||
:return: a list of objects that are related to the Song object
|
||||
"""
|
||||
options = self.main_artist_list.copy()
|
||||
options.extend(self.feature_artist_list.copy())
|
||||
if not self.album.empty:
|
||||
options.append(self.album)
|
||||
options.append(self)
|
||||
return options
|
||||
|
||||
def get_option_string(self) -> str:
|
||||
return f"Song({self.title}) of Album({self.album.title}) from Artists({self.get_artist_credits()})"
|
||||
|
||||
tracksort_str = property(fget=get_tracksort_str)
|
||||
main_artist_list: list = property(fget=lambda self: self.main_artist_collection.copy())
|
||||
feature_artist_list: list = property(fget=lambda self: self.feature_artist_collection.copy())
|
||||
|
||||
|
||||
"""
|
||||
All objects dependent on Album
|
||||
"""
|
||||
|
||||
|
||||
class Album(DatabaseObject, SourceAttribute, MetadataAttribute):
|
||||
def __init__(
|
||||
self,
|
||||
id_: str = None,
|
||||
title: str = None,
|
||||
label: str = None,
|
||||
album_status: str = None,
|
||||
language: pycountry.Languages = None,
|
||||
date: ID3Timestamp = None,
|
||||
country: str = None,
|
||||
barcode: str = None,
|
||||
is_split: bool = False,
|
||||
albumsort: int = None,
|
||||
dynamic: bool = False,
|
||||
source_list: List[Source] = None,
|
||||
artist_list: list = None,
|
||||
tracklist: List[Song] = None,
|
||||
album_type: str = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
DatabaseObject.__init__(self, id_=id_, dynamic=dynamic, **kwargs)
|
||||
|
||||
"""
|
||||
TODO
|
||||
add to db
|
||||
"""
|
||||
self.album_type = album_type
|
||||
|
||||
self.title: str = title
|
||||
self.album_status: str = album_status
|
||||
self.label = label
|
||||
self.language: pycountry.Languages = language
|
||||
self.date: ID3Timestamp = date or ID3Timestamp()
|
||||
self.country: str = country
|
||||
"""
|
||||
TODO
|
||||
find out the id3 tag for barcode and implement it
|
||||
maybee look at how mutagen does it with easy_id3
|
||||
"""
|
||||
self.barcode: str = barcode
|
||||
self.is_split: bool = is_split
|
||||
"""
|
||||
TODO
|
||||
implement a function in the Artist class,
|
||||
to set albumsort with help of the release year
|
||||
"""
|
||||
self.albumsort: int | None = albumsort
|
||||
|
||||
|
||||
self._tracklist = Collection(
|
||||
data=tracklist or [],
|
||||
map_attributes=["title"],
|
||||
element_type=Song
|
||||
)
|
||||
self.source_list = source_list or []
|
||||
self.artists = Collection(
|
||||
data=artist_list or [],
|
||||
map_attributes=["name"],
|
||||
element_type=Artist
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"-----{self.title}-----\n{self.tracklist}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"Album(\"{self.title}\")"
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.tracklist)
|
||||
|
||||
def set_tracklist(self, tracklist):
|
||||
tracklist_list = []
|
||||
if type(tracklist) == Collection:
|
||||
tracklist_list = tracklist_list
|
||||
elif type(tracklist) == list:
|
||||
tracklist_list = tracklist
|
||||
|
||||
self._tracklist = Collection(
|
||||
data=tracklist_list,
|
||||
map_attributes=["title"],
|
||||
element_type=Song
|
||||
)
|
||||
|
||||
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.artists],
|
||||
id3Mapping.DATE: [self.date.timestamp]
|
||||
})
|
||||
|
||||
def get_copyright(self) -> str:
|
||||
if self.date is None:
|
||||
return None
|
||||
if self.date.year == 1 or self.label is None:
|
||||
return None
|
||||
|
||||
return f"{self.date.year} {self.label}"
|
||||
|
||||
def get_iso_639_2_lang(self) -> str:
|
||||
if self.language is None:
|
||||
return None
|
||||
|
||||
return self.language.alpha_3
|
||||
|
||||
def get_options(self) -> list:
|
||||
options = self.artists.copy()
|
||||
options.append(self)
|
||||
for track in self.tracklist:
|
||||
new_track: Song = copy.copy(track)
|
||||
new_track.album = self
|
||||
options.append(new_track)
|
||||
|
||||
return options
|
||||
|
||||
def get_option_string(self) -> str:
|
||||
return f"Album: {self.title}; Artists {', '.join([i.name for i in self.artists])}"
|
||||
|
||||
|
||||
copyright = property(fget=get_copyright)
|
||||
iso_639_2_language = property(fget=get_iso_639_2_lang)
|
||||
tracklist: Collection = property(fget=lambda self: self._tracklist, fset=set_tracklist)
|
||||
|
||||
|
||||
"""
|
||||
All objects dependent on Artist
|
||||
"""
|
||||
|
||||
|
||||
class Artist(DatabaseObject, SourceAttribute, MetadataAttribute):
|
||||
"""
|
||||
main_songs
|
||||
feature_song
|
||||
|
||||
albums
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id_: str = None,
|
||||
name: str = None,
|
||||
source_list: List[Source] = None,
|
||||
feature_songs: List[Song] = None,
|
||||
main_albums: List[Album] = None,
|
||||
notes: FormattedText = None,
|
||||
lyrical_themes: List[str] = None,
|
||||
general_genre: str = "",
|
||||
country=None,
|
||||
formed_in: ID3Timestamp = None
|
||||
):
|
||||
DatabaseObject.__init__(self, id_=id_)
|
||||
|
||||
"""
|
||||
TODO implement album type and notes
|
||||
"""
|
||||
self.country: pycountry.Country = country
|
||||
self.formed_in: ID3Timestamp = formed_in
|
||||
"""
|
||||
notes, generall genre, lyrics themes are attributes
|
||||
which are meant to only use in outputs to describe the object
|
||||
i mean do as you want but there aint no strict rule about em so good luck
|
||||
"""
|
||||
self.notes: FormattedText = notes or FormattedText()
|
||||
self.lyrical_themes: List[str] = lyrical_themes or []
|
||||
self.general_genre = general_genre
|
||||
|
||||
self.name: str = name
|
||||
|
||||
self.feature_songs = Collection(
|
||||
data=feature_songs,
|
||||
map_attributes=["title"],
|
||||
element_type=Song
|
||||
)
|
||||
|
||||
self.main_albums = Collection(
|
||||
data=main_albums,
|
||||
map_attributes=["title"],
|
||||
element_type=Album
|
||||
)
|
||||
|
||||
if source_list is not None:
|
||||
self.source_list = source_list
|
||||
|
||||
def __str__(self):
|
||||
string = self.name or ""
|
||||
plaintext_notes = self.notes.get_plaintext()
|
||||
if plaintext_notes is not None:
|
||||
string += "\n" + plaintext_notes
|
||||
return string
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
return self.id_ == __o.id_
|
||||
|
||||
def get_features(self) -> Album:
|
||||
feature_release = Album(
|
||||
title="features",
|
||||
album_status="dynamic",
|
||||
is_split=True,
|
||||
albumsort=666,
|
||||
dynamic=True
|
||||
)
|
||||
for feature in self.feature_songs:
|
||||
feature_release.add_song(feature)
|
||||
|
||||
return feature_release
|
||||
|
||||
def get_all_songs(self) -> List[Song]:
|
||||
"""
|
||||
returns a list of all Songs.
|
||||
probaply not that usefull, because it is unsorted
|
||||
"""
|
||||
collection = []
|
||||
for album in self.discography:
|
||||
collection.extend(album)
|
||||
|
||||
return collection
|
||||
|
||||
def get_discography(self) -> List[Album]:
|
||||
flat_copy_discography = self.main_albums.copy()
|
||||
flat_copy_discography.append(self.get_features())
|
||||
|
||||
return flat_copy_discography
|
||||
|
||||
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_albums)
|
||||
options.extend(self.feature_songs)
|
||||
return options
|
||||
|
||||
def get_option_string(self) -> str:
|
||||
return f"Artist: {self.name}"
|
||||
|
||||
discography: List[Album] = property(fget=get_discography)
|
||||
features: Album = property(fget=get_features)
|
||||
all_songs: Album = property(fget=get_all_songs)
|
||||
194
src/music_kraken/objects/source.py
Normal file
194
src/music_kraken/objects/source.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from enum import Enum
|
||||
from typing import List, Dict
|
||||
|
||||
from .metadata import Mapping, MetadataAttribute
|
||||
from .parents import (
|
||||
DatabaseObject,
|
||||
SongAttribute,
|
||||
)
|
||||
|
||||
|
||||
class SourceTypes(Enum):
|
||||
SONG = "song"
|
||||
ALBUM = "album"
|
||||
ARTIST = "artist"
|
||||
LYRICS = "lyrics"
|
||||
|
||||
|
||||
class SourcePages(Enum):
|
||||
YOUTUBE = "youtube"
|
||||
MUSIFY = "musify"
|
||||
GENIUS = "genius"
|
||||
MUSICBRAINZ = "musicbrainz"
|
||||
ENCYCLOPAEDIA_METALLUM = "encyclopaedia metallum"
|
||||
BANDCAMP = "bandcamp"
|
||||
DEEZER = "deezer"
|
||||
SPOTIFY = "spotify"
|
||||
|
||||
# This has nothing to do with audio, but bands can be here
|
||||
INSTAGRAM = "instagram"
|
||||
FACEBOOK = "facebook"
|
||||
TWITTER = "twitter" # I will use nitter though lol
|
||||
|
||||
@classmethod
|
||||
def get_homepage(cls, attribute) -> str:
|
||||
homepage_map = {
|
||||
cls.YOUTUBE: "https://www.youtube.com/",
|
||||
cls.MUSIFY: "https://musify.club/",
|
||||
cls.MUSICBRAINZ: "https://musicbrainz.org/",
|
||||
cls.ENCYCLOPAEDIA_METALLUM: "https://www.metal-archives.com/",
|
||||
cls.GENIUS: "https://genius.com/",
|
||||
cls.BANDCAMP: "https://bandcamp.com/",
|
||||
cls.DEEZER: "https://www.deezer.com/",
|
||||
cls.INSTAGRAM: "https://www.instagram.com/",
|
||||
cls.FACEBOOK: "https://www.facebook.com/",
|
||||
cls.SPOTIFY: "https://open.spotify.com/",
|
||||
cls.TWITTER: "https://twitter.com/"
|
||||
}
|
||||
return homepage_map[attribute]
|
||||
|
||||
|
||||
class Source(DatabaseObject, SongAttribute, MetadataAttribute):
|
||||
"""
|
||||
create somehow like that
|
||||
```python
|
||||
# url won't be a valid one due to it being just an example
|
||||
Source(src="youtube", url="https://youtu.be/dfnsdajlhkjhsd")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, page_enum, url: str, id_: str = None, type_enum=None) -> None:
|
||||
DatabaseObject.__init__(self, id_=id_)
|
||||
SongAttribute.__init__(self)
|
||||
|
||||
self.type_enum = type_enum
|
||||
self.page_enum = page_enum
|
||||
|
||||
self.url = url
|
||||
|
||||
@classmethod
|
||||
def match_url(cls, url: str):
|
||||
"""
|
||||
this shouldn't be used, unlesse you are not certain what the source is for
|
||||
the reason is that it is more inefficient
|
||||
"""
|
||||
if url.startswith("https://www.youtube"):
|
||||
return cls(SourcePages.YOUTUBE, url)
|
||||
|
||||
if url.startswith("https://www.deezer"):
|
||||
return cls(SourcePages.DEEZER, url)
|
||||
|
||||
if url.startswith("https://open.spotify.com"):
|
||||
return cls(SourcePages.SPOTIFY, url)
|
||||
|
||||
if "bandcamp" in url:
|
||||
return cls(SourcePages.BANDCAMP, url)
|
||||
|
||||
if url.startswith("https://www.metal-archives.com/"):
|
||||
return cls(SourcePages.ENCYCLOPAEDIA_METALLUM, url)
|
||||
|
||||
# the less important once
|
||||
if url.startswith("https://www.facebook"):
|
||||
return cls(SourcePages.FACEBOOK, url)
|
||||
|
||||
if url.startswith("https://www.instagram"):
|
||||
return cls(SourcePages.INSTAGRAM, url)
|
||||
|
||||
if url.startswith("https://twitter"):
|
||||
return cls(SourcePages.TWITTER, url)
|
||||
|
||||
def get_song_metadata(self) -> MetadataAttribute.Metadata:
|
||||
return MetadataAttribute.Metadata({
|
||||
Mapping.FILE_WEBPAGE_URL: [self.url],
|
||||
Mapping.SOURCE_WEBPAGE_URL: [self.homepage]
|
||||
})
|
||||
|
||||
def get_artist_metadata(self) -> MetadataAttribute.Metadata:
|
||||
return MetadataAttribute.Metadata({
|
||||
Mapping.ARTIST_WEBPAGE_URL: [self.url]
|
||||
})
|
||||
|
||||
def get_metadata(self) -> MetadataAttribute.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()
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Src({self.page_enum.value}: {self.url})"
|
||||
|
||||
page_str = property(fget=lambda self: self.page_enum.value)
|
||||
type_str = property(fget=lambda self: self.type_enum.value)
|
||||
homepage = property(fget=lambda self: SourcePages.get_homepage(self.page_enum))
|
||||
|
||||
|
||||
class SourceAttribute:
|
||||
"""
|
||||
This is a class that is meant to be inherited from.
|
||||
it adds the source_list attribute to a class
|
||||
"""
|
||||
_source_dict: Dict[object, List[Source]]
|
||||
source_url_map: Dict[str, Source]
|
||||
|
||||
def __new__(cls, **kwargs):
|
||||
new = object.__new__(cls)
|
||||
new._source_dict = {page_enum: list() for page_enum in SourcePages}
|
||||
new.source_url_map = dict()
|
||||
return new
|
||||
|
||||
def match_source_with_url(self, url: str) -> bool:
|
||||
"""
|
||||
this function returns true, if a source with this url exists,
|
||||
else it returns false
|
||||
:param url:
|
||||
:return source_with_url_exists:
|
||||
"""
|
||||
return url in self.source_url_map
|
||||
|
||||
def match_source(self, source: Source) -> bool:
|
||||
return self.match_source_with_url(source.url)
|
||||
|
||||
def add_source(self, source: Source):
|
||||
"""
|
||||
adds a new Source to the sources
|
||||
"""
|
||||
if self.match_source(source):
|
||||
return
|
||||
self.source_url_map[source.url] = source
|
||||
self._source_dict[source.page_enum].append(source)
|
||||
|
||||
def get_sources_from_page(self, page_enum) -> List[Source]:
|
||||
"""
|
||||
getting the sources for a specific page like
|
||||
youtube or musify
|
||||
"""
|
||||
return self._source_dict[page_enum]
|
||||
|
||||
def get_source_list(self) -> List[Source]:
|
||||
"""
|
||||
gets all sources
|
||||
"""
|
||||
return [item for _, page_list in self._source_dict.items() for item in page_list]
|
||||
|
||||
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[object, List[Source]]:
|
||||
"""
|
||||
gets a dictionary of all Sources,
|
||||
where the key is a page enum,
|
||||
and the value is a List with all sources of according page
|
||||
"""
|
||||
return self._source_dict
|
||||
|
||||
source_list: List[Source] = property(fget=get_source_list, fset=set_source_list)
|
||||
source_dict: Dict[object, List[Source]] = property(fget=get_source_dict)
|
||||
Reference in New Issue
Block a user