diff --git a/src/music_kraken/database/data_models.py b/src/music_kraken/database/data_models.py index 6e3f88d..9621600 100644 --- a/src/music_kraken/database/data_models.py +++ b/src/music_kraken/database/data_models.py @@ -25,6 +25,8 @@ EVEN if that means to for example keep decimal values stored in strings. class BaseModel(Model): + notes: str = CharField(null=True) + class Meta: database = None @@ -38,33 +40,41 @@ class BaseModel(Model): return self -class Album(BaseModel): +class ObjectModel(BaseModel): + additional_arguments: str = CharField(null=True) + notes: str = CharField(null=True) + + +class Album(ObjectModel): """A class representing an album in the music database.""" title: str = CharField(null=True) - label: str = CharField(null=True) album_status: str = CharField(null=True) album_type: str = CharField(null=True) language: str = CharField(null=True) date: str = CharField(null=True) date_format: str = CharField(null=True) - country: str = CharField(null=True) + barcode: str = CharField(null=True) - albumsort: int = IntegerField(null=True) is_split: bool = BooleanField(default=False) + albumsort: int = IntegerField(null=True) + class Artist(BaseModel): """A class representing an artist in the music database.""" - name: str = CharField() - notes: str = CharField() + name: str = CharField(null=True) + country: str = CharField(null=True) + formed_in_date: str = CharField(null=True) + formed_in_date_format: str = CharField(null=True) + general_genre: str = CharField(null=True) class Song(BaseModel): """A class representing a song in the music database.""" - name: str = CharField(null=True) + title: str = CharField(null=True) isrc: str = CharField(null=True) length: int = IntegerField(null=True) tracksort: int = IntegerField(null=True) @@ -87,6 +97,12 @@ class Lyrics(BaseModel): song = ForeignKeyField(Song, backref='lyrics') +class SongTarget(BaseModel): + song: ForeignKeyField = ForeignKeyField(Song, backref='song_target') + artist: ForeignKeyField = ForeignKeyField(Target, backref='song_target') + is_feature: bool = BooleanField(default=False) + + class SongArtist(BaseModel): """A class representing the relationship between a song and an artist.""" diff --git a/src/music_kraken/objects/lyrics.py b/src/music_kraken/objects/lyrics.py new file mode 100644 index 0000000..afb9296 --- /dev/null +++ b/src/music_kraken/objects/lyrics.py @@ -0,0 +1,27 @@ +from typing import List + +import pycountry + +from .parents import DatabaseObject +from .source import SourceAttribute, Source +from .metadata import MetadataAttribute +from .formatted_text import FormattedText + + +class Lyrics(DatabaseObject, SourceAttribute, MetadataAttribute): + def __init__( + self, + text: FormattedText, + language: pycountry.Languages, + _id: str = None, + dynamic: bool = False, + source_list: List[Source] = None, + **kwargs + ) -> None: + DatabaseObject.__init__(_id=_id, dynamic=dynamic) + + self.text: FormattedText = text + self.language: pycountry.Languages = language + + if source_list is not None: + self.source_list = source_list diff --git a/src/music_kraken/objects/metadata.py b/src/music_kraken/objects/metadata.py index 3194ed3..19298c8 100644 --- a/src/music_kraken/objects/metadata.py +++ b/src/music_kraken/objects/metadata.py @@ -16,7 +16,7 @@ class Mapping(Enum): # Textframes TITLE = "TIT2" ISRC = "TSRC" - LENGTH = "TLEN" # in milliseconds + LENGTH = "TLEN" # in milliseconds DATE = "TYER" TRACKNUMBER = "TRCK" TOTALTRACKS = "TRCK" # Stored in the same frame with TRACKNUMBER, separated by '/': e.g. '4/9'. @@ -193,17 +193,14 @@ class ID3Timestamp: 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): """ diff --git a/src/music_kraken/objects/parents.py b/src/music_kraken/objects/parents.py index d3701db..8ee1f58 100644 --- a/src/music_kraken/objects/parents.py +++ b/src/music_kraken/objects/parents.py @@ -1,96 +1,48 @@ +from typing import Optional import uuid from src.music_kraken.utils.shared import ( - SONG_LOGGER as logger + 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, **kwargs) -> None: + if _id is None and not dynamic: + """ + generates a random UUID + https://docs.python.org/3/library/uuid.html + """ + _id = str(uuid.uuid4()) + LOGGER.info(f"id for {self.__name__} isn't set. Setting to {_id}") + + # The id can only be None, if the object is dynamic (self.dynamic = True) + self.id: Optional[str] = _id - 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") +class MainObject(DatabaseObject): + """ + This is the parent class for all "main" data objects: + - Song + - Album + - Artist + - Label - if self.id_ is None: - self.id_ = str(uuid.uuid4()) - logger.info(f"id for {self.__str__()} isn't set. Setting to {self.id_}") + It has all the functionality of the "DatabaseObject" (it inherits from said class) + but also some added functions as well. + """ + def __init__(self, _id: str = None, dynamic: bool = False, **kwargs): + super().__init__(_id=_id, dynamic=dynamic, **kwargs) - return self.id_ - - def get_reference(self) -> Reference: - return Reference(self.id) + self.additional_arguments: dict = kwargs 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) diff --git a/src/music_kraken/objects/song.py b/src/music_kraken/objects/song.py index d3ead54..6ee8f55 100644 --- a/src/music_kraken/objects/song.py +++ b/src/music_kraken/objects/song.py @@ -1,7 +1,6 @@ import os from typing import List, Optional, Type, Dict import pycountry -import copy from .metadata import ( Mapping as id3Mapping, @@ -10,12 +9,11 @@ from .metadata import ( ) from src.music_kraken.utils.shared import ( MUSIC_DIR, - DATABASE_LOGGER as logger + DATABASE_LOGGER as LOGGER ) from .parents import ( DatabaseObject, - Reference, - SongAttribute + MainObject ) from .source import ( Source, @@ -26,6 +24,8 @@ from .source import ( from .formatted_text import FormattedText from .collection import Collection from .album import AlbumType, AlbumStatus +from .lyrics import Lyrics +from .target import Target """ All Objects dependent @@ -34,77 +34,7 @@ All Objects dependent CountryTyping = type(list(pycountry.countries)[0]) -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) -> Optional[str]: - 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) -> Optional[str]: - 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: pycountry.Languages, - 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 Song(MainObject, 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. @@ -114,14 +44,15 @@ class Song(DatabaseObject, SourceAttribute, MetadataAttribute): def __init__( self, - id_: str = None, + _id: str = None, + dynamic: bool = False, title: str = None, isrc: str = None, length: int = None, tracksort: int = None, genre: str = None, source_list: List[Source] = None, - target_list: Target = None, + target_list: List[Target] = None, lyrics_list: List[Lyrics] = None, album_list: Type['Album'] = None, main_artist_list: List[Type['Artist']] = None, @@ -131,7 +62,7 @@ class Song(DatabaseObject, SourceAttribute, MetadataAttribute): """ Initializes the Song object with the following attributes: """ - super().__init__(id_=id_, **kwargs) + MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) # attributes self.title: str = title self.isrc: str = isrc @@ -242,10 +173,10 @@ All objects dependent on Album """ -class Album(DatabaseObject, SourceAttribute, MetadataAttribute): +class Album(MainObject, SourceAttribute, MetadataAttribute): def __init__( self, - id_: str = None, + _id: str = None, title: str = None, language: pycountry.Languages = None, date: ID3Timestamp = None, @@ -261,7 +192,7 @@ class Album(DatabaseObject, SourceAttribute, MetadataAttribute): label_list: List[Type['Label']] = None, **kwargs ) -> None: - DatabaseObject.__init__(self, id_=id_, dynamic=dynamic, **kwargs) + MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) self.title: str = title self.album_status: AlbumStatus = album_status @@ -384,10 +315,11 @@ All objects dependent on Artist """ -class Artist(DatabaseObject, SourceAttribute, MetadataAttribute): +class Artist(MainObject, SourceAttribute, MetadataAttribute): def __init__( self, - id_: str = None, + _id: str = None, + dynamic: bool = False, name: str = None, source_list: List[Source] = None, feature_song_list: List[Song] = None, @@ -398,8 +330,11 @@ class Artist(DatabaseObject, SourceAttribute, MetadataAttribute): country: CountryTyping = None, formed_in: ID3Timestamp = None, label_list: List[Type['Label']] = None, + **kwargs ): - DatabaseObject.__init__(self, id_=id_) + MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) + + self.name: str = name """ TODO implement album type and notes @@ -412,11 +347,13 @@ class Artist(DatabaseObject, SourceAttribute, MetadataAttribute): i mean do as you want but there aint no strict rule about em so good luck """ self.notes: FormattedText = notes or FormattedText() + """ + TODO + implement in db + """ self.lyrical_themes: List[str] = lyrical_themes or [] self.general_genre = general_genre - self.name: str = name - self.feature_song_collection: Collection = Collection( data=feature_song_list, map_attributes=["title"], @@ -526,17 +463,18 @@ Label """ -class Label(DatabaseObject, SourceAttribute, MetadataAttribute): +class Label(MainObject, SourceAttribute, MetadataAttribute): def __init__( self, - id_: str = None, + _id: str = None, + dynamic: bool = False, name: str = None, album_list: List[Album] = None, current_artist_list: List[Artist] = None, source_list: List[Source] = None, **kwargs ): - DatabaseObject.__init__(self, id_=id_) + MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) self.name: str = name @@ -553,7 +491,11 @@ class Label(DatabaseObject, SourceAttribute, MetadataAttribute): ) self.source_list = source_list or [] - self.additional_attributes = kwargs - album_list = property(fget=lambda self: self.album_collection.copy()) - current_artist_list = property(fget=lambda self: self.current_artist_collection.copy()) + @property + def album_list(self) -> List[Album]: + return self.album_collection.copy() + + @property + def current_artist_list(self) -> List[Artist]: + self.current_artist_collection.copy() diff --git a/src/music_kraken/objects/source.py b/src/music_kraken/objects/source.py index b80ef89..d884545 100644 --- a/src/music_kraken/objects/source.py +++ b/src/music_kraken/objects/source.py @@ -133,7 +133,7 @@ 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_dict: Dict[SourcePages, List[Source]] source_url_map: Dict[str, Source] def __new__(cls, **kwargs): diff --git a/src/music_kraken/objects/target.py b/src/music_kraken/objects/target.py new file mode 100644 index 0000000..7c829a9 --- /dev/null +++ b/src/music_kraken/objects/target.py @@ -0,0 +1,35 @@ +from typing import Optional +from pathlib import Path + +from ..utils import shared +from .parents import DatabaseObject + + +class Target(DatabaseObject): + """ + create somehow like that + ```python + # I know path is pointless, and I will change that (don't worry about backwards compatibility there) + Target(file="song.mp3", path="~/Music/genre/artist/album") + ``` + """ + + def __init__( + self, + file: str = None, + path: str = None, + _id: str = None, + dynamic: bool = False, + relative_to_music_dir: bool = False + ) -> None: + super().__init__(_id=_id, dynamic=dynamic) + self._file: Path = Path(file) + self._path = path + + self.is_relative_to_music_dir: bool = relative_to_music_dir + + @property + def file_path(self) -> Path: + if self.is_relative_to_music_dir: + return Path(shared.MUSIC_DIR, self._path, self._file) + return Path(self._path, self._file)