STARTED IMPLEMENTING DB

STARTED IMPLEMENTING DB
This commit is contained in:
Hellow
2023-02-14 23:07:16 +01:00
parent 021f8a6905
commit 5a699c3937
27 changed files with 990 additions and 755 deletions

View 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

View 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

View 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()

View 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())

View 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 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
"""
return MetadataAttribute.Metadata()
metadata = property(fget=lambda self: self.get_metadata())

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

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

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