diff --git a/src/create_custom_objects.py b/src/create_custom_objects.py index 47017be..b3551b6 100644 --- a/src/create_custom_objects.py +++ b/src/create_custom_objects.py @@ -1,63 +1,104 @@ -from music_kraken import objects, recurse - -import pycountry +from music_kraken.objects import ( + Song, + Album, + Artist, + Label, + Source, + DatabaseObject +) +from music_kraken.utils.enums import SourcePages -song = objects.Song( - genre="HS Core", - title="Vein Deep in the Solution", - length=666, - isrc="US-S1Z-99-00001", - tracksort=2, - target=[ - objects.Target(file="song.mp3", path="example") - ], - lyrics_list=[ - objects.Lyrics(text="these are some depressive lyrics", language="en"), - objects.Lyrics(text="Dies sind depressive Lyrics", language="de") - ], - source_list=[ - objects.Source(objects.SourcePages.YOUTUBE, "https://youtu.be/dfnsdajlhkjhsd"), - objects.Source(objects.SourcePages.MUSIFY, "https://ln.topdf.de/Music-Kraken/") - ], - album_list=[ - objects.Album( - title="One Final Action", - date=objects.ID3Timestamp(year=1986, month=3, day=1), - language=pycountry.languages.get(alpha_2="en"), - label_list=[ - objects.Label(name="an album label") +only_smile = Artist( + name="Only Smile", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/")], + main_album_list=[ + Album( + title="Few words...", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/few-words")], + song_list=[ + Song(title="Everything will be fine"), + Song(title="Only Smile"), + Song(title="Dear Diary"), + Song(title="Sad Story") ], - source_list=[ - objects.Source(objects.SourcePages.ENCYCLOPAEDIA_METALLUM, "https://www.metal-archives.com/albums/I%27m_in_a_Coffin/One_Final_Action/207614") - ] - ), - ], - main_artist_list=[ - objects.Artist( - name="I'm in a coffin", - source_list=[ - objects.Source( - objects.SourcePages.ENCYCLOPAEDIA_METALLUM, - "https://www.metal-archives.com/bands/I%27m_in_a_Coffin/127727" - ) - ], - label_list=[ - objects.Label(name="Depressive records") + artist_list=[ + Artist( + name="Only Smile", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/")], + main_album_list=[ + Album( + title="Few words...", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/few-words")], + song_list=[ + Song(title="Everything will be fine"), + Song(title="Only Smile"), + Song(title="Dear Diary"), + Song(title="Sad Story") + ], + artist_list=[ + Artist( + name="Only Smile", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/")] + ) + ] + ), + Album( + title="Your best friend", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/your-best-friend")] + ) + ] + ), + Artist( + name="Only Smile", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/")], + main_album_list=[ + Album( + title="Few words...", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/few-words")], + song_list=[ + Song(title="Everything will be fine"), + Song(title="Only Smile"), + Song(title="Dear Diary"), + Song(title="Sad Story") + ], + artist_list=[ + Artist( + name="Only Smile", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/")] + ) + ] + ), + Album( + title="Your best friend", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/your-best-friend")] + ) + ] + ) ] ), - objects.Artist(name="some_split_artist") - ], - feature_artist_list=[ - objects.Artist( - name="Ruffiction", - label_list=[ - objects.Label(name="Ruffiction Productions") - ] + Album( + title="Your best friend", + source_list=[Source(SourcePages.BANDCAMP, "https://onlysmile.bandcamp.com/album/your-best-friend")] ) - ], + ] ) -song.compile() -print(song.options) +objects_by_id = {} + +def add_to_objects_dump(db_obj: DatabaseObject): + objects_by_id[db_obj.id] = db_obj + + for collection in db_obj.all_collections: + for new_db_obj in collection: + if new_db_obj.id not in objects_by_id: + add_to_objects_dump(new_db_obj) + + +add_to_objects_dump(only_smile) + +for _id, _object in objects_by_id.items(): + print(_id, _object, sep=": ") + +print(only_smile) diff --git a/src/music_kraken/__init__.py b/src/music_kraken/__init__.py index 748a628..555b154 100644 --- a/src/music_kraken/__init__.py +++ b/src/music_kraken/__init__.py @@ -1,7 +1,6 @@ import logging - import gc -import musicbrainzngs +import sys from .utils.shared import DEBUG, DEBUG_LOGGIN from .utils.config import logging_settings, main_settings, read_config @@ -9,6 +8,10 @@ read_config() from . import cli +# I am SO sorry +print(sys.setrecursionlimit(500)) + + # configure logger default logging.basicConfig( level=logging_settings['log_level'] if not DEBUG_LOGGIN else logging.DEBUG, diff --git a/src/music_kraken/objects/collection.py b/src/music_kraken/objects/collection.py index 5310211..4f50ff1 100644 --- a/src/music_kraken/objects/collection.py +++ b/src/music_kraken/objects/collection.py @@ -3,6 +3,11 @@ from collections import defaultdict from dataclasses import dataclass from .parents import DatabaseObject +from ..utils.hooks import HookEventTypes, Hooks, Event + + +class CollectionHooks(HookEventTypes): + APPEND_NEW = "append_new" T = TypeVar('T', bound=DatabaseObject) @@ -46,6 +51,8 @@ class Collection(Generic[T]): self._attribute_to_object_map: Dict[str, Dict[object, T]] = defaultdict(dict) self._used_ids: set = set() + self.hooks: Hooks = Hooks(self) + if data is not None: self.extend(data, merge_on_conflict=True) @@ -74,7 +81,7 @@ class Collection(Generic[T]): pass def append(self, element: T, merge_on_conflict: bool = True, - merge_into_existing: bool = True) -> AppendResult: + merge_into_existing: bool = True, no_hook: bool = False) -> AppendResult: """ :param element: :param merge_on_conflict: @@ -84,6 +91,10 @@ class Collection(Generic[T]): if element is None: return AppendResult(False, None, False) + for existing_element in self._data: + if element is existing_element: + return AppendResult(False, None, False) + # if the element type has been defined in the initializer it checks if the type matches if self.element_type is not None and not isinstance(element, self.element_type): raise TypeError(f"{type(element)} is not the set type {self.element_type}") @@ -117,18 +128,60 @@ class Collection(Generic[T]): self.map_element(element) return AppendResult(True, existing_object, False) + if not no_hook: + self.hooks.trigger_event(CollectionHooks.APPEND_NEW, new_object=element) self._data.append(element) self.map_element(element) return AppendResult(False, element, False) def extend(self, element_list: Iterable[T], merge_on_conflict: bool = True, - merge_into_existing: bool = True): + merge_into_existing: bool = True, no_hook: bool = False): + if element_list is None: + return + if len(element_list) <= 0: + return + if element_list is self: + return for element in element_list: - self.append(element, merge_on_conflict=merge_on_conflict, merge_into_existing=merge_into_existing) + self.append(element, merge_on_conflict=merge_on_conflict, merge_into_existing=merge_into_existing, no_hook=no_hook) + + def sync_collection(self, collection_attribute: str): + def on_append(event: Event, new_object: T, *args, **kwargs): + new_collection = new_object.__getattribute__(collection_attribute) + if self is new_collection: + return + + self.extend(new_object.__getattribute__(collection_attribute), no_hook=True) + new_object.__setattr__(collection_attribute, self) + + self.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_append) + + def sync_main_collection(self, main_collection: "Collection", collection_attribute: str): + def on_append(event: Event, new_object: T, *args, **kwargs): + new_collection = new_object.__getattribute__(collection_attribute) + if main_collection is new_collection: + return + + main_collection.extend(new_object.__getattribute__(collection_attribute), no_hook=True) + new_object.__setattr__(collection_attribute, main_collection) + + self.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_append) + + """ + def on_append(event: Event, new_object: T, *args, **kwargs): + new_collection: Collection = new_object.__getattribute__(collection_attribute) + if self is new_collection: + return + + self.extend(new_collection.shallow_list, no_hook=False) + new_object.__setattr__(collection_attribute, self) + + self.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_append) + """ def __iter__(self) -> Iterator[T]: - for element in self.shallow_list: + for element in self._data: yield element def __str__(self) -> str: diff --git a/src/music_kraken/objects/parents.py b/src/music_kraken/objects/parents.py index e0f629d..24642bf 100644 --- a/src/music_kraken/objects/parents.py +++ b/src/music_kraken/objects/parents.py @@ -11,32 +11,36 @@ from ..utils.config import main_settings, logging_settings LOGGER = logging_settings["object_logger"] -T = TypeVar('T') +P = TypeVar('P') @dataclass -class StaticAttribute(Generic[T]): +class StaticAttribute(Generic[P]): name: str default_value: Any = None weight: float = 0 - is_simple: bool = True - is_collection: bool = False is_downwards_collection: bool = False is_upwards_collection: bool = False -class Attribute(Generic[T]): + + +class Attribute(Generic[P]): def __init__(self, database_object: "DatabaseObject", static_attribute: StaticAttribute) -> None: self.database_object: DatabaseObject = database_object self.static_attribute: StaticAttribute = static_attribute - def get(self) -> T: + @property + def name(self) -> str: + return self.static_attribute.name + + def get(self) -> P: return self.database_object.__getattribute__(self.name) - def set(self, value: T): + def set(self, value: P): self.database_object.__setattr__(self.name, value) @@ -75,16 +79,15 @@ class DatabaseObject: attribute: Attribute = Attribute(self, static_attribute) self._attributes.append(attribute) - if static_attribute.is_simple: - self._simple_attribute_list.append(attribute) - else: + if static_attribute.is_collection: if static_attribute.is_collection: self._collection_attributes.append(attribute) if static_attribute.is_upwards_collection: self._upwards_collection_attributes.append(attribute) if static_attribute.is_downwards_collection: self._downwards_collection_attributes.append(attribute) - + else: + self._simple_attribute_list.append(attribute) # The id can only be None, if the object is dynamic (self.dynamic = True) self.id: Optional[int] = _id @@ -102,6 +105,11 @@ class DatabaseObject: for attribute in self._downwards_collection_attributes: yield attribute.get() + @property + def all_collections(self) -> "Collection": + for attribute in self._collection_attributes: + yield attribute.get() + def __hash__(self): if self.dynamic: raise TypeError("Dynamic DatabaseObjects are unhashable.") @@ -151,8 +159,10 @@ class DatabaseObject: LOGGER.warning(f"can't merge \"{type(other)}\" into \"{type(self)}\"") return - for collection in type(self).COLLECTION_STRING_ATTRIBUTES: - getattr(self, collection).extend(getattr(other, collection)) + for collection in self._collection_attributes: + if hasattr(self, collection.name) and hasattr(other, collection.name): + if collection.get() is not getattr(other, collection.name): + collection.get().extend(getattr(other, collection.name)) for simple_attribute, default_value in type(self).SIMPLE_STRING_ATTRIBUTES.items(): if getattr(other, simple_attribute) == default_value: diff --git a/src/music_kraken/objects/song.py b/src/music_kraken/objects/song.py index dae43ea..ddc883f 100644 --- a/src/music_kraken/objects/song.py +++ b/src/music_kraken/objects/song.py @@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Tuple, Type import pycountry from ..utils.enums.album import AlbumType, AlbumStatus -from .collection import Collection +from .collection import Collection, CollectionHooks from .formatted_text import FormattedText from .lyrics import Lyrics from .contact import Contact @@ -86,7 +86,7 @@ class Song(MainObject): notes: FormattedText = None, **kwargs ) -> None: - MainObject.__init__(self, _id=_id, dynamic=dynamic, **kwargs) + super().__init__(_id=_id, dynamic=dynamic, **kwargs) # attributes self.title: str = title self.unified_title: str = unified_title @@ -102,9 +102,23 @@ class Song(MainObject): self.source_collection: SourceCollection = SourceCollection(source_list) self.target_collection: Collection[Target] = Collection(data=target_list, element_type=Target) self.lyrics_collection: Collection[Lyrics] = Collection(data=lyrics_list, element_type=Lyrics) - self.album_collection: Collection[Album] = Collection(data=album_list, element_type=Album) + + # main_artist_collection = album.artist collection self.main_artist_collection: Collection[Artist] = Collection(data=main_artist_list, element_type=Artist) - self.feature_artist_collection: Collection[Artist] = Collection(data=feature_artist_list, element_type=Artist) + + # this album_collection equals no collection + self.album_collection: Collection[Album] = Collection(data=[], element_type=Album) + self.album_collection.sync_main_collection(self.main_artist_collection, "artist_collection") + self.album_collection.extend(album_list) + # self.album_collection.sync_collection("song_collection") + # self.album_collection.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_album_append) + + # on feature_artist_collection append, append self to artist self + self.feature_artist_collection: Collection[Artist] = Collection(data=[], element_type=Artist) + def on_feature_artist_append(event, new_object: Artist, *args, **kwargs): + new_object.feature_song_collection.append(self, no_hook=True) + self.feature_artist_collection.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_feature_artist_append) + self.feature_artist_collection.extend(feature_artist_list) def _build_recursive_structures(self, build_version: int, merge: bool): if build_version == self.build_version: @@ -307,8 +321,13 @@ class Album(MainObject): self.notes = notes or FormattedText() self.source_collection: SourceCollection = SourceCollection(source_list) - self.song_collection: Collection[Song] = Collection(data=song_list, element_type=Song) + self.artist_collection: Collection[Artist] = Collection(data=artist_list, element_type=Artist) + + self.song_collection: Collection[Song] = Collection(data=[], element_type=Song) + self.song_collection.sync_main_collection(self.artist_collection, "main_artist_collection") + self.song_collection.extend(song_list) + self.label_collection: Collection[Label] = Collection(data=label_list, element_type=Label) def _build_recursive_structures(self, build_version: int, merge: bool): @@ -565,15 +584,28 @@ class Artist(MainObject): self.lyrical_themes: List[str] = lyrical_themes or [] self.general_genre = general_genre + self.unformated_location: Optional[str] = unformated_location self.source_collection: SourceCollection = SourceCollection(source_list) - self.feature_song_collection: Collection[Song] = Collection(data=feature_song_list, element_type=Song) - self.main_album_collection: Collection[Album] = Collection(data=main_album_list, element_type=Album) - self.label_collection: Collection[Label] = Collection(data=label_list, element_type=Label) - self.contact_collection: Collection[Label] = Collection(data=contact_list, element_type=Contact) - self.unformated_location: Optional[str] = unformated_location + self.feature_song_collection: Collection[Song] = Collection(data=[], element_type=Song) + def on_feature_song_append(event, new_object: Song, *args, **kwargs): + new_object.feature_artist_collection.append(self, no_hook=True) + self.feature_song_collection.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_feature_song_append) + self.feature_song_collection.extend(feature_song_list) + + self.main_album_collection: Collection[Album] = Collection(data=[], element_type=Album) + def on_album_append(event, new_object: Album, *args, **kwargs): + new_object.artist_collection.append(self, no_hook=True) + self.main_album_collection.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_album_append) + self.main_album_collection.extend(main_album_list) + + self.label_collection: Collection[Label] = Collection(data=label_list, element_type=Label) + def on_label_append(event, new_object: Label, *args, **kwargs): + new_object.current_artist_collection.append(self, no_hook=True) + self.label_collection.hooks.add_event_listener(CollectionHooks.APPEND_NEW, on_label_append) + def _add_other_db_objects(self, object_type: Type["DatabaseObject"], object_list: List["DatabaseObject"]): if object_type is Song: diff --git a/src/music_kraken/pages/abstract.py b/src/music_kraken/pages/abstract.py index 4995b99..16d127c 100644 --- a/src/music_kraken/pages/abstract.py +++ b/src/music_kraken/pages/abstract.py @@ -140,6 +140,9 @@ def clean_object(dirty_object: DatabaseObject) -> DatabaseObject: Song: Collection(element_type=Song) } + if isinstance(dirty_object, Song): + return dirty_object + _clean_music_object(dirty_object, collections) return dirty_object @@ -153,7 +156,7 @@ def merge_together(old_object: DatabaseObject, new_object: DatabaseObject, do_co new_object = clean_object(new_object) old_object.merge(new_object) - if do_compile: + if do_compile and False: old_object.compile(merge_into=False) return old_object @@ -274,7 +277,7 @@ class Page: post_process=False )) - return merge_together(music_object, new_music_object, do_compile=post_process) + return music_object.merge(new_music_object) def fetch_object_from_source(self, source: Source, stop_at_level: int = 2, enforce_type: Type[DatabaseObject] = None, post_process: bool = True) -> Optional[DatabaseObject]: obj_type = self.get_source_type(source) @@ -308,10 +311,7 @@ class Page: for sub_element in collection: sub_element.merge(self.fetch_details(sub_element, stop_at_level=stop_at_level-1, post_process=False)) - - if post_process and music_object: - return build_new_object(music_object) - + return music_object def fetch_song(self, source: Source, stop_at_level: int = 1) -> Song: diff --git a/src/music_kraken/utils/enums/__init__.py b/src/music_kraken/utils/enums/__init__.py index e69de29..b90f9aa 100644 --- a/src/music_kraken/utils/enums/__init__.py +++ b/src/music_kraken/utils/enums/__init__.py @@ -0,0 +1 @@ +from .source import SourcePages \ No newline at end of file diff --git a/src/music_kraken/utils/hooks.py b/src/music_kraken/utils/hooks.py new file mode 100644 index 0000000..e3cd954 --- /dev/null +++ b/src/music_kraken/utils/hooks.py @@ -0,0 +1,29 @@ +from typing import List, Iterable, Dict, TypeVar, Generic, Iterator, Any, Type +from enum import Enum +from dataclasses import dataclass +from collections import defaultdict + + +class HookEventTypes(Enum): + pass + + +@dataclass +class Event: + target: Any + + +class Hooks: + def __init__(self, target) -> None: + self.target = target + + self._callbacks: Dict[HookEventTypes, List[callable]] = defaultdict(list) + + def add_event_listener(self, event_type: HookEventTypes, callback: callable): + self._callbacks[event_type].append(callback) + + def trigger_event(self, event_type: HookEventTypes, *args, **kwargs): + event: Event = Event(target=self.target) + + for callback in self._callbacks[event_type]: + callback(event, *args, **kwargs)