From 64477d61fa9ccea7b3f29199979326e47f55fb23 Mon Sep 17 00:00:00 2001 From: lars Date: Wed, 26 Oct 2022 17:42:11 +0200 Subject: [PATCH 01/35] asddasd --- README.md | 5 +- src/download.py | 10 +- src/download_links.py | 5 +- src/main.py | 15 +- src/metadata.py | 461 ------------------------------- src/metadata/download.py | 424 ++++++++++++++++++++++++++++ src/metadata/metadata.py | 142 ++++++++++ src/metadata/object_handeling.py | 22 ++ src/metadata/options.py | 118 ++++++++ src/musify.py | 15 +- 10 files changed, 738 insertions(+), 479 deletions(-) delete mode 100644 src/metadata.py create mode 100644 src/metadata/download.py create mode 100644 src/metadata/metadata.py create mode 100644 src/metadata/object_handeling.py create mode 100644 src/metadata/options.py diff --git a/README.md b/README.md index 01df802..26f48dc 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,10 @@ For searching, as well as for downloading I use the programm `youtube-dl`, which There are two bottlenecks with this approach though: 1. `youtube-dl` is just slow. Actually it has to be, to not get blocked by youtube. -2. Ofthen musicbrainz just doesn't give the isrc for some songs. +2. Often musicbrainz just doesn't give the isrc for some songs. **TODO** - look at how the isrc id derived an try to generate it for the tracks without directly getting it from mb. + +**Progress** +- There is a great site whith a huge isrc database [https://isrc.soundexchange.com/](https://isrc.soundexchange.com/). diff --git a/src/download.py b/src/download.py index 6f1d185..342c945 100644 --- a/src/download.py +++ b/src/download.py @@ -50,13 +50,11 @@ def path_stuff(path: str, file_: str): class Download: - def __init__(self, session: requests.Session = requests.Session(), file: str = ".cache3.csv", temp: str = "temp", + def __init__(self, proxies: dict = None, file: str = ".cache3.csv", temp: str = "temp", base_path: str = ""): - self.session = session - self.session.headers = { - "Connection": "keep-alive", - "Referer": "https://musify.club/" - } + if proxies is not None: + musify.set_proxy(proxies) + self.temp = temp self.file = file diff --git a/src/download_links.py b/src/download_links.py index 78e7178..22baec6 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -9,8 +9,11 @@ import youtube_music class Download: - def __init__(self, metadata_csv: str = ".cache1.csv", session: requests.Session = requests.Session(), + def __init__(self, metadata_csv: str = ".cache1.csv", proxies: dict = None, file: str = ".cache2.csv", temp: str = "temp") -> None: + if proxies is not None: + musify.set_proxy(proxies) + self.temp = temp self.metadata = pd.read_csv(os.path.join(self.temp, metadata_csv), index_col=0) diff --git a/src/main.py b/src/main.py index c92a055..507beab 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,9 @@ -import metadata +import metadata.metadata import download_links import url_to_path import download import logging -import requests import os @@ -15,7 +14,7 @@ STEP_THREE_CACHE = ".cache3.csv" NOT_A_GENRE = ".", "..", "misc_scripts", "Music", "script", ".git", ".idea" MUSIC_DIR = os.path.expanduser('~/Music') -TOR = False +TOR = True logging.basicConfig(level=logging.INFO) @@ -30,7 +29,7 @@ def get_existing_genre(): def search_for_metadata(query: str): - search = metadata.Search(query=query, temp=TEMP) + search = metadata.metadata.Search(query=query, temp=TEMP) print(search.options) while True: @@ -71,9 +70,9 @@ def get_genre(): def cli(start_at: int = 0): - session = requests.Session() + proxies = None if TOR: - session.proxies = { + proxies = { 'http': 'socks5h://127.0.0.1:9150', 'https': 'socks5h://127.0.0.1:9150' } @@ -89,7 +88,7 @@ def cli(start_at: int = 0): if start_at <= 1: logging.info("Fetching Download Links") - download_links.Download(file=STEP_TWO_CACHE, metadata_csv=STEP_ONE_CACHE, temp=TEMP, session=session) + download_links.Download(file=STEP_TWO_CACHE, metadata_csv=STEP_ONE_CACHE, temp=TEMP, proxies=proxies) if start_at <= 2: logging.info("creating Paths") @@ -97,7 +96,7 @@ def cli(start_at: int = 0): if start_at <= 3: logging.info("starting to download the mp3's") - download.Download(session=session, file=STEP_THREE_CACHE, temp=TEMP, base_path=MUSIC_DIR) + download.Download(proxies=proxies, file=STEP_THREE_CACHE, temp=TEMP, base_path=MUSIC_DIR) if __name__ == "__main__": diff --git a/src/metadata.py b/src/metadata.py deleted file mode 100644 index 8a6d24c..0000000 --- a/src/metadata.py +++ /dev/null @@ -1,461 +0,0 @@ -import os.path -import logging - -import musicbrainzngs -import pandas as pd - -mb_log = logging.getLogger("musicbrainzngs") -mb_log.setLevel(logging.WARNING) -musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") - -KNOWN_KIND_OF_OPTIONS = ["artist", "release", "track"] - - -def output(msg: str): - print(msg) - - -def get_elem_from_obj(current_object, keys: list, after_process=lambda x: x, return_if_none=None): - current_object = current_object - for key in keys: - if key in current_object or (type(key) == int and key < len(current_object)): - current_object = current_object[key] - else: - return return_if_none - return after_process(current_object) - - -class Search: - def __init__(self, query: str = None, artist: str = None, temp: str = "temp"): - if query is None and artist is None: - raise ValueError("no query provided") - - self.options_history = [] - self.current_options = None - self.current_chosen_option = None - - self.temp = temp - - # initial search - if query is not None: - self.set_options(self.Options([musicbrainzngs.search_artists(query), musicbrainzngs.search_releases(query), - musicbrainzngs.search_recordings(query)])) - elif artist is not None: - self.set_options(self.Options([musicbrainzngs.search_artists(artist=artist)])) - - def download(self, file: str = ".cache1.csv"): - kind = self.current_chosen_option['kind'] - mb_id = self.current_chosen_option['id'] - - metadata_list = [] - if kind == "artist": - - metadata_list = self.download_artist(mb_id) - elif kind == "release": - metadata_list = self.download_release(mb_id) - elif kind == "track": - metadata_list = self.download_track(mb_id) - - metadata_df = pd.DataFrame(metadata_list) - metadata_df.to_csv(os.path.join(self.temp, file)) - - return metadata_df - - def download_artist(self, mb_id): - """ - Available includes: recordings, releases, release-groups, works, various-artists, discids, media, isrcs, - aliases, annotation, area-rels, artist-rels, label-rels, place-rels, event-rels, recording-rels, - release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels, tags, user-tags, - ratings, user-ratings - """ - metadata_list = [] - result = musicbrainzngs.get_artist_by_id(mb_id, includes=["releases"]) - - for i, release in enumerate(result["artist"]["release-list"]): - metadata_list.extend(self.download_release(release["id"], i)) - return metadata_list - - def download_release(self, mb_id, album_sort: int = None): - """ - Available includes: artists, labels, recordings, release-groups, media, artist-credits, discids, isrcs, - recording-level-rels, work-level-rels, annotation, aliases, tags, user-tags, area-rels, artist-rels, - label-rels, place-rels, event-rels, recording-rels, release-rels, release-group-rels, series-rels, url-rels, - work-rels, instrument-rels - """ - - def get_additional_artist_info(mb_id_): - r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) - - album_sort = 0 - for i, release in enumerate(r["artist"]["release-list"]): - id_ = release["id"] - if id_ == mb_id: - album_sort = i - break - - return album_sort - - result = musicbrainzngs.get_release_by_id(mb_id, includes=["artists", "recordings", 'release-groups']) - - if album_sort is None: - album_sort = get_additional_artist_info( - get_elem_from_obj(result, ['release', 'artist-credit', 0, 'artist', 'id'])) - release_type = get_elem_from_obj(result, ['release', 'release-group', 'type']) - - tracklist_metadata = [] - - is_various_artist = len(result['release']['artist-credit']) > 1 - tracklist = result['release']['medium-list'][0]['track-list'] - track_count = len(tracklist) - this_track = 0 - for track in tracklist: - track_id = track["recording"]["id"] - this_track = track["position"] - - tracklist_metadata.extend( - self.download_track(track_id, is_various_artist=is_various_artist, track=this_track, - total_tracks=track_count, album_sort=album_sort, album_type=release_type, - release_data=result['release'])) - - return tracklist_metadata - - def download_track(self, mb_id, is_various_artist: bool = None, track: int = None, total_tracks: int = None, - album_sort: int = None, album_type: str = None, release_data: dict = None): - """ - TODO - bpm its kind of possible via the AcousticBrainz API. however, the data may not be of very good - quality and AB is scheduled to go away in some time. - - compilation Field that is used by iTunes to mark albums as compilation. - Either enter the value 1 or delete the field. https://en.wikipedia.org/wiki/Compilation_album - How should I get it? I don't fucking know. Now I do. Release Group Type is Compilation - - composer, copyright, discsubtitle - 'musicbrainz_discid', - 'asin', - 'performer', - 'catalognumber', - 'musicbrainz_releasetrackid', - 'musicbrainz_releasegroupid', - 'musicbrainz_workid', - 'acoustid_fingerprint', - 'acoustid_id' - - DONE - - album - title - artist - albumartist - tracknumber - !!!albumsort can sort albums cronological - titlesort is just set to the tracknumber to sort by track order to sort correctly - isrc - musicbrainz_artistid - musicbrainz_albumid - musicbrainz_albumartistid - musicbrainz_albumstatus - language - musicbrainz_albumtype - 'releasecountry' - 'barcode' - - Album Art - """ - """ - Available includes: artists, releases, discids, media, artist-credits, isrcs, work-level-rels, annotation, - aliases, tags, user-tags, ratings, user-ratings, area-rels, artist-rels, label-rels, place-rels, event-rels, - recording-rels, release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels - """ - - result = musicbrainzngs.get_recording_by_id(mb_id, includes=["artists", "releases", "recording-rels", "isrcs", - "work-level-rels"]) - recording_data = result['recording'] - isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) - - if release_data is None: - # choosing the last release, because it is the least likely one to be a single - release_data = recording_data['release-list'][-1] - mb_release_id = release_data['id'] - - title = recording_data['title'] - - artist = [] - mb_artist_ids = [] - for artist_ in recording_data['artist-credit']: - name_ = get_elem_from_obj(artist_, ['artist', 'name']) - if name_ is None: - continue - artist.append(name_) - mb_artist_ids.append(get_elem_from_obj(artist_, ['artist', 'id'])) - - def get_additional_artist_info(mb_id_): - r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) - - album_sort = 0 - for i, release in enumerate(r["artist"]["release-list"]): - id_ = release["id"] - if id_ == mb_release_id: - album_sort = i - break - - return album_sort - - def get_additional_release_info(mb_id_): - r = musicbrainzngs.get_release_by_id(mb_id_, - includes=["artists", "recordings", "recording-rels", 'release-groups']) - is_various_artist_ = len(r['release']['artist-credit']) > 1 - tracklist = r['release']['medium-list'][0]['track-list'] - track_count_ = len(tracklist) - this_track_ = 0 - for track in tracklist: - if track["recording"]["id"] == mb_id: - this_track_ = track["position"] - - release_type = get_elem_from_obj(r, ['release', 'release-group', 'type']) - - return is_various_artist_, this_track_, track_count_, release_type - - album_id = get_elem_from_obj(release_data, ['id']) - album = get_elem_from_obj(release_data, ['title']) - album_status = get_elem_from_obj(release_data, ['status']) - language = get_elem_from_obj(release_data, ['text-representation', 'language']) - - year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) - date = get_elem_from_obj(release_data, ['date']) - if is_various_artist is None or track is None or total_tracks is None or album_type is None: - is_various_artist, track, total_tracks, album_type = get_additional_release_info(album_id) - if album_sort is None: - album_sort = get_additional_artist_info(mb_artist_ids[0]) - album_artist = "Various Artists" if is_various_artist else artist[0] - album_artist_id = None if album_artist == "Various Artists" else mb_artist_ids[0] - compilation = "1" if album_type == "Compilation" else None - country = get_elem_from_obj(release_data, ['country']) - barcode = get_elem_from_obj(release_data, ['barcode']) - - return [{ - 'id': mb_id, - 'album': album, - 'title': title, - 'artist': artist, - 'album_artist': album_artist, - 'tracknumber': str(track), - 'albumsort': album_sort, - 'titlesort': track, - 'isrc': isrc, - 'date': date, - 'year': year, - 'musicbrainz_artistid': mb_artist_ids[0], - 'musicbrainz_albumid': mb_release_id, - 'musicbrainz_albumartistid': album_artist_id, - 'musicbrainz_albumstatus': album_status, - 'total_tracks': total_tracks, - 'language': language, - 'musicbrainz_albumtype': album_type, - 'compilation': compilation, - 'releasecountry': country, - 'barcode': barcode - }] - - def browse_artist(self, artist: dict, limit: int = 25): - options_sets = [ - {"artist-list": [artist, ], "artist-count": 1}, - musicbrainzngs.browse_releases(artist=artist["id"], limit=limit), - musicbrainzngs.browse_recordings(artist=artist["id"], limit=limit) - ] - return self.set_options(self.Options(options_sets)) - - def browse_release(self, release: dict, limit: int = 25): - options_sets = [ - musicbrainzngs.browse_artists(release=release["id"], limit=limit), - {"release-list": [release, ], "release-count": 1}, - musicbrainzngs.browse_recordings(release=release["id"], limit=limit) - ] - return self.set_options(self.Options(options_sets)) - - def browse_track(self, track: dict, limit: int = 25): - options_sets = [ - musicbrainzngs.browse_artists(recording=track["id"], limit=limit), - musicbrainzngs.browse_releases(recording=track["id"], limit=limit), - {"recording-list": [track, ], "recording-count": 1} - ] - return self.set_options(self.Options(options_sets)) - - def choose(self, index, limit: int = 25, ignore_limit_for_tracklist: bool = True): - if not self.current_options.choose(index): - return self.current_options - - self.current_chosen_option = self.current_options.get_current_option() - kind = self.current_chosen_option['kind'] - if kind == 'artist': - return self.browse_artist(self.current_chosen_option, limit=limit) - if kind == 'release': - release_limit = limit if not ignore_limit_for_tracklist else 100 - return self.browse_release(self.current_chosen_option, limit=release_limit) - if kind == 'track': - track_limit = limit if not ignore_limit_for_tracklist else 100 - return self.browse_track(self.current_chosen_option, limit=track_limit) - - return self.current_options - - def get_options(self): - return self.current_options - - def set_options(self, option_instance): - self.options_history.append(option_instance) - self.current_options = option_instance - - return option_instance - - def get_previous_options(self): - self.options_history.pop(-1) - self.current_options = self.options_history[-1] - return self.current_options - - options = property(fget=get_options) - - class Options: - def __init__(self, results: list): - self.results = results - - self.artist_count = 0 - self.release_count = 0 - self.track_count = 0 - self.result_list = [] - self.set_options_values() - - self.current_option_ind = None - - def get_current_option(self): - if self.current_option_ind is None: - raise Exception("It must first be chosen, which option to get, before getting it") - - return self.result_list[self.current_option_ind] - - def choose(self, index: int) -> bool: - if len(self.result_list) <= index - 1: - return False - self.current_option_ind = index - return True - - def get_string_for_artist(self, artist: dict) -> str: - string = f"'{artist['name']}'" - if "country" in artist: - string += f" from {artist['country']}" - if 'disambiguation' in artist: - string += f", '{artist['disambiguation']}'" - return string + "\n" - - def get_string_for_release(self, release: dict) -> str: - string = "" - if "type" in release: - string += f"the {release['type']} titled " - string += f"'{release['title']}'" - if "artist-credit-phrase" in release: - string += f" by: {release['artist-credit-phrase']}" - - return string + "\n" - - def get_string_for_tracks(self, tracks: dict) -> str: - # I know it's not the best practice but whatever - return self.get_string_for_release(tracks) - - def get_string_for_option(self, option: dict) -> str: - kind = option['kind'] - if kind == "artist": - return self.get_string_for_artist(option) - if kind == "release": - return self.get_string_for_release(option) - if kind == "track": - return self.get_string_for_tracks(option) - return "Error\n" - - def __str__(self) -> str: - string = f"artists: {self.artist_count}; releases {self.release_count}; tracks {self.track_count}\n" - for i, option in enumerate(self.result_list): - string += f"{i})\t{option['kind']}:\t" + self.get_string_for_option(option) - return string - - def set_options_values(self): - for option_set in self.results: - if "artist-list" in option_set: - self.set_artist_values(option_set) - continue - if "release-list" in option_set: - self.set_release_values(option_set) - continue - if "recording-list" in option_set: - self.set_track_values(option_set) - continue - - def set_artist_values(self, option_set: dict): - self.artist_count += option_set['artist-count'] - for artist in option_set['artist-list']: - artist['kind'] = "artist" - self.result_list.append(artist) - - def set_release_values(self, option_set: dict): - self.release_count += option_set['release-count'] - for release in option_set['release-list']: - release['kind'] = "release" - self.result_list.append(release) - - def set_track_values(self, option_set: dict): - self.track_count += option_set['recording-count'] - for track in option_set['recording-list']: - track['kind'] = "track" - self.result_list.append(track) - - -def automated_demo(): - search = Search(query="psychonaut 4") - print(search.options) - print(search.choose(0)) - search.download() - print(search.choose(2)) - search.download() - print(search.choose(4)) - print(search.download()) - - -def interactive_demo(): - search = Search(query=input("initial query: ")) - print(search.options) - while True: - input_ = input( - "d to download, q to quit, .. for previous options, . for current options, int for this element: ").lower() - input_.strip() - if input_ == "q": - break - if input_ == ".": - print(search.options) - continue - if input_ == "..": - print(search.get_previous_options()) - continue - if input_.isdigit(): - print(search.choose(int(input_))) - continue - if input_ == "d": - search.download() - break - - -if __name__ == "__main__": - # interactive_demo() - # automated_demo() - search = Search(query="psychonaut 4") - # search.download_release("27f00fb8-983c-4d5c-950f-51418aac55dc") - search.download_release("1aeb676f-e556-4b17-b45e-64ab69ef0375") - # for track_ in search.download_artist("c0c720b5-012f-4204-a472-981403f37b12"): - # print(track_) - # res = search.download_track("83a30323-aee1-401a-b767-b3c1bdd026c0") - # res = search.download_track("5e1ee2c5-502c-44d3-b1bc-22803441d8c6") - res = search.download_track("86b43bec-eea6-40ae-8624-c1e404204ba1") - # res = search.download_track("5cc28584-10c6-40e2-b6d4-6891e7e7c575") - - for key in res[0]: - if res[0][key] is None: - continue - - print(key, res[0][key]) diff --git a/src/metadata/download.py b/src/metadata/download.py new file mode 100644 index 0000000..08ed57d --- /dev/null +++ b/src/metadata/download.py @@ -0,0 +1,424 @@ +from typing import List + +import musicbrainzngs +import pandas as pd +import logging +from datetime import date + +from object_handeling import get_elem_from_obj, parse_music_brainz_date + +mb_log = logging.getLogger("musicbrainzngs") +mb_log.setLevel(logging.WARNING) +musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") + + +# IMPORTANT +# https://python-musicbrainzngs.readthedocs.io/en/v0.7.1/api/#getting-data + +class Artist: + def __init__( + self, + musicbrainz_artistid: str, + release_groups: List = [], + new_release_groups: bool = True + ): + """ + release_groups: list + """ + self.release_groups = release_groups + + self.musicbrainz_artistid = musicbrainz_artistid + + result = musicbrainzngs.get_artist_by_id(self.musicbrainz_artistid, includes=["release-groups", "releases"]) + artist_data = get_elem_from_obj(result, ['artist'], return_if_none={}) + + self.artist = get_elem_from_obj(artist_data, ['name']) + + if not new_release_groups: + return + # sort all release groups by date and add album sort to have them in chronological order. + release_groups = artist_data['release-group-list'] + for i, release_group in enumerate(release_groups): + release_groups[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) + release_groups.sort(key=lambda x: x['first-release-date']) + + for i, release_group in enumerate(release_groups): + self.release_groups.append(ReleaseGroup( + musicbrainz_releasegroupid=release_group['id'], + artists=[self], + albumsort=i + 1 + )) + + def __str__(self): + newline = "\n" + return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" + + +class ReleaseGroup: + def __init__( + self, + musicbrainz_releasegroupid: str, + artists: List[Artist] = [], + albumsort: int = None, + only_download_distinct_releases: bool = True + ): + """ + split_artists: list -> if len > 1: album_artist=VariousArtists + releases: list + """ + + self.musicbrainz_releasegroupid = musicbrainz_releasegroupid + self.artists = artists + self.releases = [] + + result = musicbrainzngs.get_release_group_by_id(musicbrainz_releasegroupid, + includes=["artist-credits", "releases"]) + release_group_data = get_elem_from_obj(result, ['release-group'], return_if_none={}) + artist_datas = get_elem_from_obj(release_group_data, ['artist-credit'], return_if_none={}) + release_datas = get_elem_from_obj(release_group_data, ['release-list'], return_if_none={}) + + for artist_data in artist_datas: + artist_id = get_elem_from_obj(artist_data, ['artist', 'id']) + if artist_id is None: + continue + self.append_artist(artist_id) + self.albumartist = "Various Artists" if len(self.artists) >= 1 else self.artists[0].artist + + self.albumsort = albumsort + self.musicbrainz_albumtype = get_elem_from_obj(release_group_data, ['primary-type']) + self.compilation = "1" if self.musicbrainz_albumtype == "Compilation" else None + + if only_download_distinct_releases: + self.append_distinct_releases(release_datas) + else: + self.append_all_releases(release_datas) + + def __str__(self): + newline = "\n" + return f"{newline.join([str(release_group) for release_group in self.releases])}" + + def append_artist(self, artist_id: str) -> Artist: + for existing_artist in self.artists: + if artist_id == existing_artist.musicbrainz_artistid: + return existing_artist + new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) + self.artists.append(new_artist) + return new_artist + + def append_release(self, release_data: dict): + musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) + if musicbrainz_albumid is None: + return + self.releases.append(Release(musicbrainz_albumid, release_group=self)) + + def append_distinct_releases(self, release_datas: List[dict]): + titles = {} + + for release_data in release_datas: + title = get_elem_from_obj(release_data, ['title']) + if title is None: + continue + titles[title] = release_data + + for key in titles: + self.append_release(titles[key]) + + def append_all_releases(self, release_datas: List[dict]): + for release_data in release_datas: + self.append_release(release_data) + + +class Release: + def __init__( + self, + musicbrainz_albumid: str, + release_group: ReleaseGroup = None + ): + """ + release_group: ReleaseGroup + tracks: list + """ + self.musicbrainz_albumid = musicbrainz_albumid + self.release_group = release_group + self.tracklist = [] + + result = musicbrainzngs.get_release_by_id(self.musicbrainz_albumid, includes=["recordings", "labels"]) + release_data = get_elem_from_obj(result, ['release'], return_if_none={}) + label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={}) + recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[]) + + self.title = get_elem_from_obj(release_data, ['title']) + self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) + + self.append_recordings(recording_datas) + + def append_recordings(self, recording_datas: dict): + for recording_data in recording_datas: + musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['id']) + if musicbrainz_releasetrackid is None: + continue + + self.tracklist.append(musicbrainz_releasetrackid) + + def __str__(self): + return f"{self.title} ©{self.copyright}" + + +class Track: + def __init__( + self, + musicbrainz_releasetrackid: str, + release: Release = None + ): + """ + release: Release + feature_artists: list + """ + + self.musicbrainz_releasetrackid = musicbrainz_releasetrackid + self.release = release + + +def download(option: dict): + type_ = option['type'] + mb_id = option['id'] + + metadata_list = [] + if type_ == "artist": + artist = Artist(mb_id) + print(artist) + elif type_ == "release": + metadata_list = download_release(mb_id) + elif type_ == "track": + metadata_list = download_track(mb_id) + + print(metadata_list) + metadata_df = pd.DataFrame(metadata_list) + # metadata_df.to_csv(os.path.join(self.temp, file)) + return metadata_df + + +def download_artist(mb_id): + """ + Available includes: recordings, releases, release-groups, works, various-artists, discids, media, isrcs, + aliases, annotation, area-rels, artist-rels, label-rels, place-rels, event-rels, recording-rels, + release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels, tags, user-tags, + ratings, user-ratings + """ + + metadata_list = [] + # from this dict everything will be taken + following_data = {} + + result = musicbrainzngs.get_artist_by_id(mb_id, includes=["release-groups", "releases"]) + artist_data = result['artist'] + + # sort all release groups by date and add album sort to have them in chronological order. + release_groups = artist_data['release-group-list'] + for i, release_group in enumerate(release_groups): + release_groups[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) + release_groups.sort(key=lambda x: x['first-release-date']) + + for i, release_group in enumerate(release_groups): + release_groups[i]['albumsort'] = i + 1 + + def numeric_release_type(release_type: str) -> int: + if release_type == "Album" or release_type == "EP": + return 1 + return 2 + + release_groups.sort(key=lambda x: numeric_release_type(x['type'])) + + for release_group in release_groups: + download_release_groups() + + +def download_release(mb_id, album_sort: int = None): + """ + Available includes: artists, labels, recordings, release-groups, media, artist-credits, discids, isrcs, + recording-level-rels, work-level-rels, annotation, aliases, tags, user-tags, area-rels, artist-rels, + label-rels, place-rels, event-rels, recording-rels, release-rels, release-group-rels, series-rels, url-rels, + work-rels, instrument-rels + """ + + def get_additional_artist_info(mb_id_): + r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) + + album_sort = 0 + for i, release in enumerate(r["artist"]["release-list"]): + id_ = release["id"] + if id_ == mb_id: + album_sort = i + break + + return album_sort + + result = musicbrainzngs.get_release_by_id(mb_id, includes=["artists", "recordings", 'release-groups']) + + if album_sort is None: + album_sort = get_additional_artist_info( + get_elem_from_obj(result, ['release', 'artist-credit', 0, 'artist', 'id'])) + release_type = get_elem_from_obj(result, ['release', 'release-group', 'type']) + + tracklist_metadata = [] + + is_various_artist = len(result['release']['artist-credit']) > 1 + tracklist = result['release']['medium-list'][0]['track-list'] + track_count = len(tracklist) + for track in tracklist: + track_id = track["recording"]["id"] + this_track = track["position"] + + tracklist_metadata.extend( + download_track(track_id, is_various_artist=is_various_artist, track=this_track, + total_tracks=track_count, album_sort=album_sort, album_type=release_type, + release_data=result['release'])) + + return tracklist_metadata + + +def download_track(mb_id, is_various_artist: bool = None, track: int = None, total_tracks: int = None, + album_sort: int = None, album_type: str = None, release_data: dict = None): + """ + TODO + bpm its kind of possible via the AcousticBrainz API. however, the data may not be of very good + quality and AB is scheduled to go away in some time. + + compilation Field that is used by iTunes to mark albums as compilation. + Either enter the value 1 or delete the field. https://en.wikipedia.org/wiki/Compilation_album + How should I get it? I don't fucking know. Now I do. Release Group Type is Compilation + + composer, copyright, discsubtitle + 'musicbrainz_discid', + 'asin', + 'performer', + 'catalognumber', + 'musicbrainz_releasetrackid', + 'musicbrainz_releasegroupid', + 'musicbrainz_workid', + 'acoustid_fingerprint', + 'acoustid_id' + + DONE + + album + title + artist + albumartist + tracknumber + !!!albumsort can sort albums cronological + titlesort is just set to the tracknumber to sort by track order to sort correctly + isrc + musicbrainz_artistid + musicbrainz_albumid + musicbrainz_albumartistid + musicbrainz_albumstatus + language + musicbrainz_albumtype + 'releasecountry' + 'barcode' + + Album Art + """ + """ + Available includes: artists, releases, discids, media, artist-credits, isrcs, work-level-rels, annotation, + aliases, tags, user-tags, ratings, user-ratings, area-rels, artist-rels, label-rels, place-rels, event-rels, + recording-rels, release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels + """ + + result = musicbrainzngs.get_recording_by_id(mb_id, includes=["artists", "releases", "recording-rels", "isrcs", + "work-level-rels"]) + recording_data = result['recording'] + isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) + + if release_data is None: + # choosing the last release, because it is the least likely one to be a single + release_data = recording_data['release-list'][-1] + mb_release_id = release_data['id'] + + title = recording_data['title'] + + artist = [] + mb_artist_ids = [] + for artist_ in recording_data['artist-credit']: + name_ = get_elem_from_obj(artist_, ['artist', 'name']) + if name_ is None: + continue + artist.append(name_) + mb_artist_ids.append(get_elem_from_obj(artist_, ['artist', 'id'])) + + def get_additional_artist_info(mb_id_): + r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) + + album_sort = 0 + for i, release in enumerate(r["artist"]["release-list"]): + id_ = release["id"] + if id_ == mb_release_id: + album_sort = i + break + + return album_sort + + def get_additional_release_info(mb_id_): + r = musicbrainzngs.get_release_by_id(mb_id_, + includes=["artists", "recordings", "recording-rels", 'release-groups']) + is_various_artist_ = len(r['release']['artist-credit']) > 1 + tracklist = r['release']['medium-list'][0]['track-list'] + track_count_ = len(tracklist) + this_track_ = 0 + for track in tracklist: + if track["recording"]["id"] == mb_id: + this_track_ = track["position"] + + release_type = get_elem_from_obj(r, ['release', 'release-group', 'type']) + + return is_various_artist_, this_track_, track_count_, release_type + + album_id = get_elem_from_obj(release_data, ['id']) + album = get_elem_from_obj(release_data, ['title']) + album_status = get_elem_from_obj(release_data, ['status']) + language = get_elem_from_obj(release_data, ['text-representation', 'language']) + + year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) + date = get_elem_from_obj(release_data, ['date']) + if is_various_artist is None or track is None or total_tracks is None or album_type is None: + is_various_artist, track, total_tracks, album_type = get_additional_release_info(album_id) + if album_sort is None: + album_sort = get_additional_artist_info(mb_artist_ids[0]) + album_artist = "Various Artists" if is_various_artist else artist[0] + album_artist_id = None if album_artist == "Various Artists" else mb_artist_ids[0] + compilation = "1" if album_type == "Compilation" else None + country = get_elem_from_obj(release_data, ['country']) + barcode = get_elem_from_obj(release_data, ['barcode']) + + return [{ + 'id': mb_id, + 'album': album, + 'title': title, + 'artist': artist, + 'album_artist': album_artist, + 'tracknumber': str(track), + 'albumsort': album_sort, + 'titlesort': track, + 'isrc': isrc, + 'date': date, + 'year': year, + 'musicbrainz_artistid': mb_artist_ids[0], + 'musicbrainz_albumid': mb_release_id, + 'musicbrainz_albumartistid': album_artist_id, + 'musicbrainz_albumstatus': album_status, + 'total_tracks': total_tracks, + 'language': language, + 'musicbrainz_albumtype': album_type, + 'compilation': compilation, + 'releasecountry': country, + 'barcode': barcode + }] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + + download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) + # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) + # download({'id': 'c24ed9e7-6df9-44de-8570-975f1a5a75d1', 'type': 'track'}) diff --git a/src/metadata/metadata.py b/src/metadata/metadata.py new file mode 100644 index 0000000..25c6680 --- /dev/null +++ b/src/metadata/metadata.py @@ -0,0 +1,142 @@ +import logging +import musicbrainzngs + +import options +from object_handeling import get_elem_from_obj + +mb_log = logging.getLogger("musicbrainzngs") +mb_log.setLevel(logging.WARNING) +musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") + +KNOWN_KIND_OF_OPTIONS = ["artist", "release", "track"] + + +class Search: + def __init__(self, query: str = None, artist: str = None, temp: str = "temp"): + if query is None and artist is None: + raise ValueError("no query provided") + + self.options_history = [] + self.current_options = None + self.current_chosen_option = None + + self.temp = temp + + # initial search + if query is not None: + self.set_options( + options.Options([musicbrainzngs.search_artists(query), musicbrainzngs.search_releases(query), + musicbrainzngs.search_recordings(query)])) + elif artist is not None: + self.set_options(options.Options([musicbrainzngs.search_artists(artist=artist)])) + + def browse_artist(self, artist: dict, limit: int = 25): + options_sets = [ + {"artist-list": [artist, ], "artist-count": 1}, + musicbrainzngs.browse_releases(artist=artist["id"], limit=limit), + musicbrainzngs.browse_recordings(artist=artist["id"], limit=limit) + ] + return self.set_options(options.Options(options_sets)) + + def browse_release(self, release: dict, limit: int = 25): + options_sets = [ + musicbrainzngs.browse_artists(release=release["id"], limit=limit), + {"release-list": [release, ], "release-count": 1}, + musicbrainzngs.browse_recordings(release=release["id"], limit=limit) + ] + return self.set_options(options.Options(options_sets)) + + def browse_track(self, track: dict, limit: int = 25): + options_sets = [ + musicbrainzngs.browse_artists(recording=track["id"], limit=limit), + musicbrainzngs.browse_releases(recording=track["id"], limit=limit), + {"recording-list": [track, ], "recording-count": 1} + ] + return self.set_options(options.Options(options_sets)) + + def choose(self, index, limit: int = 25, ignore_limit_for_tracklist: bool = True): + if not self.current_options.choose(index): + return self.current_options + + self.current_chosen_option = self.current_options.get_current_option() + kind = self.current_chosen_option['kind'] + if kind == 'artist': + return self.browse_artist(self.current_chosen_option, limit=limit) + if kind == 'release': + release_limit = limit if not ignore_limit_for_tracklist else 100 + return self.browse_release(self.current_chosen_option, limit=release_limit) + if kind == 'track': + track_limit = limit if not ignore_limit_for_tracklist else 100 + return self.browse_track(self.current_chosen_option, limit=track_limit) + + return self.current_options + + def get_options(self): + return self.current_options + + def set_options(self, option_instance): + self.options_history.append(option_instance) + self.current_options = option_instance + + return option_instance + + def get_previous_options(self): + self.options_history.pop(-1) + self.current_options = self.options_history[-1] + return self.current_options + + options = property(fget=get_options) + + +def automated_demo(): + search = Search(query="psychonaut 4") + print(search.options) + print(search.choose(0)) + search.download() + print(search.choose(2)) + search.download() + print(search.choose(4)) + print(search.download()) + + +def interactive_demo(): + search = Search(query=input("initial query: ")) + print(search.options) + while True: + input_ = input( + "d to download, q to quit, .. for previous options, . for current options, int for this element: ").lower() + input_.strip() + if input_ == "q": + break + if input_ == ".": + print(search.options) + continue + if input_ == "..": + print(search.get_previous_options()) + continue + if input_.isdigit(): + print(search.choose(int(input_))) + continue + if input_ == "d": + search.download() + break + + +if __name__ == "__main__": + # interactive_demo() + # automated_demo() + search = Search(query="psychonaut 4") + # search.download_release("27f00fb8-983c-4d5c-950f-51418aac55dc") + search.download_release("1aeb676f-e556-4b17-b45e-64ab69ef0375") + # for track_ in search.download_artist("c0c720b5-012f-4204-a472-981403f37b12"): + # print(track_) + # res = search.download_track("83a30323-aee1-401a-b767-b3c1bdd026c0") + # res = search.download_track("5e1ee2c5-502c-44d3-b1bc-22803441d8c6") + res = search.download_track("86b43bec-eea6-40ae-8624-c1e404204ba1") + # res = search.download_track("5cc28584-10c6-40e2-b6d4-6891e7e7c575") + + for key in res[0]: + if res[0][key] is None: + continue + + print(key, res[0][key]) diff --git a/src/metadata/object_handeling.py b/src/metadata/object_handeling.py new file mode 100644 index 0000000..3830c8e --- /dev/null +++ b/src/metadata/object_handeling.py @@ -0,0 +1,22 @@ +from datetime import date + +def get_elem_from_obj(current_object, keys: list, after_process=lambda x: x, return_if_none=None): + current_object = current_object + for key in keys: + if key in current_object or (type(key) == int and key < len(current_object)): + current_object = current_object[key] + else: + return return_if_none + return after_process(current_object) + +def parse_music_brainz_date(mb_date: str) -> date: + year = 1 + month = 1 + day = 1 + + first_release_date = mb_date + if first_release_date.count("-") == 2: + year, month, day = [int(i) for i in first_release_date.split("-")] + elif first_release_date.count("-") == 0: + year = int(first_release_date) + return date(year, month, day) diff --git a/src/metadata/options.py b/src/metadata/options.py new file mode 100644 index 0000000..07ae634 --- /dev/null +++ b/src/metadata/options.py @@ -0,0 +1,118 @@ +def get_string_for_artist(artist: dict) -> str: + string = f"'{artist['name']}'" + if "country" in artist: + string += f" from {artist['country']}" + if 'disambiguation' in artist: + string += f", '{artist['disambiguation']}'" + return string + "\n" + + +def get_string_for_release(release: dict) -> str: + string = "" + if "type" in release: + string += f"the {release['type']} titled " + string += f"'{release['title']}'" + if "artist-credit-phrase" in release: + string += f" by: {release['artist-credit-phrase']}" + + return string + "\n" + + +def get_string_for_tracks(tracks: dict) -> str: + # I know it's not the best practice but whatever + return get_string_for_release(tracks) + + +def get_string_for_option(option: dict) -> str: + kind = option['kind'] + if kind == "artist": + return get_string_for_artist(option) + if kind == "release": + return get_string_for_release(option) + if kind == "track": + return get_string_for_tracks(option) + return "Error\n" + + +class Options: + def __init__(self, results: list): + self.results = results + + self.artist_count = 0 + self.release_count = 0 + self.track_count = 0 + self.result_list = [] + self.set_options_values() + + self.current_option_ind = None + + def get_current_option(self, komplex: bool = False): + if self.current_option_ind is None: + raise Exception("It must first be chosen, which option to get, before getting it") + + if komplex: + return self.result_list[self.current_option_ind] + + komplex_information = self.result_list[self.current_option_ind] + return { + 'id': komplex_information['id'], + 'type': komplex_information['kind'] + } + + def choose(self, index: int) -> bool: + if len(self.result_list) <= index - 1: + return False + self.current_option_ind = index + return True + + def __str__(self) -> str: + string = f"artists: {self.artist_count}; releases {self.release_count}; tracks {self.track_count}\n" + for i, option in enumerate(self.result_list): + string += f"{i})\t{option['kind']}:\t" + get_string_for_option(option) + return string + + def set_options_values(self): + for option_set in self.results: + if "artist-list" in option_set: + self.set_artist_values(option_set) + continue + if "release-list" in option_set: + self.set_release_values(option_set) + continue + if "recording-list" in option_set: + self.set_track_values(option_set) + continue + + def set_artist_values(self, option_set: dict): + self.artist_count += option_set['artist-count'] + for artist in option_set['artist-list']: + artist['kind'] = "artist" + self.result_list.append(artist) + + def set_release_values(self, option_set: dict): + self.release_count += option_set['release-count'] + for release in option_set['release-list']: + release['kind'] = "release" + self.result_list.append(release) + + def set_track_values(self, option_set: dict): + self.track_count += option_set['recording-count'] + for track in option_set['recording-list']: + track['kind'] = "track" + self.result_list.append(track) + +""" +example +{'artist-list': [{'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'type': 'Group', 'ext:score': '100', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'country': 'GE', 'area': {'id': '7e081aa0-817b-3ae0-9fe2-4bb4e3b3cc95', 'type': 'Country', 'name': 'Georgia', 'sort-name': 'Georgia', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': '76c77b6c-f1e1-4a58-8fe1-01a7efadd1f7', 'type': 'City', 'name': 'Tbilisi', 'sort-name': 'Tbilisi', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Georgian depressive black metal', 'life-span': {'begin': '2010', 'ended': 'false'}, 'tag-list': [{'count': '0', 'name': 'black metal'}, {'count': '5', 'name': 'depressive black metal'}, {'count': '1', 'name': 'sex'}, {'count': '1', 'name': 'suicide'}, {'count': '1', 'name': 'hopelessness'}, {'count': '1', 'name': 'drugs'}, {'count': '1', 'name': 'depression'}, {'count': '1', 'name': 'alcohol'}]}, {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'type': 'Group', 'ext:score': '82', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'country': 'BE', 'area': {'id': '5b8a5ee5-0bb3-34cf-9a75-c27c44e341fc', 'type': 'Country', 'name': 'Belgium', 'sort-name': 'Belgium', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': 'f05e07a9-0ea2-4b8e-9538-370456752089', 'type': 'City', 'name': 'Mechelen', 'sort-name': 'Mechelen', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Belgian sludge metal band', 'isni-list': ['0000000475468695'], 'life-span': {'begin': '2013', 'ended': 'false'}}, {'id': '35ed62b6-a358-41a5-8c09-80e7aaae1e1b', 'type': 'Group', 'ext:score': '81', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'country': 'DE', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'type': 'Country', 'name': 'Germany', 'sort-name': 'Germany', 'life-span': {'ended': 'false'}}, 'disambiguation': 'German trance group; Lothar Herrmann & Oliver Balser', 'life-span': {'ended': 'false'}}, {'id': 'ad37f13c-73b6-48c0-8606-2321059a41ce', 'type': 'Group', 'ext:score': '81', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'country': 'AU', 'area': {'id': '106e0bec-b638-3b37-b731-f53d507dc00e', 'type': 'Country', 'name': 'Australia', 'sort-name': 'Australia', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': '21e8495a-32fa-4a97-8a92-57184e36235a', 'type': 'City', 'name': 'Perth', 'sort-name': 'Perth', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Australian thrash metal band', 'life-span': {'begin': '1997', 'ended': 'false'}}, {'id': '48abf0e3-14e6-4bf9-a14e-45bc7f327891', 'type': 'Group', 'ext:score': '81', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'country': 'CZ', 'area': {'id': '51d34c28-61bf-3d21-849f-7492672a9d44', 'type': 'Country', 'name': 'Czechia', 'sort-name': 'Czechia', 'life-span': {'begin': '1993-01-01', 'ended': 'false'}}, 'begin-area': {'id': 'a99b59f9-f506-4c1a-876c-43d8dda0ce0e', 'type': 'City', 'name': 'Opava', 'sort-name': 'Opava', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Czech funk rock band from Opava', 'life-span': {'begin': '1999', 'ended': 'false'}}, {'id': '6ece3c88-f052-4529-bacf-5e12290a331a', 'ext:score': '79', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'bass/dubstep artist', 'life-span': {'ended': 'false'}}, {'id': '113f46a8-8cc9-4c81-8681-dbc0d947bb5a', 'type': 'Group', 'ext:score': '79', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'country': 'US', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'type': 'Country', 'name': 'United States', 'sort-name': 'United States', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': '4fb3cf81-d45c-4607-aa90-53856d69f5ff', 'type': 'City', 'name': 'Harrisonburg', 'sort-name': 'Harrisonburg', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Garage psych band from Virginia', 'life-span': {'begin': '2016', 'ended': 'false'}}, {'id': '7c976521-a20f-4107-b3e0-766ebae07a56', 'type': 'Person', 'ext:score': '79', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Alias of Spanish trance DJ Javi Golo', 'life-span': {'ended': 'false'}}, {'id': 'e2e692b5-efbc-4cd7-ab38-bce0bd457a8b', 'type': 'Group', 'ext:score': '74', 'name': 'Psychonaut 75', 'sort-name': 'Psychonaut 75', 'country': 'US', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'type': 'Country', 'name': 'United States', 'sort-name': 'United States', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': 'c920948b-83e3-40b7-8fe9-9ab5abaac55b', 'type': 'City', 'name': 'Houston', 'sort-name': 'Houston', 'life-span': {'ended': 'false'}}, 'disambiguation': 'dark industrial dance', 'life-span': {'begin': '1997', 'ended': 'false'}, 'alias-list': [{'sort-name': 'Psychonaut', 'type': 'Artist name', 'begin-date': '1997', 'end-date': '2001', 'alias': 'Psychonaut'}, {'sort-name': 'Psychonaut 75', 'type': 'Artist name', 'begin-date': '2001', 'alias': 'Psychonaut 75'}], 'tag-list': [{'count': '1', 'name': 'industrial'}, {'count': '1', 'name': 'ritual ambient'}]}, {'id': 'fa6521a7-56b5-4e56-b946-fda469becba9', 'type': 'Group', 'ext:score': '69', 'name': '4 Strings', 'sort-name': '4 Strings', 'country': 'NL', 'area': {'id': 'ef1b7cc0-cd26-36f4-8ea0-04d9623786c7', 'type': 'Country', 'name': 'Netherlands', 'sort-name': 'Netherlands', 'life-span': {'ended': 'false'}}, 'disambiguation': 'Netherlands dance duo', 'life-span': {'ended': 'false'}, 'alias-list': [{'sort-name': '4Strings', 'alias': '4Strings'}], 'tag-list': [{'count': '1', 'name': 'dance pop'}, {'count': '0', 'name': 'dutch trance'}, {'count': '1', 'name': 'trance pop'}, {'count': '2', 'name': 'vocal trance'}, {'count': '3', 'name': 'trance'}, {'count': '1', 'name': 'uplifting'}, {'count': '1', 'name': 'uplifting trance'}, {'count': '1', 'name': 'female vocal trance'}, {'count': '1', 'name': 'dutch'}, {'count': '1', 'name': 'pop trance'}, {'count': '1', 'name': 'dance trance'}, {'count': '1', 'name': 'female vocal dance'}, {'count': '1', 'name': 'vocal dance'}, {'count': '2', 'name': 'dance-pop'}]}, {'id': 'bdbd99ab-3f4b-4028-9306-cd71d8695fbc', 'type': 'Group', 'ext:score': '66', 'name': 'Anonymous 4', 'sort-name': 'Anonymous 4', 'country': 'US', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'type': 'Country', 'name': 'United States', 'sort-name': 'United States', 'life-span': {'ended': 'false'}}, 'disambiguation': 'vocal quartet specialising in medieval chant and polyphony', 'isni-list': ['0000000123640949'], 'life-span': {'begin': '1986', 'ended': 'false'}, 'alias-list': [{'sort-name': 'An English Ladymass', 'alias': 'An English Ladymass'}, {'sort-name': 'A Star in the East', 'alias': 'A Star in the East'}, {'sort-name': 'On Yoolis Night', 'alias': 'On Yoolis Night'}, {'sort-name': 'Eleven Thousand Virgins', 'alias': 'Eleven Thousand Virgins'}, {'sort-name': "Miracles of Saint'Iago", 'alias': "Miracles of Saint'Iago"}, {'sort-name': "Love's Illusion", 'alias': "Love's Illusion"}, {'sort-name': 'The Lily & The Lamb', 'alias': 'The Lily & The Lamb'}], 'tag-list': [{'count': '1', 'name': 'early music'}, {'count': '1', 'name': 'medieval'}, {'count': '2', 'name': 'a cappella'}]}, {'id': 'c869ad3e-6e8f-46e3-89e0-dc9d28d6bc61', 'type': 'Person', 'ext:score': '64', 'name': '4*', 'sort-name': '4*', 'gender': 'male', 'area': {'id': '30bcaa92-9870-4798-be1a-4e0036755316', 'type': 'City', 'name': 'Osaka', 'sort-name': 'Osaka', 'life-span': {'ended': 'false'}}, 'life-span': {'ended': 'false'}}, {'id': '3d1dc3db-4fc3-42b2-a8cd-4d8983637450', 'type': 'Group', 'ext:score': '63', 'name': 'Radio 4', 'sort-name': 'Radio 4', 'country': 'US', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'type': 'Country', 'name': 'United States', 'sort-name': 'United States', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': 'a71b0d32-7752-49e9-8594-2247ad6ac12c', 'type': 'District', 'name': 'Brooklyn', 'sort-name': 'Brooklyn', 'life-span': {'ended': 'false'}}, 'isni-list': ['000000011882564X'], 'life-span': {'begin': '1999', 'ended': 'false'}, 'alias-list': [{'sort-name': 'Radio4', 'alias': 'Radio4'}], 'tag-list': [{'count': '1', 'name': 'indie rock'}, {'count': '1', 'name': 'rock and indie'}, {'count': '1', 'name': 'dance-punk'}, {'count': '1', 'name': 'american'}, {'count': '1', 'name': 'post-punk revival'}]}, {'id': '5fdbf7e7-8643-44ad-94a7-3d669e3876e4', 'type': 'Group', 'ext:score': '62', 'name': 'Twenty 4 Seven', 'sort-name': 'Twenty 4 Seven', 'country': 'NL', 'area': {'id': 'ef1b7cc0-cd26-36f4-8ea0-04d9623786c7', 'type': 'Country', 'name': 'Netherlands', 'sort-name': 'Netherlands', 'life-span': {'ended': 'false'}}, 'disambiguation': 'euro house', 'isni-list': ['0000000122900577'], 'life-span': {'begin': '1989', 'ended': 'false'}, 'alias-list': [{'sort-name': 'Twenty 4th Street', 'alias': 'Twenty 4th Street'}, {'sort-name': 'Twenty Four Seven', 'alias': 'Twenty Four Seven'}, {'sort-name': '24-7', 'alias': '24-7'}], 'tag-list': [{'count': '1', 'name': 'dance'}, {'count': '1', 'name': 'euro house'}, {'count': '1', 'name': 'house'}, {'count': '1', 'name': 'eurodance'}]}, {'id': '32ff0254-5c1f-425c-910a-ff62ff888ca2', 'type': 'Group', 'ext:score': '62', 'name': 'Lab 4', 'sort-name': 'Lab 4', 'country': 'GB', 'area': {'id': '8a754a16-0027-3a29-b6d7-2b40ea0481ed', 'type': 'Country', 'name': 'United Kingdom', 'sort-name': 'United Kingdom', 'life-span': {'ended': 'false'}}, 'life-span': {'begin': '1994', 'ended': 'false'}, 'alias-list': [{'sort-name': 'LAB4', 'alias': 'LAB4'}, {'sort-name': 'Lab-4', 'alias': 'Lab-4'}]}, {'id': '95711c34-6d6a-4acd-a713-e460a7915494', 'type': 'Group', 'ext:score': '62', 'name': '4', 'sort-name': '4', 'country': 'US', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'type': 'Country', 'name': 'United States', 'sort-name': 'United States', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': '398c6575-6e22-44f9-b93a-349c9f5c8889', 'type': 'City', 'name': 'Costa Mesa', 'sort-name': 'Costa Mesa', 'life-span': {'ended': 'false'}}, 'disambiguation': '1990s California alternative/punk band', 'life-span': {'ended': 'false'}}, {'id': 'a21768f9-0bb1-41f2-9860-90e95aaa0f34', 'type': 'Group', 'ext:score': '61', 'name': '4 Clubbers', 'sort-name': '4 Clubbers', 'country': 'DE', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'type': 'Country', 'name': 'Germany', 'sort-name': 'Germany', 'life-span': {'ended': 'false'}}, 'life-span': {'ended': 'false'}, 'alias-list': [{'sort-name': '4Clubbers', 'alias': '4Clubbers'}]}, {'id': '7e5134b6-add1-4cbe-accd-f01547a4b6c3', 'ext:score': '61', 'name': '4$', 'sort-name': '4$', 'disambiguation': 'trap', 'life-span': {'ended': 'false'}}, {'id': 'c0e9bf2e-9c31-4ff1-8e23-0021652a9015', 'type': 'Group', 'ext:score': '61', 'name': '4 Позиции Бруно', 'sort-name': '4 Positions of Bruno', 'country': 'RU', 'area': {'id': '1f1fc3a4-9500-39b8-9f10-f0a465557eef', 'type': 'Country', 'name': 'Russia', 'sort-name': 'Russia', 'life-span': {'ended': 'false'}}, 'begin-area': {'id': '0918dfa2-f667-4dbe-bac9-644d81a06612', 'type': 'City', 'name': 'Yekaterinburg', 'sort-name': 'Yekaterinburg', 'life-span': {'ended': 'false'}}, 'life-span': {'begin': '2002', 'ended': 'false'}, 'alias-list': [{'sort-name': '4 pozitsii Bruno', 'alias': '4 pozitsii Bruno'}, {'locale': 'en', 'sort-name': 'Bruno’s 4 Positions', 'alias': 'Bruno’s 4 Positions'}], 'tag-list': [{'count': '3', 'name': 'electronic'}, {'count': '3', 'name': 'ambient'}, {'count': '3', 'name': 'experimental'}]}, {'id': 'df659499-80f2-4301-bb46-0eade0213f5b', 'ext:score': '61', 'name': ':4:', 'sort-name': 'channel4four', 'disambiguation': 'experimental music', 'life-span': {'ended': 'false'}, 'alias-list': [{'sort-name': 'channel4four', 'alias': 'channel4four'}]}, {'id': 'a5891a6b-d0a9-4264-b47d-bcf526bbb826', 'ext:score': '61', 'name': 'Eponymous 4', 'sort-name': 'Eponymous 4', 'area': {'id': '10adc6b5-63bf-4b4e-993e-ed83b05c22fc', 'type': 'City', 'name': 'Seattle', 'sort-name': 'Seattle', 'life-span': {'ended': 'false'}}, 'life-span': {'begin': '1999', 'ended': 'false'}}, {'id': '318ec65f-9046-4c41-a60f-664f5f997ba5', 'ext:score': '61', 'name': '⦅౪⦆', 'sort-name': '⦅౪⦆', 'life-span': {'ended': 'false'}, 'alias-list': [{'sort-name': 'drunkenhigh', 'alias': 'drunkenhigh'}], 'tag-list': [{'count': '1', 'name': 'sillyname'}, {'count': '1', 'name': '!hyperfocus'}]}, {'id': 'e3891ce8-1f22-4ee9-8b3d-5c8f1bb8dbd2', 'ext:score': '61', 'name': '4', 'sort-name': '4', 'disambiguation': '/v/', 'life-span': {'ended': 'false'}}, {'id': 'ceb3cccd-3c71-4acf-a2da-146abd021760', 'ext:score': '61', 'name': '4', 'sort-name': '4', 'disambiguation': 'rapper us ?', 'life-span': {'ended': 'false'}}, {'id': 'a5c07e9f-1995-4c81-9425-7fea1489c37d', 'type': 'Group', 'ext:score': '60', 'name': '4 Promille', 'sort-name': '4 Promille', 'country': 'DE', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'type': 'Country', 'name': 'Germany', 'sort-name': 'Germany', 'life-span': {'ended': 'false'}}, 'life-span': {'ended': 'false'}, 'alias-list': [{'sort-name': 'Promille', 'alias': 'Promille'}], 'tag-list': [{'count': '1', 'name': 'punk'}]}], 'artist-count': 941} +{'release-list': [{'id': 'd6e6be74-b7a4-4982-955a-d3361885541c', 'ext:score': '100', 'title': '40%', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': 'a557bf98-e902-4336-8b28-ecbede7f93bf', 'type': 'Album', 'title': '40%', 'primary-type': 'Album', 'secondary-type-list': ['Demo']}, 'date': '2011-06-04', 'country': 'UA', 'release-event-list': [{'date': '2011-06-04', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'label-info-list': [{'label': {'id': 'ce4c6532-a7a4-47e7-83c1-169e68e461df', 'name': 'Depressive Illusions Records'}}], 'medium-list': [{'format': 'Cassette', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 3}], 'medium-track-count': 3, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'ext:score': '100', 'title': 'Dipsomania', 'status': 'Official', 'packaging': 'Digipak', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': 'a37697eb-176b-4242-95fc-27de66342463', 'type': 'Album', 'title': 'Dipsomania', 'primary-type': 'Album'}, 'date': '2015-04-25', 'country': 'AT', 'release-event-list': [{'date': '2015-04-25', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'label-info-list': [{'catalog-number': 'TR025CD', 'label': {'id': 'f6824a99-ac3b-4cc4-95fe-8e47769b33a7', 'name': 'Talheim Records'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 12}], 'medium-track-count': 12, 'medium-count': 1, 'tag-list': [{'count': '1', 'name': 'metal'}, {'count': '0', 'name': 'dipsomania'}, {'count': '1', 'name': 'psychonaut 4'}, {'count': '0', 'name': 'psychonaut'}, {'count': '1', 'name': 'depressive'}, {'count': '1', 'name': 'black'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'ab61f4b0-88e8-43b5-9b24-af184c6f1f12', 'ext:score': '100', 'title': 'Free Portion ov Madness I', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': 'fe7f2480-fa00-483e-bad6-e6fc839ec3ea', 'type': 'Single', 'title': 'Free Portion ov Madness I', 'primary-type': 'Single'}, 'date': '2014-04-11', 'country': 'XW', 'release-event-list': [{'date': '2014-04-11', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'barcode': '', 'label-info-list': [{'label': {'id': '157afde4-4bf5-4039-8ad2-5a15acc85176', 'name': '[no label]'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 1}], 'medium-track-count': 1, 'medium-count': 1, 'tag-list': [{'count': '2', 'name': 'metal'}, {'count': '1', 'name': 'dsbm'}, {'count': '1', 'name': 'suicidal black metal'}, {'count': '1', 'name': 'depressive suicidal black metal'}, {'count': '2', 'name': 'post black metal'}, {'count': '1', 'name': 'depressive rock'}, {'count': '1', 'name': 'tbilisi'}, {'count': '1', 'name': 'post suicidal black metal'}, {'count': '2', 'name': 'depressive black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'ext:score': '100', 'title': 'Have a Nice Trip', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'label-info-list': [{'catalog-number': 'cut 668', 'label': {'id': 'ce4c6532-a7a4-47e7-83c1-169e68e461df', 'name': 'Depressive Illusions Records'}}], 'medium-list': [{'format': 'Cassette', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'ext:score': '100', 'title': 'Neurasthenia', 'status': 'Official', 'packaging': 'Digipak', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'label-info-list': [{'catalog-number': 'TR045CD', 'label': {'id': 'f6824a99-ac3b-4cc4-95fe-8e47769b33a7', 'name': 'Talheim Records'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'f8d4b24d-2c46-4e9c-8078-0c0f337c84dd', 'ext:score': '100', 'title': 'Beautyfall', 'status': 'Official', 'packaging': 'Digipak', 'text-representation': {'language': 'mul', 'script': 'Qaaa'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': 'b58eaf6e-1cb3-4d61-bf3b-54dd6971e0d0', 'type': 'Album', 'title': 'Beautyfall', 'primary-type': 'Album'}, 'date': '2020-10-31', 'country': 'GE', 'release-event-list': [{'date': '2020-10-31', 'area': {'id': '7e081aa0-817b-3ae0-9fe2-4bb4e3b3cc95', 'name': 'Georgia', 'sort-name': 'Georgia', 'iso-3166-1-code-list': ['GE']}}], 'label-info-list': [{'catalog-number': 'TR068CD', 'label': {'id': 'f6824a99-ac3b-4cc4-95fe-8e47769b33a7', 'name': 'Talheim Records'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 8}], 'medium-track-count': 8, 'medium-count': 1, 'tag-list': [{'count': '1', 'name': 'depressive black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'afb2e006-7e6f-4f37-b43e-f844bf3ea94b', 'ext:score': '100', 'title': 'Beautyfall / სულდაცემა', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': 'b58eaf6e-1cb3-4d61-bf3b-54dd6971e0d0', 'type': 'Album', 'title': 'Beautyfall', 'primary-type': 'Album'}, 'date': '2020-10-30', 'country': 'XW', 'release-event-list': [{'date': '2020-10-30', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 8}], 'medium-track-count': 8, 'medium-count': 1, 'tag-list': [{'count': '1', 'name': 'post suicidal black metal'}, {'count': '2', 'name': 'metal'}, {'count': '2', 'name': 'black metal'}, {'count': '1', 'name': 'tbilisi'}, {'count': '1', 'name': 'depressive suicidal black metal'}, {'count': '1', 'name': 'metal metal'}, {'count': '1', 'name': 'suicidal depressive black'}, {'count': '1', 'name': 'depressivesuicidalblackmetal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'da90dfc2-95cf-4c24-8549-84339d6d3076', 'ext:score': '100', 'title': 'Have a Nice Trip', 'status': 'Official', 'packaging': 'None', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2018-08-15', 'country': 'XW', 'release-event-list': [{'date': '2018-08-15', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'barcode': '4061707064036', 'asin': 'B07G2D5GZ9', 'label-info-list': [{'catalog-number': 'none', 'label': {'id': 'f6824a99-ac3b-4cc4-95fe-8e47769b33a7', 'name': 'Talheim Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1, 'tag-list': [{'count': '2', 'name': 'depressive black metal'}, {'count': '1', 'name': 'tbilisi'}, {'count': '1', 'name': 'depressive rock'}, {'count': '2', 'name': 'metal'}, {'count': '1', 'name': 'post suicidal black metal'}, {'count': '1', 'name': 'depressive/black-metal'}, {'count': '1', 'name': 'depressive suicidal black metal'}, {'count': '1', 'name': 'suicidal black metal'}, {'count': '2', 'name': 'post black metal'}, {'count': '1', 'name': 'dsbm'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '2b7e9b09-8651-490f-a638-0b99e157a12c', 'ext:score': '100', 'title': 'Have a Nice Trip', 'status': 'Official', 'packaging': 'Digipak', 'text-representation': {'language': 'mul', 'script': 'Qaaa'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2018-12-01', 'country': 'AT', 'release-event-list': [{'date': '2018-12-01', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'barcode': '', 'label-info-list': [{'catalog-number': 'TR050CD', 'label': {'id': 'f6824a99-ac3b-4cc4-95fe-8e47769b33a7', 'name': 'Talheim Records'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 1, 'track-list': [], 'track-count': 12}], 'medium-track-count': 12, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'ext:score': '100', 'title': 'Have a Nice Trip', 'status': 'Official', 'text-representation': {'language': 'mul', 'script': 'Qaaa'}, 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'label-info-list': [{'catalog-number': 'SAD 008', 'label': {'id': 'efb18b12-0baa-4592-ac33-14cdd684aaff', 'name': 'Solitude and Despair Music'}}, {'catalog-number': 'DNW025', 'label': {'id': 'ebc3b589-d7ff-4978-baa4-434588819f6d', 'name': 'Der Neue Weg'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '3fb91753-fb1a-47d5-b83f-5cf88a12b904', 'ext:score': '71', 'title': 'Liber Al Vel Legis', 'status': 'Official', 'packaging': 'Digipak', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': 'e2e692b5-efbc-4cd7-ab38-bce0bd457a8b', 'name': 'Psychonaut 75', 'sort-name': 'Psychonaut 75', 'disambiguation': 'dark industrial dance'}}], 'release-group': {'id': 'bd708a0f-26c5-3fd9-981f-b2149401c8d3', 'type': 'Album', 'title': 'Liber Al Vel Legis', 'primary-type': 'Album'}, 'date': '2001', 'country': 'FR', 'release-event-list': [{'date': '2001', 'area': {'id': '08310658-51eb-3801-80de-5a0739207115', 'name': 'France', 'sort-name': 'France', 'iso-3166-1-code-list': ['FR']}}], 'label-info-list': [{'catalog-number': 'ATNR 013', 'label': {'id': '820c7c21-2d29-4e2f-83cb-692cfd335fab', 'name': 'Athanor'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 16}], 'medium-track-count': 16, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '5c08a8fe-c644-494b-9a1f-a0943b0cd18b', 'ext:score': '71', 'title': 'Pylon of Daath', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': 'e2e692b5-efbc-4cd7-ab38-bce0bd457a8b', 'name': 'Psychonaut 75', 'sort-name': 'Psychonaut 75', 'disambiguation': 'dark industrial dance'}}], 'release-group': {'id': '7b1b38a1-e024-48d3-a824-b412e633bd4b', 'type': 'Album', 'title': 'Pylon of Daath', 'primary-type': 'Album'}, 'date': '1999', 'country': 'US', 'release-event-list': [{'date': '1999', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'name': 'United States', 'sort-name': 'United States', 'iso-3166-1-code-list': ['US']}}], 'label-info-list': [{'catalog-number': 'LBRF 008', 'label': {'id': '7ea66d0f-b37d-4010-879e-5fa4c3efe4ad', 'name': 'Live Bait Recording Foundation'}}], 'medium-list': [{'format': 'Cassette', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '037f04c6-5326-4df2-87fc-a2855e6c43a8', 'ext:score': '71', 'title': 'Antagonistic Pathways', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '35ed62b6-a358-41a5-8c09-80e7aaae1e1b', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'German trance group; Lothar Herrmann & Oliver Balser'}}], 'release-group': {'id': 'cb177b0b-1ea2-3f04-8515-846038dac043', 'type': 'Album', 'title': 'Antagonistic Pathways', 'primary-type': 'Album'}, 'date': '2002-07-23', 'country': 'DE', 'release-event-list': [{'date': '2002-07-23', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'barcode': '5413356178920', 'asin': 'B00006643W', 'label-info-list': [{'catalog-number': 'GTN 1105.20', 'label': {'id': 'e7e72105-7013-495a-b360-47dbe4c7e4e0', 'name': 'GTN'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 8}], 'medium-track-count': 8, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': 'b44b3d0d-6b9d-4ff8-8e80-274cdf3ead59', 'ext:score': '71', 'title': 'Volume 1', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '35ed62b6-a358-41a5-8c09-80e7aaae1e1b', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'German trance group; Lothar Herrmann & Oliver Balser'}}], 'release-group': {'id': 'fab107ed-fae5-3a96-abb2-04bb0c3bbb7b', 'type': 'Album', 'title': 'Volume 1', 'primary-type': 'Album'}, 'date': '2000-09', 'country': 'DE', 'release-event-list': [{'date': '2000-09', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'barcode': '5413356771121', 'label-info-list': [{'catalog-number': 'GTN 1035.20', 'label': {'id': 'e7e72105-7013-495a-b360-47dbe4c7e4e0', 'name': 'GTN'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 8}], 'medium-track-count': 8, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '98a85c0c-7315-4c0b-b6fb-4c4fcbaccf8c', 'ext:score': '71', 'title': 'Zos Vel Thanatos', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': 'e2e692b5-efbc-4cd7-ab38-bce0bd457a8b', 'name': 'Psychonaut 75', 'sort-name': 'Psychonaut 75', 'disambiguation': 'dark industrial dance'}}], 'release-group': {'id': '4f4be69c-cfb3-35f5-8388-b9e197dc4b2e', 'type': 'Album', 'title': 'Zos Vel Thanatos', 'primary-type': 'Album'}, 'date': '1999', 'country': 'US', 'release-event-list': [{'date': '1999', 'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', 'name': 'United States', 'sort-name': 'United States', 'iso-3166-1-code-list': ['US']}}], 'label-info-list': [{'catalog-number': 'AJNA02.1999', 'label': {'id': '8bffe0f3-8f22-4090-9e64-6d18d4ab7efc', 'name': 'The Ajna Offensive'}}], 'medium-list': [{'format': '7" Vinyl', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 2}], 'medium-track-count': 2, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': 'db637be8-0f8f-4235-b8fc-dd844341395e', 'ext:score': '71', 'title': "The Witches' Sabbath", 'status': 'Official', 'packaging': 'Jewel Case', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': 'e2e692b5-efbc-4cd7-ab38-bce0bd457a8b', 'name': 'Psychonaut 75', 'sort-name': 'Psychonaut 75', 'disambiguation': 'dark industrial dance'}}], 'release-group': {'id': '66a591cf-f7df-3000-b32c-56fe91a7e2f9', 'type': 'Album', 'title': "The Witches' Sabbath", 'primary-type': 'Album'}, 'date': '2000', 'country': 'FR', 'release-event-list': [{'date': '2000', 'area': {'id': '08310658-51eb-3801-80de-5a0739207115', 'name': 'France', 'sort-name': 'France', 'iso-3166-1-code-list': ['FR']}}], 'barcode': '', 'label-info-list': [{'catalog-number': 'ATNR 007', 'label': {'id': '820c7c21-2d29-4e2f-83cb-692cfd335fab', 'name': 'Athanor'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '9c9824e3-6fb7-4e02-a92b-469d72f36281', 'ext:score': '71', 'title': 'Free-Rider', 'status': 'Official', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '35ed62b6-a358-41a5-8c09-80e7aaae1e1b', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'German trance group; Lothar Herrmann & Oliver Balser'}}], 'release-group': {'id': 'ace22e92-832d-4a5d-942d-c5ae6fbd5715', 'type': 'Album', 'title': 'Free-Rider', 'primary-type': 'Album'}, 'date': '2010-09-03', 'country': 'DE', 'release-event-list': [{'date': '2010-09-03', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'asin': 'B003GNIO7E', 'label-info-list': [{'label': {'id': 'd8a2de8d-1e2b-4ff7-9b78-61dfb26e545d', 'name': 'Crotus Records'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 9}], 'medium-track-count': 9, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': 'c4d68715-beec-4d3a-ab74-1f18682ee437', 'ext:score': '71', 'title': 'Interbeing', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': '59e54a49-9a2c-429f-9bbd-56f729018289', 'type': 'Single', 'title': 'Interbeing', 'primary-type': 'Single'}, 'date': '2022-06-28', 'country': 'XW', 'release-event-list': [{'date': '2022-06-28', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 1}], 'medium-track-count': 1, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '1b0bfd1c-8698-4177-9bb6-5395f4c2691b', 'ext:score': '71', 'title': '... Jsem tady', 'status': 'Official', 'text-representation': {'language': 'ces', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '48abf0e3-14e6-4bf9-a14e-45bc7f327891', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Czech funk rock band from Opava'}}], 'release-group': {'id': '9844eb39-01f0-4144-9dad-b1872b950a89', 'type': 'Album', 'title': '... Jsem tady', 'primary-type': 'Album'}, 'date': '2004', 'country': 'CZ', 'release-event-list': [{'date': '2004', 'area': {'id': '51d34c28-61bf-3d21-849f-7492672a9d44', 'name': 'Czechia', 'sort-name': 'Czechia', 'iso-3166-1-code-list': ['CZ']}}], 'barcode': '8594056290130', 'label-info-list': [{'catalog-number': 'XP 013', 'label': {'id': '1e2ea326-ffac-45f0-a8f3-2e2966d84016', 'name': 'X Production'}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 13}], 'medium-track-count': 13, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '159542de-8d9c-46dc-818b-d9c52c3ca90b', 'ext:score': '71', 'title': 'Live at Penthouse Studio', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': 'fc7913fe-6a17-4819-8efd-02b0b3613797', 'type': 'EP', 'title': 'Live at Penthouse Studio', 'primary-type': 'EP', 'secondary-type-list': ['Live']}, 'date': '2021-02-05', 'country': 'XW', 'release-event-list': [{'date': '2021-02-05', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 3}], 'medium-track-count': 3, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '3d9703c6-be9e-4043-8a5c-5ea0ea4af923', 'ext:score': '71', 'title': 'The Story of Your Enslavement', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': '287f1682-d297-4f6e-8248-0461d3ef06b0', 'type': 'Single', 'title': 'The Story of Your Enslavement', 'primary-type': 'Single'}, 'date': '2020-02-14', 'country': 'XW', 'release-event-list': [{'date': '2020-02-14', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 1}], 'medium-track-count': 1, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '7c206048-26d6-4296-b5dd-0bf02dc0c691', 'ext:score': '71', 'title': 'Kabuddah', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': 'ac846cfe-e7ff-4e7d-bbfd-a50331fd1396', 'type': 'Single', 'title': 'Kabuddah', 'primary-type': 'Single'}, 'date': '2020-01-31', 'country': 'XW', 'release-event-list': [{'date': '2020-01-31', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 1}], 'medium-track-count': 1, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '09cd3222-c863-4c55-a14e-dd7c02ec18be', 'ext:score': '71', 'title': 'Violate Consensus Reality', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': '9fd45de3-4497-43c4-88c5-8fbb26cfdfbd', 'type': 'Single', 'title': 'Violate Consensus Reality', 'primary-type': 'Single'}, 'date': '2022-09-08', 'country': 'XW', 'release-event-list': [{'date': '2022-09-08', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 2}], 'medium-track-count': 2, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': 'e5df1bf9-e573-4549-9791-aa5f87704f69', 'ext:score': '71', 'title': 'Violate Consensus Reality', 'status': 'Official', 'packaging': 'None', 'text-representation': {'language': 'eng', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '55e84562-e29b-4967-8d2b-a7fff673cd1e', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Belgian sludge metal band'}}], 'release-group': {'id': 'f1d9dcc3-6287-475b-8ddb-2cc3b8e0e348', 'type': 'Album', 'title': 'Violate Consensus Reality', 'primary-type': 'Album'}, 'date': '2022-10-28', 'country': 'XW', 'release-event-list': [{'date': '2022-10-28', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'label-info-list': [{'label': {'id': 'df682909-df86-4be1-b2e8-1bd12c06bd01', 'name': 'Pelagic Records'}}], 'medium-list': [{'format': 'Digital Media', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 8}], 'medium-track-count': 8, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}, {'id': '8ce4c671-50e6-4fb7-a1e7-e93344dce010', 'ext:score': '71', 'title': 'Zeměven', 'status': 'Official', 'text-representation': {'language': 'ces', 'script': 'Latn'}, 'artist-credit': [{'name': 'Psychonaut', 'artist': {'id': '48abf0e3-14e6-4bf9-a14e-45bc7f327891', 'name': 'Psychonaut', 'sort-name': 'Psychonaut', 'disambiguation': 'Czech funk rock band from Opava'}}], 'release-group': {'id': '29a0c11f-3700-4133-a15a-5da0d6b1b119', 'type': 'Album', 'title': 'Zeměven', 'primary-type': 'Album'}, 'date': '2001', 'country': 'CZ', 'release-event-list': [{'date': '2001', 'area': {'id': '51d34c28-61bf-3d21-849f-7492672a9d44', 'name': 'Czechia', 'sort-name': 'Czechia', 'iso-3166-1-code-list': ['CZ']}}], 'medium-list': [{'format': 'CD', 'disc-list': [], 'disc-count': 0, 'track-list': [], 'track-count': 9}], 'medium-track-count': 9, 'medium-count': 1, 'tag-list': [], 'artist-credit-phrase': 'Psychonaut'}], 'release-count': 1421} +{'recording-list': [{'id': '20011be9-e649-4a11-ab66-7a2d8bc20c91', 'ext:score': '100', 'title': 'Have a Nice Trip', 'length': '540000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '89d0bbd9-5e9c-41c2-80a7-a156e93a8b3a', 'number': '4', 'title': 'Have A Nice Trip', 'length': '540000', 'track_or_recording_length': '540000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'be904c08-c8e8-36cb-9ba1-9c479e40afba', 'number': 'A4', 'title': 'Have a Nice Trip', 'length': '540000', 'track_or_recording_length': '540000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'tag-list': [{'count': '1', 'name': 'rock'}, {'count': '1', 'name': 'metal'}, {'count': '2', 'name': 'black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'c5a7fef9-6391-44be-a5ab-73ac88ff7226', 'ext:score': '100', 'title': 'Overdose Was the Best Way to Die', 'length': '323000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'ad6ea3fd-6df4-43e0-85d0-f388692966b7', 'number': '5', 'title': 'Overdose Was The Best Way To Die', 'length': '323000', 'track_or_recording_length': '323000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': '689f7df6-81a7-3e2a-8c3f-90999bcf468e', 'number': 'A5', 'title': 'Overdose Was the Best Way to Die', 'length': '323000', 'track_or_recording_length': '323000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '13a505ec-97ab-4053-adf9-c54b5cb944ae', 'ext:score': '100', 'title': 'Drop by Drop', 'length': '411000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '3b58ca08-cf3c-430b-8f1d-fce2d7e5f9af', 'number': '8', 'title': 'Drop By Drop (Album Version)', 'length': '411000', 'track_or_recording_length': '411000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'ed936633-cf6e-323a-858b-8577dcc1f8e4', 'number': 'B3', 'title': 'Drop by Drop', 'length': '411000', 'track_or_recording_length': '411000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '71c9beb2-7cf2-416a-a69e-ce6aa05aa327', 'ext:score': '100', 'title': 'Intro', 'length': '139000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '84f768d5-a6af-429f-8384-b1f998eda172', 'number': '6', 'title': 'Intro (Part II)', 'length': '139000', 'track_or_recording_length': '139000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'b2e1c794-eb71-3b7c-aa9c-73bd31fa28df', 'number': 'B1', 'title': 'Intro', 'length': '139000', 'track_or_recording_length': '139000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'b9bcdb8b-4bd4-4252-8f30-8b9a746474e0', 'ext:score': '100', 'title': 'Pseudo', 'length': '279000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'f7e18fd1-ac16-43b4-ab30-586f0775ec16', 'number': '3', 'title': 'Pseudo', 'length': '345000', 'track_or_recording_length': '345000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': '27b4f585-1f3a-3a3e-a6fc-3785e9f98f09', 'number': 'B2', 'title': 'Pseudo', 'length': '279000', 'track_or_recording_length': '279000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'b55c4962-f80f-4bd9-9103-4fd319a7b6f6', 'ext:score': '100', 'title': 'Lethargic Dialogue', 'length': '279000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '711cda4c-cd20-4974-893d-6a3b5a0a4ae2', 'number': '7', 'title': 'Lethargic Dialogue', 'length': '279000', 'track_or_recording_length': '279000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'ceb575a9-fc84-379c-844f-b32413d1dd9f', 'number': 'A3', 'title': 'Lethargic Dialogue', 'length': '345000', 'track_or_recording_length': '345000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '917481b6-8549-4127-a319-4b8c2198e863', 'ext:score': '100', 'title': 'Hate Parade', 'length': '285000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'c8e6b62c-c350-4cbe-90e2-2a8db4d636aa', 'number': '9', 'title': 'Hate Parade', 'length': '285000', 'track_or_recording_length': '285000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': '03faf038-50d1-363a-b0dd-e0b9a2f0133a', 'number': 'B4', 'title': 'Hate Parade', 'length': '285000', 'track_or_recording_length': '285000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'f301ed85-0a36-4d41-b60a-bb31b59ab4e4', 'ext:score': '100', 'title': 'ყლე', 'length': '279000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'bef00ad1-df23-4626-90d5-940f1e8b15e7', 'number': '11', 'title': 'ყლე', 'length': '279000', 'track_or_recording_length': '279000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'f7262517-fe52-365b-9770-7f167bb111a8', 'number': 'B6', 'title': 'ყლე', 'length': '279000', 'track_or_recording_length': '279000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '1cdce395-3618-471b-a7ce-cf05b5e23261', 'ext:score': '100', 'title': 'Parasite', 'length': '458000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'e7a0d5cd-ba0b-41f0-b723-a12a2a47a250', 'number': '2', 'title': 'Parasite', 'length': '458000', 'track_or_recording_length': '458000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': '0b711166-1075-3c69-9c14-a28ca5d0e4e0', 'number': 'A2', 'title': 'Parasite', 'length': '458000', 'track_or_recording_length': '458000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'tag-list': [{'count': '1', 'name': 'rock'}, {'count': '1', 'name': 'metal'}, {'count': '2', 'name': 'black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '4c10e8fb-4e22-4ed1-8a06-8e00c0e104bd', 'ext:score': '100', 'title': 'Intro', 'length': '211000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'faa570ec-44de-3ee3-a007-1231230197bb', 'number': 'A1', 'title': 'Intro', 'length': '211000', 'track_or_recording_length': '211000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}], 'tag-list': [{'count': '1', 'name': 'rock'}, {'count': '1', 'name': 'metal'}, {'count': '2', 'name': 'black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'dd53f08d-27e0-4ce7-aae2-b79e8a53526f', 'ext:score': '100', 'title': 'Serial Lier', 'length': '380000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'd6e6be74-b7a4-4982-955a-d3361885541c', 'title': '40%', 'status': 'Official', 'release-group': {'id': 'a557bf98-e902-4336-8b28-ecbede7f93bf', 'type': 'Album', 'title': '40%', 'primary-type': 'Album', 'secondary-type-list': ['Demo']}, 'date': '2011-06-04', 'country': 'UA', 'release-event-list': [{'date': '2011-06-04', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': '4ea2d709-a6b9-3704-b1c1-1b200997d1a7', 'number': '1', 'title': 'Serial Lier', 'length': '380000', 'track_or_recording_length': '380000'}], 'track-count': 3}], 'medium-track-count': 3, 'medium-count': 1}], 'tag-list': [{'count': '1', 'name': 'rock'}, {'count': '1', 'name': 'metal'}, {'count': '2', 'name': 'black metal'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '97aaef50-a3bf-40e2-b38d-ea47b40611af', 'ext:score': '100', 'title': 'Parasite', 'length': '425000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'd6e6be74-b7a4-4982-955a-d3361885541c', 'title': '40%', 'status': 'Official', 'release-group': {'id': 'a557bf98-e902-4336-8b28-ecbede7f93bf', 'type': 'Album', 'title': '40%', 'primary-type': 'Album', 'secondary-type-list': ['Demo']}, 'date': '2011-06-04', 'country': 'UA', 'release-event-list': [{'date': '2011-06-04', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'c24b8241-8611-3259-b976-3118a5465e42', 'number': '2', 'title': 'Parasite', 'length': '425000', 'track_or_recording_length': '425000'}], 'track-count': 3}], 'medium-track-count': 3, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'd44a8347-ba77-4a1b-9d61-d55f1c60c52f', 'ext:score': '100', 'title': 'Antihuman [Drug = Друг]', 'length': '548000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'e37ff962-2c1e-4af2-887d-89d2d88c4bea', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-11', 'country': 'DE', 'release-event-list': [{'date': '2012-11', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'bd8d9250-749e-4968-ab24-8f5c74ebc58a', 'number': '10', 'title': 'Antihuman [Drug=Друг]', 'length': '548000', 'track_or_recording_length': '548000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'a3015b31-3b62-4ffa-9184-9fb7bfd39660', 'title': 'Have a Nice Trip', 'status': 'Official', 'release-group': {'id': '9b1453e9-5b74-43c3-8e4f-0f4c78e46aff', 'type': 'Album', 'title': 'Have a Nice Trip', 'primary-type': 'Album'}, 'date': '2012-08-11', 'country': 'UA', 'release-event-list': [{'date': '2012-08-11', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'be7a59ea-5000-3cad-ae91-4e2f540a33e8', 'number': 'B5', 'title': 'Antihuman [Drug=Друг]', 'length': '548000', 'track_or_recording_length': '548000'}], 'track-count': 11}], 'medium-track-count': 11, 'medium-count': 1}, {'id': 'd6e6be74-b7a4-4982-955a-d3361885541c', 'title': '40%', 'status': 'Official', 'release-group': {'id': 'a557bf98-e902-4336-8b28-ecbede7f93bf', 'type': 'Album', 'title': '40%', 'primary-type': 'Album', 'secondary-type-list': ['Demo']}, 'date': '2011-06-04', 'country': 'UA', 'release-event-list': [{'date': '2011-06-04', 'area': {'id': '904768d0-61ca-3c40-93ac-93adc36fef4b', 'name': 'Ukraine', 'sort-name': 'Ukraine', 'iso-3166-1-code-list': ['UA']}}], 'medium-list': [{'position': '1', 'format': 'Cassette', 'track-list': [{'id': 'e3931ca8-1281-3b38-b4cb-6f2f0503a87b', 'number': '3', 'title': 'Antihuman [Drug = Друг]', 'length': '548000', 'track_or_recording_length': '548000'}], 'track-count': 3}], 'medium-track-count': 3, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'e183ef11-de34-4fa4-9381-44a7117e8611', 'ext:score': '100', 'title': 'Nackskott', 'length': '175000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '27f00fb8-983c-4d5c-950f-51418aac55dc', 'title': 'Tired, Numb, Still Alive', 'status': 'Official', 'artist-credit': [{'name': 'Unjoy', 'artist': {'id': '52685d5c-f5a9-40f9-8664-dc083ff242bd', 'name': 'Unjoy', 'sort-name': 'Unjoy', 'disambiguation': 'Black Metal band'}}, ' / ', {'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}, ' / ', {'name': 'Eurythmie', 'artist': {'id': 'c148d680-4739-429a-8550-53c098ac0b16', 'name': 'Eurythmie', 'sort-name': 'Eurythmie'}}], 'release-group': {'id': '6f46e1a4-0d33-48f5-b809-b131c369c97e', 'type': 'Album', 'title': 'Tired, Numb, Still Alive', 'primary-type': 'Album'}, 'date': '2012-06-15', 'country': 'DE', 'release-event-list': [{'date': '2012-06-15', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'f788c549-ece2-370b-95de-b6219347da45', 'number': '6', 'title': 'Nackskott', 'length': '175000', 'track_or_recording_length': '175000'}], 'track-count': 6}], 'medium-track-count': 6, 'medium-count': 1, 'artist-credit-phrase': 'Unjoy / Psychonaut 4 / Eurythmie'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '83a30323-aee1-401a-b767-b3c1bdd026c0', 'ext:score': '100', 'title': 'Drop by Drop', 'length': '414000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '27f00fb8-983c-4d5c-950f-51418aac55dc', 'title': 'Tired, Numb, Still Alive', 'status': 'Official', 'artist-credit': [{'name': 'Unjoy', 'artist': {'id': '52685d5c-f5a9-40f9-8664-dc083ff242bd', 'name': 'Unjoy', 'sort-name': 'Unjoy', 'disambiguation': 'Black Metal band'}}, ' / ', {'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}, ' / ', {'name': 'Eurythmie', 'artist': {'id': 'c148d680-4739-429a-8550-53c098ac0b16', 'name': 'Eurythmie', 'sort-name': 'Eurythmie'}}], 'release-group': {'id': '6f46e1a4-0d33-48f5-b809-b131c369c97e', 'type': 'Album', 'title': 'Tired, Numb, Still Alive', 'primary-type': 'Album'}, 'date': '2012-06-15', 'country': 'DE', 'release-event-list': [{'date': '2012-06-15', 'area': {'id': '85752fda-13c4-31a3-bee5-0e5cb1f51dad', 'name': 'Germany', 'sort-name': 'Germany', 'iso-3166-1-code-list': ['DE']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'f8efd599-3df2-342c-9521-b531ca74c89d', 'number': '5', 'title': 'Drop by Drop', 'length': '414000', 'track_or_recording_length': '414000'}], 'track-count': 6}], 'medium-track-count': 6, 'medium-count': 1, 'artist-credit-phrase': 'Unjoy / Psychonaut 4 / Eurythmie'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '975a7b3d-d66d-431b-8a76-4aba81ddad83', 'ext:score': '100', 'title': 'I Measure Time in Mililiters', 'length': '377920', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '66b70765-0c9f-41ff-9190-4830a3bd59e4', 'title': 'Urban Negativism', 'status': 'Official', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}, ' / ', {'name': 'In Luna', 'artist': {'id': 'fe7d5a6d-ad23-4444-aad5-14b7bf64304c', 'name': 'In Luna', 'sort-name': 'In Luna'}}, ' / ', {'name': 'Ofdrykkja', 'artist': {'id': '553462b6-fbb5-4bee-8536-d524ae3b5d13', 'name': 'Ofdrykkja', 'sort-name': 'Ofdrykkja'}}, ' / ', {'name': 'Vanhelga', 'artist': {'id': '20365b56-cd76-4f49-8e1d-87251724bbd8', 'name': 'Vanhelga', 'sort-name': 'Vanhelga'}}], 'release-group': {'id': '3378a8fd-8d9a-4bdf-85f1-10d630213053', 'type': 'Album', 'title': 'Urban Negativism', 'primary-type': 'Album'}, 'date': '2015-04-06', 'country': 'XW', 'release-event-list': [{'date': '2015-04-06', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '7debff25-a430-458b-9aea-f139d276b4c6', 'number': '7', 'title': 'I Measure Time in Mililiters', 'length': '377920', 'track_or_recording_length': '377920'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1, 'artist-credit-phrase': 'Psychonaut 4 / In Luna / Ofdrykkja / Vanhelga'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'cb0ad965-c7d6-4510-994c-1351134e9879', 'ext:score': '100', 'title': 'In a Good Movie Hero Always Dies', 'length': '199453', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '66b70765-0c9f-41ff-9190-4830a3bd59e4', 'title': 'Urban Negativism', 'status': 'Official', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}, ' / ', {'name': 'In Luna', 'artist': {'id': 'fe7d5a6d-ad23-4444-aad5-14b7bf64304c', 'name': 'In Luna', 'sort-name': 'In Luna'}}, ' / ', {'name': 'Ofdrykkja', 'artist': {'id': '553462b6-fbb5-4bee-8536-d524ae3b5d13', 'name': 'Ofdrykkja', 'sort-name': 'Ofdrykkja'}}, ' / ', {'name': 'Vanhelga', 'artist': {'id': '20365b56-cd76-4f49-8e1d-87251724bbd8', 'name': 'Vanhelga', 'sort-name': 'Vanhelga'}}], 'release-group': {'id': '3378a8fd-8d9a-4bdf-85f1-10d630213053', 'type': 'Album', 'title': 'Urban Negativism', 'primary-type': 'Album'}, 'date': '2015-04-06', 'country': 'XW', 'release-event-list': [{'date': '2015-04-06', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '15d8d514-9a95-44a5-a3a3-fc62ccc67d48', 'number': '8', 'title': 'In a Good Movie Hero Always Dies', 'length': '199453', 'track_or_recording_length': '199453'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1, 'artist-credit-phrase': 'Psychonaut 4 / In Luna / Ofdrykkja / Vanhelga'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '0c8cbbc4-84be-4f5a-8a56-be1777029960', 'ext:score': '100', 'title': 'Not a Love Song', 'length': '353613', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '66b70765-0c9f-41ff-9190-4830a3bd59e4', 'title': 'Urban Negativism', 'status': 'Official', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}, ' / ', {'name': 'In Luna', 'artist': {'id': 'fe7d5a6d-ad23-4444-aad5-14b7bf64304c', 'name': 'In Luna', 'sort-name': 'In Luna'}}, ' / ', {'name': 'Ofdrykkja', 'artist': {'id': '553462b6-fbb5-4bee-8536-d524ae3b5d13', 'name': 'Ofdrykkja', 'sort-name': 'Ofdrykkja'}}, ' / ', {'name': 'Vanhelga', 'artist': {'id': '20365b56-cd76-4f49-8e1d-87251724bbd8', 'name': 'Vanhelga', 'sort-name': 'Vanhelga'}}], 'release-group': {'id': '3378a8fd-8d9a-4bdf-85f1-10d630213053', 'type': 'Album', 'title': 'Urban Negativism', 'primary-type': 'Album'}, 'date': '2015-04-06', 'country': 'XW', 'release-event-list': [{'date': '2015-04-06', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'ed49885b-037d-45a7-b642-8dc11a194c89', 'number': '6', 'title': 'Not a Love Song', 'length': '353613', 'track_or_recording_length': '353613'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1, 'artist-credit-phrase': 'Psychonaut 4 / In Luna / Ofdrykkja / Vanhelga'}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'f6424595-9d4d-4a44-908b-99bdea5fa7e7', 'ext:score': '100', 'title': 'Prologue', 'length': '450000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '015ba6d6-cb44-41a6-931f-3e40f68a0f2c', 'number': '1', 'title': 'Prologue', 'length': '450000', 'track_or_recording_length': '450000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': 'a154d647-80ea-4372-8590-b307ed10999c', 'ext:score': '100', 'title': 'Death Is a Form of Art', 'length': '498000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '2e558d0a-ce38-4b78-8902-497a1d1e8a91', 'number': '2', 'title': 'Death Is a Form of Art', 'length': '498000', 'track_or_recording_length': '498000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '19baec10-dd65-4826-bc2b-daaea6004cb7', 'ext:score': '100', 'title': 'Bad t.RIP', 'length': '519000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '44106bbc-dbed-485d-9c91-2ee100680109', 'number': '5', 'title': 'Bad t.RIP', 'length': '519000', 'track_or_recording_length': '519000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '5ff80fa9-8e63-42e7-9a3d-49befc5101c9', 'ext:score': '100', 'title': 'Song Written in Paris', 'length': '353000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': '883577c7-eac4-4274-ab31-3d55adf910da', 'number': '6', 'title': 'Song Written in Paris', 'length': '353000', 'track_or_recording_length': '353000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '0f8ddb6b-5e2f-4817-83bf-069edd6c7daf', 'ext:score': '100', 'title': 'Thoughts of Death', 'length': '457000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'e6737a96-7646-4e1d-93d0-57df708a99d1', 'number': '10', 'title': 'Thoughts of Death', 'length': '457000', 'track_or_recording_length': '457000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '7bdd08b3-efd7-4421-89bf-d499b36c230d', 'ext:score': '100', 'title': 'I Wanna Be Your Dog', 'length': '308000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': 'ab61f4b0-88e8-43b5-9b24-af184c6f1f12', 'title': 'Free Portion ov Madness I', 'status': 'Official', 'release-group': {'id': 'fe7f2480-fa00-483e-bad6-e6fc839ec3ea', 'type': 'Single', 'title': 'Free Portion ov Madness I', 'primary-type': 'Single'}, 'date': '2014-04-11', 'country': 'XW', 'release-event-list': [{'date': '2014-04-11', 'area': {'id': '525d4e18-3d00-31b9-a58b-a146a916de8f', 'name': '[Worldwide]', 'sort-name': '[Worldwide]', 'iso-3166-1-code-list': ['XW']}}], 'medium-list': [{'position': '1', 'format': 'Digital Media', 'track-list': [{'id': '2cbaf960-3e0d-4ccf-933d-7a790f051dec', 'number': '1', 'title': 'I Wanna Be Your Dog', 'length': '308000', 'track_or_recording_length': '308000'}], 'track-count': 1}], 'medium-track-count': 1, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}, {'id': '6b40186b-6678-4328-a4b8-eb7c9806a9fb', 'ext:score': '100', 'title': 'Sweet Decadance', 'length': '718000', 'artist-credit': [{'name': 'Psychonaut 4', 'artist': {'id': 'c0c720b5-012f-4204-a472-981403f37b12', 'name': 'Psychonaut 4', 'sort-name': 'Psychonaut 4', 'disambiguation': 'Georgian depressive black metal'}}], 'release-list': [{'id': '0d229a02-74f6-4c77-8c20-6612295870ae', 'title': 'Neurasthenia', 'status': 'Official', 'release-group': {'id': '855c95c7-b7c2-40b2-b453-e3d37f43401e', 'type': 'Album', 'title': 'Neurasthenia', 'primary-type': 'Album'}, 'date': '2016-10-07', 'country': 'AT', 'release-event-list': [{'date': '2016-10-07', 'area': {'id': 'caac77d1-a5c8-3e6e-8e27-90b44dcc1446', 'name': 'Austria', 'sort-name': 'Austria', 'iso-3166-1-code-list': ['AT']}}], 'medium-list': [{'position': '1', 'format': 'CD', 'track-list': [{'id': 'b01fa728-514b-4730-879c-4ffc1df2554a', 'number': '3', 'title': 'Sweet Decadance', 'length': '718000', 'track_or_recording_length': '718000'}], 'track-count': 10}], 'medium-track-count': 10, 'medium-count': 1}], 'artist-credit-phrase': 'Psychonaut 4'}], 'recording-count': 13023} + +""" + +if __name__ == "__main__": + import musicbrainzngs + + musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") + options = Options([musicbrainzngs.search_artists("Crystal F")]) + options.choose(0) + print(options.get_current_option()) diff --git a/src/musify.py b/src/musify.py index 795d067..5a652e1 100644 --- a/src/musify.py +++ b/src/musify.py @@ -11,6 +11,10 @@ session.headers = { } +def set_proxy(proxies): + session.proxies = proxies + + def get_musify_url(row): title = row.title artists = row.artist @@ -75,6 +79,8 @@ def search_for_track(row): soup = get_soup_of_search(f"{artist[0]} - {track}") tracklist_container_soup = soup.find_all("div", {"class": "playlist"}) + if len(tracklist_container_soup) == 0: + return None if len(tracklist_container_soup) != 1: raise Exception("Connfusion Error. HTML Layout of https://musify.club changed.") tracklist_container_soup = tracklist_container_soup[0] @@ -113,12 +119,17 @@ if __name__ == "__main__": import pandas as pd import json + TOR = True + if TOR: + set_proxy({ + 'http': 'socks5h://127.0.0.1:9150', + 'https': 'socks5h://127.0.0.1:9150' + }) + df = pd.read_csv("../temp/.cache1.csv") for idx, row in df.iterrows(): row['artist'] = json.loads(row['artist'].replace("'", '"')) print("-" * 200) - print("fast") - print(get_musify_url(row)) print("slow") print(get_musify_url_slow(row)) From 595eeac88035729d35d63067f0059a116ee4ccb8 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Thu, 27 Oct 2022 14:15:18 +0200 Subject: [PATCH 02/35] started on an sqllite db --- sql.db | 0 src/metadata/database.py | 49 +++++++++++++++++++++++++++++ src/metadata/database_structure.sql | 37 ++++++++++++++++++++++ src/metadata/download.py | 17 ++++++++++ 4 files changed, 103 insertions(+) create mode 100644 sql.db create mode 100644 src/metadata/database.py create mode 100644 src/metadata/database_structure.sql diff --git a/sql.db b/sql.db new file mode 100644 index 0000000..e69de29 diff --git a/src/metadata/database.py b/src/metadata/database.py new file mode 100644 index 0000000..25b4e11 --- /dev/null +++ b/src/metadata/database.py @@ -0,0 +1,49 @@ +import sqlite3 +import os +import logging + +logging.basicConfig(level=logging.DEBUG) + +def get_temp_dir(): + import tempfile + + temp_folder = "music-downloader" + temp_dir = os.path.join(tempfile.gettempdir(), temp_folder) + if not os.path.exists(temp_dir): + os.mkdir(temp_dir) + return temp_dir + +DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" +TEMP_DIR = get_temp_dir() +DATABASE_FILE = "metadata.db" +db_path = os.path.join(TEMP_DIR, DATABASE_FILE) + +sqliteConnection = sqlite3.connect(db_path) +cursor = sqliteConnection.cursor() + +def init_db(cursor, reset_anyways: bool = False): + # check if db exists + exists = True + try: + query = 'SELECT * FROM track;' + cursor.execute(query) + _ = cursor.fetchall() + except sqlite3.OperationalError: + exists = False + + if not exists: + logging.info("Database does not exist yet.") + + if reset_anyways or not exists: + # reset the database if reset_anyways is true or if an error has been thrown previously. + logging.info("Creating/Reseting Database.") + + # read the file + with open(DATABASE_STRUCTURE_FILE, "r") as database_structure_file: + query = database_structure_file.read() + cursor.executescript(query) + +init_db(cursor=cursor) + +if __name__ == "__main__": + pass diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql new file mode 100644 index 0000000..91c7c5f --- /dev/null +++ b/src/metadata/database_structure.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS artist; +CREATE TABLE artist ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT +); + +DROP TABLE IF EXISTS artist_release_group; +CREATE TABLE artist_release_group ( + artist_id TEXT NOT NULL, + release_group_id TEXT NOT NULL +); +DROP TABLE IF EXISTS artist_track; +CREATE TABLE artist_track ( + artist_id TEXT NOT NULL, + track_id TEXT NOT NULL +); + +DROP TABLE IF EXISTS release_group; +CREATE TABLE release_group ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT +); + +DROP TABLE IF EXISTS release_; +CREATE TABLE release_ ( + id TEXT PRIMARY KEY NOT NULL, + release_group TEXT NOT NULL, + name TEXT +); + +DROP TABLE IF EXISTS track; +CREATE TABLE track ( + id TEXT PRIMARY KEY NOT NULL, + release_id TEXT NOT NULL, + name TEXT +); + diff --git a/src/metadata/download.py b/src/metadata/download.py index 08ed57d..f800c60 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -5,8 +5,13 @@ import pandas as pd import logging from datetime import date +import sqlite3 + from object_handeling import get_elem_from_obj, parse_music_brainz_date +# I don't know if it would be feesable to set up my own mb instance +# https://github.com/metabrainz/musicbrainz-docker + mb_log = logging.getLogger("musicbrainzngs") mb_log.setLevel(logging.WARNING) musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") @@ -34,6 +39,7 @@ class Artist: self.artist = get_elem_from_obj(artist_data, ['name']) + logging.info(f"artist: {self}") if not new_release_groups: return # sort all release groups by date and add album sort to have them in chronological order. @@ -150,6 +156,7 @@ class Release: self.title = get_elem_from_obj(release_data, ['title']) self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) + logging.info(f"release {self}") self.append_recordings(recording_datas) def append_recordings(self, recording_datas: dict): @@ -417,7 +424,17 @@ def download_track(mb_id, is_various_artist: bool = None, track: int = None, tot if __name__ == "__main__": + """ + import tempfile + import os + + TEMP_FOLDER = "music-downloader" + TEMP_DIR = os.path.join(tempfile.gettempdir(), TEMP_FOLDER) + if not os.path.exists(TEMP_DIR): + os.mkdir(TEMP_DIR) + """ logging.basicConfig(level=logging.DEBUG) + sqliteConnection = sqlite3.connect('sql.db') download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) From 035a45b7d8e4f3987b8ef14c37351b3ecd138f56 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Thu, 27 Oct 2022 15:00:24 +0200 Subject: [PATCH 03/35] started on an sqllite db --- src/metadata/database.py | 38 ++++++++++++++++++++++++++--- src/metadata/database_structure.sql | 6 ++++- src/metadata/download.py | 25 ++++++++++++++++++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/metadata/database.py b/src/metadata/database.py index 25b4e11..1264b81 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -18,10 +18,10 @@ TEMP_DIR = get_temp_dir() DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) -sqliteConnection = sqlite3.connect(db_path) -cursor = sqliteConnection.cursor() +connection = sqlite3.connect(db_path) +cursor = connection.cursor() -def init_db(cursor, reset_anyways: bool = False): +def init_db(cursor, connection, reset_anyways: bool = False): # check if db exists exists = True try: @@ -43,7 +43,37 @@ def init_db(cursor, reset_anyways: bool = False): query = database_structure_file.read() cursor.executescript(query) -init_db(cursor=cursor) +init_db(cursor=cursor, connection=connection, reset_anyways=True) + +def add_artist( + musicbrainz_artistid: str, + artist: str = None +): + query = "INSERT INTO artist (id, name) VALUES (?, ?);" + values = musicbrainz_artistid, artist + + cursor.execute(query, values) + connection.commit() + +def add_release_group( + musicbrainz_releasegroupid: str, + artist_ids: list, + albumartist: str = None, + albumsort: int = None, + musicbrainz_albumtype: str = None, + compilation: str = None +): + # add adjacency + adjacency_list = [] + for artist_id in artist_ids: + adjacency_list.append((musicbrainz_releasegroupid, artist_id)) + adjacency_values = tuple(adjacency_list) + adjacency_query = "INSERT INTO artist_release_group (artist_id, release_group_id) VALUES (?, ?);" + cursor.executemany(adjacency_query, adjacency_values) + connection.commit() + + # add release group + query = "INSERT INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation) VALUES (?, ?);" if __name__ == "__main__": pass diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql index 91c7c5f..0a2fae0 100644 --- a/src/metadata/database_structure.sql +++ b/src/metadata/database_structure.sql @@ -9,6 +9,7 @@ CREATE TABLE artist_release_group ( artist_id TEXT NOT NULL, release_group_id TEXT NOT NULL ); + DROP TABLE IF EXISTS artist_track; CREATE TABLE artist_track ( artist_id TEXT NOT NULL, @@ -18,7 +19,10 @@ CREATE TABLE artist_track ( DROP TABLE IF EXISTS release_group; CREATE TABLE release_group ( id TEXT PRIMARY KEY NOT NULL, - name TEXT + albumartist TEXT, + albumsort INT, + musicbrainz_albumtype TEXT, + compilation TEXT ); DROP TABLE IF EXISTS release_; diff --git a/src/metadata/download.py b/src/metadata/download.py index f800c60..ca66306 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -1,3 +1,4 @@ +import imp from typing import List import musicbrainzngs @@ -8,6 +9,7 @@ from datetime import date import sqlite3 from object_handeling import get_elem_from_obj, parse_music_brainz_date +import database # I don't know if it would be feesable to set up my own mb instance # https://github.com/metabrainz/musicbrainz-docker @@ -39,7 +41,9 @@ class Artist: self.artist = get_elem_from_obj(artist_data, ['name']) - logging.info(f"artist: {self}") + self.save() + + # STARTING TO FETCH' RELEASE GROUPS. IMPORTANT: DON'T WRITE ANYTHING BESIDES THAT HERE if not new_release_groups: return # sort all release groups by date and add album sort to have them in chronological order. @@ -55,6 +59,13 @@ class Artist: albumsort=i + 1 )) + def save(self): + logging.info(f"artist: {self}") + database.add_artist( + musicbrainz_artistid=self.musicbrainz_artistid, + artist=self.artist + ) + def __str__(self): newline = "\n" return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" @@ -94,6 +105,8 @@ class ReleaseGroup: self.musicbrainz_albumtype = get_elem_from_obj(release_group_data, ['primary-type']) self.compilation = "1" if self.musicbrainz_albumtype == "Compilation" else None + self.save() + if only_download_distinct_releases: self.append_distinct_releases(release_datas) else: @@ -111,6 +124,16 @@ class ReleaseGroup: self.artists.append(new_artist) return new_artist + def save(self): + database.add_release_group( + musicbrainz_releasegroupid=self.musicbrainz_releasegroupid, + artist_ids=[artist.musicbrainz_artistid for artist in self.artists], + albumartist = self.albumartist, + albumsort = self.albumsort, + musicbrainz_albumtype = self.musicbrainz_albumtype, + compilation=self.compilation + ) + def append_release(self, release_data: dict): musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) if musicbrainz_albumid is None: From 0f0e605cf9aa5fa2fedc1a4b5f99d739f9ee0545 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Thu, 27 Oct 2022 15:27:57 +0200 Subject: [PATCH 04/35] continued implementing caching in the sqlight db --- sql.db | 0 src/metadata/database.py | 17 ++++++++++++++++- src/metadata/database_structure.sql | 5 +++-- src/metadata/download.py | 16 ++++++++++------ 4 files changed, 29 insertions(+), 9 deletions(-) delete mode 100644 sql.db diff --git a/sql.db b/sql.db deleted file mode 100644 index e69de29..0000000 diff --git a/src/metadata/database.py b/src/metadata/database.py index 1264b81..038ce5e 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -73,7 +73,22 @@ def add_release_group( connection.commit() # add release group - query = "INSERT INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation) VALUES (?, ?);" + query = "INSERT INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation) VALUES (?, ?, ?, ?, ?);" + values = musicbrainz_releasegroupid, albumartist, albumsort, musicbrainz_albumtype, compilation + cursor.execute(query, values) + connection.commit() + +def add_release( + musicbrainz_albumid: str, + release_group_id: str, + title: str = None, + copyright_: str = None +): + query = "INSERT INTO release_ (id, release_group_id, title, copyright) VALUES (?, ?, ?, ?);" + values = musicbrainz_albumid, release_group_id, title, copyright_ + + cursor.execute(query, values) + connection.commit() if __name__ == "__main__": pass diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql index 0a2fae0..5d6542f 100644 --- a/src/metadata/database_structure.sql +++ b/src/metadata/database_structure.sql @@ -28,8 +28,9 @@ CREATE TABLE release_group ( DROP TABLE IF EXISTS release_; CREATE TABLE release_ ( id TEXT PRIMARY KEY NOT NULL, - release_group TEXT NOT NULL, - name TEXT + release_group_id TEXT NOT NULL, + title TEXT, + copyright TEXT ); DROP TABLE IF EXISTS track; diff --git a/src/metadata/download.py b/src/metadata/download.py index ca66306..35ee02f 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -1,13 +1,9 @@ -import imp from typing import List - import musicbrainzngs import pandas as pd import logging from datetime import date -import sqlite3 - from object_handeling import get_elem_from_obj, parse_music_brainz_date import database @@ -179,7 +175,7 @@ class Release: self.title = get_elem_from_obj(release_data, ['title']) self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) - logging.info(f"release {self}") + self.save() self.append_recordings(recording_datas) def append_recordings(self, recording_datas: dict): @@ -190,6 +186,15 @@ class Release: self.tracklist.append(musicbrainz_releasetrackid) + def save(self): + logging.info(f"release {self}") + database.add_release( + musicbrainz_albumid=self.musicbrainz_albumid, + release_group_id=self.release_group.musicbrainz_releasegroupid, + title=self.title, + copyright_=self.copyright + ) + def __str__(self): return f"{self.title} ©{self.copyright}" @@ -457,7 +462,6 @@ if __name__ == "__main__": os.mkdir(TEMP_DIR) """ logging.basicConfig(level=logging.DEBUG) - sqliteConnection = sqlite3.connect('sql.db') download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) From f4d0dd0089e0950658289ef4e0819b05ba19cf8a Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Thu, 27 Oct 2022 15:55:16 +0200 Subject: [PATCH 05/35] continued at step from prev commit --- src/metadata/database.py | 9 ++++--- src/metadata/download.py | 56 +++++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/metadata/database.py b/src/metadata/database.py index 038ce5e..4a323ca 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -2,8 +2,6 @@ import sqlite3 import os import logging -logging.basicConfig(level=logging.DEBUG) - def get_temp_dir(): import tempfile @@ -90,5 +88,10 @@ def add_release( cursor.execute(query, values) connection.commit() -if __name__ == "__main__": +def add_track( + +): pass + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) diff --git a/src/metadata/download.py b/src/metadata/download.py index 35ee02f..7dabeb7 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -15,7 +15,7 @@ mb_log.setLevel(logging.WARNING) musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") -# IMPORTANT +# IMPORTANT DOCUMENTATION WHICH CONTAINS FOR EXAMPLE THE INCLUDES # https://python-musicbrainzngs.readthedocs.io/en/v0.7.1/api/#getting-data class Artist: @@ -54,6 +54,10 @@ class Artist: artists=[self], albumsort=i + 1 )) + + def __str__(self): + newline = "\n" + return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" def save(self): logging.info(f"artist: {self}") @@ -62,10 +66,6 @@ class Artist: artist=self.artist ) - def __str__(self): - newline = "\n" - return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" - class ReleaseGroup: def __init__( @@ -112,15 +112,8 @@ class ReleaseGroup: newline = "\n" return f"{newline.join([str(release_group) for release_group in self.releases])}" - def append_artist(self, artist_id: str) -> Artist: - for existing_artist in self.artists: - if artist_id == existing_artist.musicbrainz_artistid: - return existing_artist - new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) - self.artists.append(new_artist) - return new_artist - def save(self): + logging.info(f"caching release_group {self}") database.add_release_group( musicbrainz_releasegroupid=self.musicbrainz_releasegroupid, artist_ids=[artist.musicbrainz_artistid for artist in self.artists], @@ -130,6 +123,14 @@ class ReleaseGroup: compilation=self.compilation ) + def append_artist(self, artist_id: str) -> Artist: + for existing_artist in self.artists: + if artist_id == existing_artist.musicbrainz_artistid: + return existing_artist + new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) + self.artists.append(new_artist) + return new_artist + def append_release(self, release_data: dict): musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) if musicbrainz_albumid is None: @@ -178,6 +179,18 @@ class Release: self.save() self.append_recordings(recording_datas) + def __str__(self): + return f"{self.title} ©{self.copyright}" + + def save(self): + logging.info(f"caching release {self}") + database.add_release( + musicbrainz_albumid=self.musicbrainz_albumid, + release_group_id=self.release_group.musicbrainz_releasegroupid, + title=self.title, + copyright_=self.copyright + ) + def append_recordings(self, recording_datas: dict): for recording_data in recording_datas: musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['id']) @@ -186,17 +199,6 @@ class Release: self.tracklist.append(musicbrainz_releasetrackid) - def save(self): - logging.info(f"release {self}") - database.add_release( - musicbrainz_albumid=self.musicbrainz_albumid, - release_group_id=self.release_group.musicbrainz_releasegroupid, - title=self.title, - copyright_=self.copyright - ) - - def __str__(self): - return f"{self.title} ©{self.copyright}" class Track: @@ -213,6 +215,12 @@ class Track: self.musicbrainz_releasetrackid = musicbrainz_releasetrackid self.release = release + def __str__(self): + return "this is a track" + + def save(self): + logging.info("caching track {self}") + def download(option: dict): type_ = option['type'] From 1253581e3032dd99436ad2b09ceb26d642dbdfcb Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 27 Oct 2022 19:53:12 +0200 Subject: [PATCH 06/35] finished implementing sqlite --- .idea/dataSources.xml | 12 +++++ src/metadata/database.py | 81 ++++++++++++++++++++--------- src/metadata/database_structure.sql | 14 +++-- src/metadata/download.py | 79 ++++++++++++++++++++++------ 4 files changed, 142 insertions(+), 44 deletions(-) create mode 100644 .idea/dataSources.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..879cd05 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:/tmp/music-downloader/metadata.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/src/metadata/database.py b/src/metadata/database.py index 4a323ca..59367eb 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -2,6 +2,7 @@ import sqlite3 import os import logging + def get_temp_dir(): import tempfile @@ -11,7 +12,8 @@ def get_temp_dir(): os.mkdir(temp_dir) return temp_dir -DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" + +DATABASE_STRUCTURE_FILE = "database_structure.sql" TEMP_DIR = get_temp_dir() DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) @@ -19,6 +21,7 @@ db_path = os.path.join(TEMP_DIR, DATABASE_FILE) connection = sqlite3.connect(db_path) cursor = connection.cursor() + def init_db(cursor, connection, reset_anyways: bool = False): # check if db exists exists = True @@ -28,7 +31,7 @@ def init_db(cursor, connection, reset_anyways: bool = False): _ = cursor.fetchall() except sqlite3.OperationalError: exists = False - + if not exists: logging.info("Database does not exist yet.") @@ -40,58 +43,88 @@ def init_db(cursor, connection, reset_anyways: bool = False): with open(DATABASE_STRUCTURE_FILE, "r") as database_structure_file: query = database_structure_file.read() cursor.executescript(query) + connection.commit() -init_db(cursor=cursor, connection=connection, reset_anyways=True) def add_artist( - musicbrainz_artistid: str, - artist: str = None + musicbrainz_artistid: str, + artist: str = None ): - query = "INSERT INTO artist (id, name) VALUES (?, ?);" + query = "INSERT OR REPLACE INTO artist (id, name) VALUES (?, ?);" values = musicbrainz_artistid, artist cursor.execute(query, values) connection.commit() + def add_release_group( - musicbrainz_releasegroupid: str, - artist_ids: list, - albumartist: str = None, - albumsort: int = None, - musicbrainz_albumtype: str = None, - compilation: str = None + musicbrainz_releasegroupid: str, + artist_ids: list, + albumartist: str = None, + albumsort: int = None, + musicbrainz_albumtype: str = None, + compilation: str = None, + album_artist_id: str = None ): # add adjacency adjacency_list = [] for artist_id in artist_ids: - adjacency_list.append((musicbrainz_releasegroupid, artist_id)) + adjacency_list.append((artist_id, musicbrainz_releasegroupid)) adjacency_values = tuple(adjacency_list) - adjacency_query = "INSERT INTO artist_release_group (artist_id, release_group_id) VALUES (?, ?);" + adjacency_query = "INSERT OR REPLACE INTO artist_release_group (artist_id, release_group_id) VALUES (?, ?);" cursor.executemany(adjacency_query, adjacency_values) connection.commit() # add release group - query = "INSERT INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation) VALUES (?, ?, ?, ?, ?);" - values = musicbrainz_releasegroupid, albumartist, albumsort, musicbrainz_albumtype, compilation + query = "INSERT OR REPLACE INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id) VALUES (?, ?, ?, ?, ?, ?);" + values = musicbrainz_releasegroupid, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id cursor.execute(query, values) connection.commit() + def add_release( - musicbrainz_albumid: str, - release_group_id: str, - title: str = None, - copyright_: str = None + musicbrainz_albumid: str, + release_group_id: str, + title: str = None, + copyright_: str = None, + album_status: str = None, + language: str = None, + year: str = None, + date: str = None, + country: str = None, + barcode: str = None ): - query = "INSERT INTO release_ (id, release_group_id, title, copyright) VALUES (?, ?, ?, ?);" - values = musicbrainz_albumid, release_group_id, title, copyright_ + query = "INSERT OR REPLACE INTO release_ (id, release_group_id, title, copyright, album_status, language, year, date, country, barcode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" + values = musicbrainz_albumid, release_group_id, title, copyright_, album_status, language, year, date, country, barcode cursor.execute(query, values) connection.commit() + def add_track( - + musicbrainz_releasetrackid: str, + musicbrainz_albumid: str, + feature_aritsts: list, + track: str = None, + isrc: str = None ): - pass + # add adjacency + adjacency_list = [] + for artist_id in feature_aritsts: + adjacency_list.append((artist_id, musicbrainz_releasetrackid)) + adjacency_values = tuple(adjacency_list) + adjacency_query = "INSERT OR REPLACE INTO artist_track (artist_id, track_id) VALUES (?, ?);" + cursor.executemany(adjacency_query, adjacency_values) + connection.commit() + + # add track + query = "INSERT OR REPLACE INTO track (id, release_id, track, isrc) VALUES (?, ?, ?, ?);" + values = musicbrainz_releasetrackid, musicbrainz_albumid, track, isrc + cursor.execute(query, values) + connection.commit() + + +init_db(cursor=cursor, connection=connection, reset_anyways=False) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql index 5d6542f..0d86ee2 100644 --- a/src/metadata/database_structure.sql +++ b/src/metadata/database_structure.sql @@ -22,7 +22,8 @@ CREATE TABLE release_group ( albumartist TEXT, albumsort INT, musicbrainz_albumtype TEXT, - compilation TEXT + compilation TEXT, + album_artist_id TEXT ); DROP TABLE IF EXISTS release_; @@ -30,13 +31,20 @@ CREATE TABLE release_ ( id TEXT PRIMARY KEY NOT NULL, release_group_id TEXT NOT NULL, title TEXT, - copyright TEXT + copyright TEXT, + album_status TEXT, + language TEXT, + year TEXT, + date TEXT, + country TEXT, + barcode TEXT ); DROP TABLE IF EXISTS track; CREATE TABLE track ( id TEXT PRIMARY KEY NOT NULL, release_id TEXT NOT NULL, - name TEXT + track TEXT, + isrc TEXT ); diff --git a/src/metadata/download.py b/src/metadata/download.py index 7dabeb7..e0ffad5 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -2,7 +2,6 @@ from typing import List import musicbrainzngs import pandas as pd import logging -from datetime import date from object_handeling import get_elem_from_obj, parse_music_brainz_date import database @@ -54,7 +53,7 @@ class Artist: artists=[self], albumsort=i + 1 )) - + def __str__(self): newline = "\n" return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" @@ -95,7 +94,8 @@ class ReleaseGroup: if artist_id is None: continue self.append_artist(artist_id) - self.albumartist = "Various Artists" if len(self.artists) >= 1 else self.artists[0].artist + self.albumartist = "Various Artists" if len(self.artists) > 1 else self.artists[0].artist + self.album_artist_id = None if self.albumartist == "Various Artists" else self.artists[0].musicbrainz_artistid self.albumsort = albumsort self.musicbrainz_albumtype = get_elem_from_obj(release_group_data, ['primary-type']) @@ -117,10 +117,11 @@ class ReleaseGroup: database.add_release_group( musicbrainz_releasegroupid=self.musicbrainz_releasegroupid, artist_ids=[artist.musicbrainz_artistid for artist in self.artists], - albumartist = self.albumartist, - albumsort = self.albumsort, - musicbrainz_albumtype = self.musicbrainz_albumtype, - compilation=self.compilation + albumartist=self.albumartist, + albumsort=self.albumsort, + musicbrainz_albumtype=self.musicbrainz_albumtype, + compilation=self.compilation, + album_artist_id=self.album_artist_id ) def append_artist(self, artist_id: str) -> Artist: @@ -176,29 +177,41 @@ class Release: self.title = get_elem_from_obj(release_data, ['title']) self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) + self.album_status = get_elem_from_obj(release_data, ['status']) + self.language = get_elem_from_obj(release_data, ['text-representation', 'language']) + self.year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) + self.date = get_elem_from_obj(release_data, ['date']) + self.country = get_elem_from_obj(release_data, ['country']) + self.barcode = get_elem_from_obj(release_data, ['barcode']) + self.save() self.append_recordings(recording_datas) def __str__(self): - return f"{self.title} ©{self.copyright}" - + return f"{self.title} ©{self.copyright} {self.album_status}" + def save(self): logging.info(f"caching release {self}") database.add_release( musicbrainz_albumid=self.musicbrainz_albumid, release_group_id=self.release_group.musicbrainz_releasegroupid, title=self.title, - copyright_=self.copyright + copyright_=self.copyright, + album_status=self.album_status, + language=self.language, + year=self.year, + date=self.date, + country=self.country, + barcode=self.barcode ) def append_recordings(self, recording_datas: dict): for recording_data in recording_datas: - musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['id']) + musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['recording', 'id']) if musicbrainz_releasetrackid is None: continue - self.tracklist.append(musicbrainz_releasetrackid) - + self.tracklist.append(Track(musicbrainz_releasetrackid, self)) class Track: @@ -214,12 +227,42 @@ class Track: self.musicbrainz_releasetrackid = musicbrainz_releasetrackid self.release = release + self.artists = [] + + result = musicbrainzngs.get_recording_by_id(self.musicbrainz_releasetrackid, includes=["artists", "releases", "recording-rels", "isrcs", "work-level-rels"]) + recording_data = result['recording'] + for artist_data in get_elem_from_obj(recording_data, ['artist-credit'], return_if_none=[]): + self.append_artist(get_elem_from_obj(artist_data, ['artist', 'id'])) + + self.isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) + self.title = recording_data['title'] + + self.save() def __str__(self): - return "this is a track" - + return f"{self.title}: {self.isrc}" + def save(self): - logging.info("caching track {self}") + logging.info(f"caching track {self}") + + database.add_track( + musicbrainz_releasetrackid=self.musicbrainz_releasetrackid, + musicbrainz_albumid=self.release.musicbrainz_albumid, + feature_aritsts=[artist.musicbrainz_artistid for artist in self.artists], + track=self.title, + isrc=self.isrc + ) + + def append_artist(self, artist_id: str) -> Artist: + if artist_id is None: + return + + for existing_artist in self.artists: + if artist_id == existing_artist.musicbrainz_artistid: + return existing_artist + new_artist = Artist(artist_id, new_release_groups=False) + self.artists.append(new_artist) + return new_artist def download(option: dict): @@ -469,7 +512,9 @@ if __name__ == "__main__": if not os.path.exists(TEMP_DIR): os.mkdir(TEMP_DIR) """ - logging.basicConfig(level=logging.DEBUG) + + logger = logging.getLogger() + logger.setLevel(logging.INFO) download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) From a50575102bfcc49f91db13bc997bc8aaf39ba729 Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 27 Oct 2022 19:54:52 +0200 Subject: [PATCH 07/35] removed unused functions --- src/metadata/download.py | 218 --------------------------------------- 1 file changed, 218 deletions(-) diff --git a/src/metadata/download.py b/src/metadata/download.py index e0ffad5..64ea8a7 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -284,224 +284,6 @@ def download(option: dict): return metadata_df -def download_artist(mb_id): - """ - Available includes: recordings, releases, release-groups, works, various-artists, discids, media, isrcs, - aliases, annotation, area-rels, artist-rels, label-rels, place-rels, event-rels, recording-rels, - release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels, tags, user-tags, - ratings, user-ratings - """ - - metadata_list = [] - # from this dict everything will be taken - following_data = {} - - result = musicbrainzngs.get_artist_by_id(mb_id, includes=["release-groups", "releases"]) - artist_data = result['artist'] - - # sort all release groups by date and add album sort to have them in chronological order. - release_groups = artist_data['release-group-list'] - for i, release_group in enumerate(release_groups): - release_groups[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) - release_groups.sort(key=lambda x: x['first-release-date']) - - for i, release_group in enumerate(release_groups): - release_groups[i]['albumsort'] = i + 1 - - def numeric_release_type(release_type: str) -> int: - if release_type == "Album" or release_type == "EP": - return 1 - return 2 - - release_groups.sort(key=lambda x: numeric_release_type(x['type'])) - - for release_group in release_groups: - download_release_groups() - - -def download_release(mb_id, album_sort: int = None): - """ - Available includes: artists, labels, recordings, release-groups, media, artist-credits, discids, isrcs, - recording-level-rels, work-level-rels, annotation, aliases, tags, user-tags, area-rels, artist-rels, - label-rels, place-rels, event-rels, recording-rels, release-rels, release-group-rels, series-rels, url-rels, - work-rels, instrument-rels - """ - - def get_additional_artist_info(mb_id_): - r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) - - album_sort = 0 - for i, release in enumerate(r["artist"]["release-list"]): - id_ = release["id"] - if id_ == mb_id: - album_sort = i - break - - return album_sort - - result = musicbrainzngs.get_release_by_id(mb_id, includes=["artists", "recordings", 'release-groups']) - - if album_sort is None: - album_sort = get_additional_artist_info( - get_elem_from_obj(result, ['release', 'artist-credit', 0, 'artist', 'id'])) - release_type = get_elem_from_obj(result, ['release', 'release-group', 'type']) - - tracklist_metadata = [] - - is_various_artist = len(result['release']['artist-credit']) > 1 - tracklist = result['release']['medium-list'][0]['track-list'] - track_count = len(tracklist) - for track in tracklist: - track_id = track["recording"]["id"] - this_track = track["position"] - - tracklist_metadata.extend( - download_track(track_id, is_various_artist=is_various_artist, track=this_track, - total_tracks=track_count, album_sort=album_sort, album_type=release_type, - release_data=result['release'])) - - return tracklist_metadata - - -def download_track(mb_id, is_various_artist: bool = None, track: int = None, total_tracks: int = None, - album_sort: int = None, album_type: str = None, release_data: dict = None): - """ - TODO - bpm its kind of possible via the AcousticBrainz API. however, the data may not be of very good - quality and AB is scheduled to go away in some time. - - compilation Field that is used by iTunes to mark albums as compilation. - Either enter the value 1 or delete the field. https://en.wikipedia.org/wiki/Compilation_album - How should I get it? I don't fucking know. Now I do. Release Group Type is Compilation - - composer, copyright, discsubtitle - 'musicbrainz_discid', - 'asin', - 'performer', - 'catalognumber', - 'musicbrainz_releasetrackid', - 'musicbrainz_releasegroupid', - 'musicbrainz_workid', - 'acoustid_fingerprint', - 'acoustid_id' - - DONE - - album - title - artist - albumartist - tracknumber - !!!albumsort can sort albums cronological - titlesort is just set to the tracknumber to sort by track order to sort correctly - isrc - musicbrainz_artistid - musicbrainz_albumid - musicbrainz_albumartistid - musicbrainz_albumstatus - language - musicbrainz_albumtype - 'releasecountry' - 'barcode' - - Album Art - """ - """ - Available includes: artists, releases, discids, media, artist-credits, isrcs, work-level-rels, annotation, - aliases, tags, user-tags, ratings, user-ratings, area-rels, artist-rels, label-rels, place-rels, event-rels, - recording-rels, release-rels, release-group-rels, series-rels, url-rels, work-rels, instrument-rels - """ - - result = musicbrainzngs.get_recording_by_id(mb_id, includes=["artists", "releases", "recording-rels", "isrcs", - "work-level-rels"]) - recording_data = result['recording'] - isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) - - if release_data is None: - # choosing the last release, because it is the least likely one to be a single - release_data = recording_data['release-list'][-1] - mb_release_id = release_data['id'] - - title = recording_data['title'] - - artist = [] - mb_artist_ids = [] - for artist_ in recording_data['artist-credit']: - name_ = get_elem_from_obj(artist_, ['artist', 'name']) - if name_ is None: - continue - artist.append(name_) - mb_artist_ids.append(get_elem_from_obj(artist_, ['artist', 'id'])) - - def get_additional_artist_info(mb_id_): - r = musicbrainzngs.get_artist_by_id(mb_id_, includes=["releases"]) - - album_sort = 0 - for i, release in enumerate(r["artist"]["release-list"]): - id_ = release["id"] - if id_ == mb_release_id: - album_sort = i - break - - return album_sort - - def get_additional_release_info(mb_id_): - r = musicbrainzngs.get_release_by_id(mb_id_, - includes=["artists", "recordings", "recording-rels", 'release-groups']) - is_various_artist_ = len(r['release']['artist-credit']) > 1 - tracklist = r['release']['medium-list'][0]['track-list'] - track_count_ = len(tracklist) - this_track_ = 0 - for track in tracklist: - if track["recording"]["id"] == mb_id: - this_track_ = track["position"] - - release_type = get_elem_from_obj(r, ['release', 'release-group', 'type']) - - return is_various_artist_, this_track_, track_count_, release_type - - album_id = get_elem_from_obj(release_data, ['id']) - album = get_elem_from_obj(release_data, ['title']) - album_status = get_elem_from_obj(release_data, ['status']) - language = get_elem_from_obj(release_data, ['text-representation', 'language']) - - year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) - date = get_elem_from_obj(release_data, ['date']) - if is_various_artist is None or track is None or total_tracks is None or album_type is None: - is_various_artist, track, total_tracks, album_type = get_additional_release_info(album_id) - if album_sort is None: - album_sort = get_additional_artist_info(mb_artist_ids[0]) - album_artist = "Various Artists" if is_various_artist else artist[0] - album_artist_id = None if album_artist == "Various Artists" else mb_artist_ids[0] - compilation = "1" if album_type == "Compilation" else None - country = get_elem_from_obj(release_data, ['country']) - barcode = get_elem_from_obj(release_data, ['barcode']) - - return [{ - 'id': mb_id, - 'album': album, - 'title': title, - 'artist': artist, - 'album_artist': album_artist, - 'tracknumber': str(track), - 'albumsort': album_sort, - 'titlesort': track, - 'isrc': isrc, - 'date': date, - 'year': year, - 'musicbrainz_artistid': mb_artist_ids[0], - 'musicbrainz_albumid': mb_release_id, - 'musicbrainz_albumartistid': album_artist_id, - 'musicbrainz_albumstatus': album_status, - 'total_tracks': total_tracks, - 'language': language, - 'musicbrainz_albumtype': album_type, - 'compilation': compilation, - 'releasecountry': country, - 'barcode': barcode - }] - - if __name__ == "__main__": """ import tempfile From 2d6061dc0441bccf08f8347bf28821be458d0b85 Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 27 Oct 2022 23:09:37 +0200 Subject: [PATCH 08/35] finished for the night *I hope* --- src/metadata/database.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/metadata/database.py b/src/metadata/database.py index 59367eb..e3b03b5 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -19,6 +19,7 @@ DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) connection = sqlite3.connect(db_path) +connection.row_factory = sqlite3.Row cursor = connection.cursor() @@ -124,7 +125,45 @@ def add_track( connection.commit() +def get_track_metadata(musicbrainz_releasetrackid: str): + # this would be vulnerable if musicbrainz_releasetrackid would be user input + query = f""" +SELECT DISTINCT + track.id as musicbrainz_releasetrackid, + release_.id as musicbrainz_albumid, + release_group.id as musicbrainz_releasegroupid, + track.track as track, + track.isrc as isrc, + release_.title as title, + release_.copyright as copyright, + release_.album_status as album_status, + release_.language as language, + release_.year as year, + release_.date as date, + release_.country as country, + release_.barcode as barcode, + release_group.albumartist as albumartist, + release_group.albumsort as albumsort, + release_group.musicbrainz_albumtype as musicbrainz_albumtype, + release_group.compilation as compilation, + release_group.album_artist_id as album_artist_id +FROM track, release_, release_group, artist +WHERE + track.id == "{musicbrainz_releasetrackid}" AND + track.release_id == release_.id AND + release_group.id == release_.release_group_id; + """ + + resulting_tracks = list(cursor.execute(query)) + if len(resulting_tracks) != 1: + return -1 + + return dict(resulting_tracks[0]) + + init_db(cursor=cursor, connection=connection, reset_anyways=False) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) + + print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) From 0baca19e02fc48ceb4d6a1102c837dfd3648635d Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Fri, 28 Oct 2022 12:35:26 +0200 Subject: [PATCH 09/35] added couple functions to fetch data from database --- src/metadata/database.py | 37 ++++++++++++++++++++++------- src/metadata/database_structure.sql | 7 ++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/metadata/database.py b/src/metadata/database.py index e3b03b5..c4ab77c 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -13,7 +13,8 @@ def get_temp_dir(): return temp_dir -DATABASE_STRUCTURE_FILE = "database_structure.sql" +# DATABASE_STRUCTURE_FILE = "database_structure.sql" +DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" TEMP_DIR = get_temp_dir() DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) @@ -124,9 +125,14 @@ def add_track( cursor.execute(query, values) connection.commit() +def get_custom_track_querry(custom_where: list) -> str: + where_args = [ + "track.release_id == release_.id", + "release_group.id == release_.release_group_id" + ] + where_args.extend(custom_where) -def get_track_metadata(musicbrainz_releasetrackid: str): - # this would be vulnerable if musicbrainz_releasetrackid would be user input + where_arg = " AND ".join(where_args) query = f""" SELECT DISTINCT track.id as musicbrainz_releasetrackid, @@ -149,21 +155,36 @@ SELECT DISTINCT release_group.album_artist_id as album_artist_id FROM track, release_, release_group, artist WHERE - track.id == "{musicbrainz_releasetrackid}" AND - track.release_id == release_.id AND - release_group.id == release_.release_group_id; + {where_arg}; """ + return query - resulting_tracks = list(cursor.execute(query)) +def get_custom_track(custom_where: list): + query = get_custom_track_querry(custom_where=custom_where) + return [dict(i) for i in cursor.execute(query)] + +def get_track_metadata(musicbrainz_releasetrackid: str): + # this would be vulnerable if musicbrainz_releasetrackid would be user input + resulting_tracks = get_custom_track([f'track.id == "{musicbrainz_releasetrackid}"']) if len(resulting_tracks) != 1: return -1 return dict(resulting_tracks[0]) +def get_tracks_to_download(): + return get_custom_track(["track.downloaded == 0"]) + +def get_tracks_without_isrc(): + return get_custom_track(["track.isrc IS NULL"]) init_db(cursor=cursor, connection=connection, reset_anyways=False) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) - print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) + # get_track(["track.downloaded == 0", "track.isrc IS NOT NULL"]) + # + for track in get_tracks_without_isrc(): + print(track['track']) + + #print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql index 0d86ee2..9f7fc29 100644 --- a/src/metadata/database_structure.sql +++ b/src/metadata/database_structure.sql @@ -43,8 +43,11 @@ CREATE TABLE release_ ( DROP TABLE IF EXISTS track; CREATE TABLE track ( id TEXT PRIMARY KEY NOT NULL, + downloaded BOOLEAN NOT NULL DEFAULT 0, release_id TEXT NOT NULL, track TEXT, - isrc TEXT + isrc TEXT, + filepath TEXT, + url TEXT, + src TEXT ); - From ba60d9bbc134b0e24f48b42e6789e053a2de17b6 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Fri, 28 Oct 2022 13:01:13 +0200 Subject: [PATCH 10/35] added couple functions to fetch data from database --- .VSCodeCounter/2022-10-28_12-39-22/details.md | 37 +++++++++++++ .../2022-10-28_12-39-22/diff-details.md | 15 ++++++ .VSCodeCounter/2022-10-28_12-39-22/diff.csv | 2 + .VSCodeCounter/2022-10-28_12-39-22/diff.md | 19 +++++++ .VSCodeCounter/2022-10-28_12-39-22/diff.txt | 22 ++++++++ .../2022-10-28_12-39-22/results.csv | 24 +++++++++ .../2022-10-28_12-39-22/results.json | 1 + .VSCodeCounter/2022-10-28_12-39-22/results.md | 29 ++++++++++ .../2022-10-28_12-39-22/results.txt | 54 +++++++++++++++++++ 9 files changed, 203 insertions(+) create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/details.md create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/diff-details.md create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/diff.csv create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/diff.md create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/diff.txt create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/results.csv create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/results.json create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/results.md create mode 100644 .VSCodeCounter/2022-10-28_12-39-22/results.txt diff --git a/.VSCodeCounter/2022-10-28_12-39-22/details.md b/.VSCodeCounter/2022-10-28_12-39-22/details.md new file mode 100644 index 0000000..e0e8fe7 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/details.md @@ -0,0 +1,37 @@ +# Details + +Date : 2022-10-28 12:39:22 + +Directory /home/lars/Projects/music-downloader + +Total : 22 files, 1132 codes, 109 comments, 340 blanks, all 1581 lines + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | +| [music-downloader/.idea/dataSources.xml](/music-downloader/.idea/dataSources.xml) | XML | 12 | 0 | 0 | 12 | +| [music-downloader/.idea/inspectionProfiles/profiles_settings.xml](/music-downloader/.idea/inspectionProfiles/profiles_settings.xml) | XML | 6 | 0 | 0 | 6 | +| [music-downloader/.idea/misc.xml](/music-downloader/.idea/misc.xml) | XML | 4 | 0 | 0 | 4 | +| [music-downloader/.idea/modules.xml](/music-downloader/.idea/modules.xml) | XML | 8 | 0 | 0 | 8 | +| [music-downloader/.idea/music-downloader.iml](/music-downloader/.idea/music-downloader.iml) | XML | 10 | 0 | 0 | 10 | +| [music-downloader/.idea/vcs.xml](/music-downloader/.idea/vcs.xml) | XML | 6 | 0 | 0 | 6 | +| [music-downloader/README.md](/music-downloader/README.md) | Markdown | 75 | 0 | 34 | 109 | +| [music-downloader/requirements.txt](/music-downloader/requirements.txt) | pip requirements | 8 | 0 | 0 | 8 | +| [music-downloader/src/download.py](/music-downloader/src/download.py) | Python | 59 | 10 | 21 | 90 | +| [music-downloader/src/download_links.py](/music-downloader/src/download_links.py) | Python | 46 | 3 | 18 | 67 | +| [music-downloader/src/main.py](/music-downloader/src/main.py) | Python | 78 | 0 | 26 | 104 | +| [music-downloader/src/metadata/database.py](/music-downloader/src/metadata/database.py) | Python | 121 | 35 | 35 | 191 | +| [music-downloader/src/metadata/database_structure.sql](/music-downloader/src/metadata/database_structure.sql) | SQLite | 48 | 0 | 6 | 54 | +| [music-downloader/src/metadata/download.py](/music-downloader/src/metadata/download.py) | Python | 211 | 33 | 60 | 304 | +| [music-downloader/src/metadata/metadata.py](/music-downloader/src/metadata/metadata.py) | Python | 106 | 9 | 28 | 143 | +| [music-downloader/src/metadata/object_handeling.py](/music-downloader/src/metadata/object_handeling.py) | Python | 19 | 0 | 4 | 23 | +| [music-downloader/src/metadata/options.py](/music-downloader/src/metadata/options.py) | Python | 87 | 8 | 24 | 119 | +| [music-downloader/src/musify.py](/music-downloader/src/musify.py) | Python | 97 | 2 | 37 | 136 | +| [music-downloader/src/phonetic_compares.py](/music-downloader/src/phonetic_compares.py) | Python | 15 | 0 | 8 | 23 | +| [music-downloader/src/test.py](/music-downloader/src/test.py) | Python | 18 | 1 | 6 | 25 | +| [music-downloader/src/url_to_path.py](/music-downloader/src/url_to_path.py) | Python | 35 | 6 | 16 | 57 | +| [music-downloader/src/youtube_music.py](/music-downloader/src/youtube_music.py) | Python | 63 | 2 | 17 | 82 | + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md b/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md new file mode 100644 index 0000000..40dc8b6 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md @@ -0,0 +1,15 @@ +# Diff Details + +Date : 2022-10-28 12:39:22 + +Directory /home/lars/Projects/music-downloader + +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/diff.csv b/.VSCodeCounter/2022-10-28_12-39-22/diff.csv new file mode 100644 index 0000000..b7d8d75 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/diff.csv @@ -0,0 +1,2 @@ +"filename", "language", "", "comment", "blank", "total" +"Total", "-", , 0, 0, 0 \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/diff.md b/.VSCodeCounter/2022-10-28_12-39-22/diff.md new file mode 100644 index 0000000..70d009c --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/diff.md @@ -0,0 +1,19 @@ +# Diff Summary + +Date : 2022-10-28 12:39:22 + +Directory /home/lars/Projects/music-downloader + +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/diff.txt b/.VSCodeCounter/2022-10-28_12-39-22/diff.txt new file mode 100644 index 0000000..d35f11d --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/diff.txt @@ -0,0 +1,22 @@ +Date : 2022-10-28 12:39:22 +Directory : /home/lars/Projects/music-downloader +Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines + +Languages ++----------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++----------+------------+------------+------------+------------+------------+ ++----------+------------+------------+------------+------------+------------+ + +Directories ++------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++------+------------+------------+------------+------------+------------+ ++------+------------+------------+------------+------------+------------+ + +Files ++----------+----------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++----------+----------+------------+------------+------------+------------+ +| Total | | 0 | 0 | 0 | 0 | ++----------+----------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/results.csv b/.VSCodeCounter/2022-10-28_12-39-22/results.csv new file mode 100644 index 0000000..5996e22 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/results.csv @@ -0,0 +1,24 @@ +"filename", "language", "Python", "XML", "Markdown", "pip requirements", "SQLite", "comment", "blank", "total" +"/home/lars/Projects/music-downloader/.idea/dataSources.xml", "XML", 0, 12, 0, 0, 0, 0, 0, 12 +"/home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml", "XML", 0, 6, 0, 0, 0, 0, 0, 6 +"/home/lars/Projects/music-downloader/.idea/misc.xml", "XML", 0, 4, 0, 0, 0, 0, 0, 4 +"/home/lars/Projects/music-downloader/.idea/modules.xml", "XML", 0, 8, 0, 0, 0, 0, 0, 8 +"/home/lars/Projects/music-downloader/.idea/music-downloader.iml", "XML", 0, 10, 0, 0, 0, 0, 0, 10 +"/home/lars/Projects/music-downloader/.idea/vcs.xml", "XML", 0, 6, 0, 0, 0, 0, 0, 6 +"/home/lars/Projects/music-downloader/README.md", "Markdown", 0, 0, 75, 0, 0, 0, 34, 109 +"/home/lars/Projects/music-downloader/requirements.txt", "pip requirements", 0, 0, 0, 8, 0, 0, 0, 8 +"/home/lars/Projects/music-downloader/src/download.py", "Python", 59, 0, 0, 0, 0, 10, 21, 90 +"/home/lars/Projects/music-downloader/src/download_links.py", "Python", 46, 0, 0, 0, 0, 3, 18, 67 +"/home/lars/Projects/music-downloader/src/main.py", "Python", 78, 0, 0, 0, 0, 0, 26, 104 +"/home/lars/Projects/music-downloader/src/metadata/database.py", "Python", 121, 0, 0, 0, 0, 35, 35, 191 +"/home/lars/Projects/music-downloader/src/metadata/database_structure.sql", "SQLite", 0, 0, 0, 0, 48, 0, 6, 54 +"/home/lars/Projects/music-downloader/src/metadata/download.py", "Python", 211, 0, 0, 0, 0, 33, 60, 304 +"/home/lars/Projects/music-downloader/src/metadata/metadata.py", "Python", 106, 0, 0, 0, 0, 9, 28, 143 +"/home/lars/Projects/music-downloader/src/metadata/object_handeling.py", "Python", 19, 0, 0, 0, 0, 0, 4, 23 +"/home/lars/Projects/music-downloader/src/metadata/options.py", "Python", 87, 0, 0, 0, 0, 8, 24, 119 +"/home/lars/Projects/music-downloader/src/musify.py", "Python", 97, 0, 0, 0, 0, 2, 37, 136 +"/home/lars/Projects/music-downloader/src/phonetic_compares.py", "Python", 15, 0, 0, 0, 0, 0, 8, 23 +"/home/lars/Projects/music-downloader/src/test.py", "Python", 18, 0, 0, 0, 0, 1, 6, 25 +"/home/lars/Projects/music-downloader/src/url_to_path.py", "Python", 35, 0, 0, 0, 0, 6, 16, 57 +"/home/lars/Projects/music-downloader/src/youtube_music.py", "Python", 63, 0, 0, 0, 0, 2, 17, 82 +"Total", "-", 955, 46, 75, 8, 48, 109, 340, 1581 \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/results.json b/.VSCodeCounter/2022-10-28_12-39-22/results.json new file mode 100644 index 0000000..87e391e --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/results.json @@ -0,0 +1 @@ +{"file:///home/lars/Projects/music-downloader/src/main.py":{"language":"Python","code":78,"comment":0,"blank":26},"file:///home/lars/Projects/music-downloader/src/download.py":{"language":"Python","code":59,"comment":10,"blank":21},"file:///home/lars/Projects/music-downloader/src/download_links.py":{"language":"Python","code":46,"comment":3,"blank":18},"file:///home/lars/Projects/music-downloader/.idea/dataSources.xml":{"language":"XML","code":12,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/src/youtube_music.py":{"language":"Python","code":63,"comment":2,"blank":17},"file:///home/lars/Projects/music-downloader/src/phonetic_compares.py":{"language":"Python","code":15,"comment":0,"blank":8},"file:///home/lars/Projects/music-downloader/src/musify.py":{"language":"Python","code":97,"comment":2,"blank":37},"file:///home/lars/Projects/music-downloader/src/url_to_path.py":{"language":"Python","code":35,"comment":6,"blank":16},"file:///home/lars/Projects/music-downloader/src/test.py":{"language":"Python","code":18,"comment":1,"blank":6},"file:///home/lars/Projects/music-downloader/README.md":{"language":"Markdown","code":75,"comment":0,"blank":34},"file:///home/lars/Projects/music-downloader/requirements.txt":{"language":"pip requirements","code":8,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/src/metadata/object_handeling.py":{"language":"Python","code":19,"comment":0,"blank":4},"file:///home/lars/Projects/music-downloader/src/metadata/database.py":{"language":"Python","code":121,"comment":35,"blank":35},"file:///home/lars/Projects/music-downloader/src/metadata/metadata.py":{"language":"Python","code":106,"comment":9,"blank":28},"file:///home/lars/Projects/music-downloader/src/metadata/download.py":{"language":"Python","code":211,"comment":33,"blank":60},"file:///home/lars/Projects/music-downloader/.idea/modules.xml":{"language":"XML","code":8,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.idea/music-downloader.iml":{"language":"XML","code":10,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/src/metadata/options.py":{"language":"Python","code":87,"comment":8,"blank":24},"file:///home/lars/Projects/music-downloader/src/metadata/database_structure.sql":{"language":"SQLite","code":48,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml":{"language":"XML","code":6,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.idea/vcs.xml":{"language":"XML","code":6,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.idea/misc.xml":{"language":"XML","code":4,"comment":0,"blank":0}} \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/results.md b/.VSCodeCounter/2022-10-28_12-39-22/results.md new file mode 100644 index 0000000..d1d74f1 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/results.md @@ -0,0 +1,29 @@ +# Summary + +Date : 2022-10-28 12:39:22 + +Directory /home/lars/Projects/music-downloader + +Total : 22 files, 1132 codes, 109 comments, 340 blanks, all 1581 lines + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| Python | 13 | 955 | 109 | 300 | 1,364 | +| Markdown | 1 | 75 | 0 | 34 | 109 | +| SQLite | 1 | 48 | 0 | 6 | 54 | +| XML | 6 | 46 | 0 | 0 | 46 | +| pip requirements | 1 | 8 | 0 | 0 | 8 | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| . | 22 | 1,132 | 109 | 340 | 1,581 | +| .idea | 6 | 46 | 0 | 0 | 46 | +| .idea/inspectionProfiles | 1 | 6 | 0 | 0 | 6 | +| src | 14 | 1,003 | 109 | 306 | 1,418 | +| src/metadata | 6 | 592 | 85 | 157 | 834 | + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-10-28_12-39-22/results.txt b/.VSCodeCounter/2022-10-28_12-39-22/results.txt new file mode 100644 index 0000000..4ec0c84 --- /dev/null +++ b/.VSCodeCounter/2022-10-28_12-39-22/results.txt @@ -0,0 +1,54 @@ +Date : 2022-10-28 12:39:22 +Directory : /home/lars/Projects/music-downloader +Total : 22 files, 1132 codes, 109 comments, 340 blanks, all 1581 lines + +Languages ++------------------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++------------------+------------+------------+------------+------------+------------+ +| Python | 13 | 955 | 109 | 300 | 1,364 | +| Markdown | 1 | 75 | 0 | 34 | 109 | +| SQLite | 1 | 48 | 0 | 6 | 54 | +| XML | 6 | 46 | 0 | 0 | 46 | +| pip requirements | 1 | 8 | 0 | 0 | 8 | ++------------------+------------+------------+------------+------------+------------+ + +Directories ++-------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++-------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| . | 22 | 1,132 | 109 | 340 | 1,581 | +| .idea | 6 | 46 | 0 | 0 | 46 | +| .idea/inspectionProfiles | 1 | 6 | 0 | 0 | 6 | +| src | 14 | 1,003 | 109 | 306 | 1,418 | +| src/metadata | 6 | 592 | 85 | 157 | 834 | ++-------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ + +Files ++-------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++-------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ +| /home/lars/Projects/music-downloader/.idea/dataSources.xml | XML | 12 | 0 | 0 | 12 | +| /home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml | XML | 6 | 0 | 0 | 6 | +| /home/lars/Projects/music-downloader/.idea/misc.xml | XML | 4 | 0 | 0 | 4 | +| /home/lars/Projects/music-downloader/.idea/modules.xml | XML | 8 | 0 | 0 | 8 | +| /home/lars/Projects/music-downloader/.idea/music-downloader.iml | XML | 10 | 0 | 0 | 10 | +| /home/lars/Projects/music-downloader/.idea/vcs.xml | XML | 6 | 0 | 0 | 6 | +| /home/lars/Projects/music-downloader/README.md | Markdown | 75 | 0 | 34 | 109 | +| /home/lars/Projects/music-downloader/requirements.txt | pip requirements | 8 | 0 | 0 | 8 | +| /home/lars/Projects/music-downloader/src/download.py | Python | 59 | 10 | 21 | 90 | +| /home/lars/Projects/music-downloader/src/download_links.py | Python | 46 | 3 | 18 | 67 | +| /home/lars/Projects/music-downloader/src/main.py | Python | 78 | 0 | 26 | 104 | +| /home/lars/Projects/music-downloader/src/metadata/database.py | Python | 121 | 35 | 35 | 191 | +| /home/lars/Projects/music-downloader/src/metadata/database_structure.sql | SQLite | 48 | 0 | 6 | 54 | +| /home/lars/Projects/music-downloader/src/metadata/download.py | Python | 211 | 33 | 60 | 304 | +| /home/lars/Projects/music-downloader/src/metadata/metadata.py | Python | 106 | 9 | 28 | 143 | +| /home/lars/Projects/music-downloader/src/metadata/object_handeling.py | Python | 19 | 0 | 4 | 23 | +| /home/lars/Projects/music-downloader/src/metadata/options.py | Python | 87 | 8 | 24 | 119 | +| /home/lars/Projects/music-downloader/src/musify.py | Python | 97 | 2 | 37 | 136 | +| /home/lars/Projects/music-downloader/src/phonetic_compares.py | Python | 15 | 0 | 8 | 23 | +| /home/lars/Projects/music-downloader/src/test.py | Python | 18 | 1 | 6 | 25 | +| /home/lars/Projects/music-downloader/src/url_to_path.py | Python | 35 | 6 | 16 | 57 | +| /home/lars/Projects/music-downloader/src/youtube_music.py | Python | 63 | 2 | 17 | 82 | +| Total | | 1,132 | 109 | 340 | 1,581 | ++-------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ \ No newline at end of file From b808595d1f26eb68ed65eda9d6248f2f8b11dd0a Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 30 Oct 2022 16:29:42 +0100 Subject: [PATCH 11/35] rewrote sqlite querry to return directly a json object --- src/metadata/database.py | 58 +++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/metadata/database.py b/src/metadata/database.py index c4ab77c..10af121 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -1,6 +1,7 @@ import sqlite3 import os import logging +import json def get_temp_dir(): @@ -13,14 +14,14 @@ def get_temp_dir(): return temp_dir -# DATABASE_STRUCTURE_FILE = "database_structure.sql" -DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" +DATABASE_STRUCTURE_FILE = "database_structure.sql" +# DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" TEMP_DIR = get_temp_dir() DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) connection = sqlite3.connect(db_path) -connection.row_factory = sqlite3.Row +# connection.row_factory = sqlite3.Row cursor = connection.cursor() @@ -125,6 +126,7 @@ def add_track( cursor.execute(query, values) connection.commit() + def get_custom_track_querry(custom_where: list) -> str: where_args = [ "track.release_id == release_.id", @@ -134,34 +136,37 @@ def get_custom_track_querry(custom_where: list) -> str: where_arg = " AND ".join(where_args) query = f""" -SELECT DISTINCT - track.id as musicbrainz_releasetrackid, - release_.id as musicbrainz_albumid, - release_group.id as musicbrainz_releasegroupid, - track.track as track, - track.isrc as isrc, - release_.title as title, - release_.copyright as copyright, - release_.album_status as album_status, - release_.language as language, - release_.year as year, - release_.date as date, - release_.country as country, - release_.barcode as barcode, - release_group.albumartist as albumartist, - release_group.albumsort as albumsort, - release_group.musicbrainz_albumtype as musicbrainz_albumtype, - release_group.compilation as compilation, - release_group.album_artist_id as album_artist_id +SELECT DISTINCT + json_object( + 'musicbrainz_releasetrackid', track.id, + 'musicbrainz_albumid', release_.id, + 'track', track.track, + 'isrc', track.isrc, + 'title', release_.title, + 'copyright', release_.copyright, + 'album_status', release_.album_status, + 'language', release_.language, + 'year', release_.year, + 'date', release_.date, + 'country', release_.country, + 'barcode', release_.barcode, + 'albumartist', release_group.albumartist, + 'albumsort', release_group.albumsort, + 'musicbrainz_albumtype', release_group.musicbrainz_albumtype, + 'compilation', release_group.compilation, + 'album_artist_id', release_group.album_artist_id + ) FROM track, release_, release_group, artist WHERE {where_arg}; """ return query + def get_custom_track(custom_where: list): query = get_custom_track_querry(custom_where=custom_where) - return [dict(i) for i in cursor.execute(query)] + return [json.loads(i[0]) for i in cursor.execute(query)] + def get_track_metadata(musicbrainz_releasetrackid: str): # this would be vulnerable if musicbrainz_releasetrackid would be user input @@ -169,14 +174,17 @@ def get_track_metadata(musicbrainz_releasetrackid: str): if len(resulting_tracks) != 1: return -1 - return dict(resulting_tracks[0]) + return resulting_tracks[0] + def get_tracks_to_download(): return get_custom_track(["track.downloaded == 0"]) + def get_tracks_without_isrc(): return get_custom_track(["track.isrc IS NULL"]) + init_db(cursor=cursor, connection=connection, reset_anyways=False) if __name__ == "__main__": @@ -187,4 +195,4 @@ if __name__ == "__main__": for track in get_tracks_without_isrc(): print(track['track']) - #print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) + # print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) From 6f2ecded4c5fb22dac3ff553caa97b2efd5a194c Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 30 Oct 2022 17:07:13 +0100 Subject: [PATCH 12/35] added functionality to retrive artist information --- src/metadata/database.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/metadata/database.py b/src/metadata/database.py index 10af121..c101ded 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -130,7 +130,9 @@ def add_track( def get_custom_track_querry(custom_where: list) -> str: where_args = [ "track.release_id == release_.id", - "release_group.id == release_.release_group_id" + "release_group.id == release_.release_group_id", + "artist_track.artist_id == artist.id", + "artist_track.track_id == track.id" ] where_args.extend(custom_where) @@ -138,6 +140,14 @@ def get_custom_track_querry(custom_where: list) -> str: query = f""" SELECT DISTINCT json_object( + 'artists', json_group_array( + ( + SELECT DISTINCT json_object( + 'id', artist.id, + 'name', artist.name + ) + ) + ), 'musicbrainz_releasetrackid', track.id, 'musicbrainz_albumid', release_.id, 'track', track.track, @@ -156,9 +166,10 @@ SELECT DISTINCT 'compilation', release_group.compilation, 'album_artist_id', release_group.album_artist_id ) -FROM track, release_, release_group, artist +FROM track, release_, release_group,artist, artist_track WHERE - {where_arg}; + {where_arg} +GROUP BY track.id; """ return query @@ -193,6 +204,6 @@ if __name__ == "__main__": # get_track(["track.downloaded == 0", "track.isrc IS NOT NULL"]) # for track in get_tracks_without_isrc(): - print(track['track']) + print(track['track'], [artist['name'] for artist in track['artists']]) # print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) From f16033b584058fe666c53ad3aabf1edce2012132 Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 30 Oct 2022 20:30:59 +0100 Subject: [PATCH 13/35] implemented the file and folder paths in the db --- src/download_links.py | 1 - src/metadata/database.py | 47 ++++++++++++++++++++++++----- src/metadata/database_structure.sql | 4 ++- src/url_to_path.py | 20 +++--------- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/download_links.py b/src/download_links.py index 22baec6..1e196d2 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -1,6 +1,5 @@ import json import os.path -import pandas as pd import requests import logging diff --git a/src/metadata/database.py b/src/metadata/database.py index c101ded..9c42439 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -148,11 +148,12 @@ SELECT DISTINCT ) ) ), + 'id', track.id, 'musicbrainz_releasetrackid', track.id, 'musicbrainz_albumid', release_.id, - 'track', track.track, + 'title', track.track, 'isrc', track.isrc, - 'title', release_.title, + 'album', release_.title, 'copyright', release_.copyright, 'album_status', release_.album_status, 'language', release_.language, @@ -164,7 +165,12 @@ SELECT DISTINCT 'albumsort', release_group.albumsort, 'musicbrainz_albumtype', release_group.musicbrainz_albumtype, 'compilation', release_group.compilation, - 'album_artist_id', release_group.album_artist_id + 'album_artist_id', release_group.album_artist_id, + 'path', track.path, + 'file', track.file, + 'genre', track.genre, + 'url', track.url, + 'src', track.src ) FROM track, release_, release_group,artist, artist_track WHERE @@ -196,14 +202,41 @@ def get_tracks_without_isrc(): return get_custom_track(["track.isrc IS NULL"]) +def get_tracks_without_filepath(): + return get_custom_track(["(track.file IS NULL OR track.path IS NULL OR track.genre IS NULL)"]) + + +def update_download_status(track_id: str): + pass + + +def set_download_data(track_id: str, url: str, src: str): + query = f""" +UPDATE track +SET url = ?, + src = ? +WHERE '{track_id}' == id; + """ + cursor.execute(query, (url, src)) + connection.commit() + + +def set_filepath(track_id: str, file: str, path: str, genre: str): + query = f""" +UPDATE track +SET file = ?, + path = ?, + genre = ? +WHERE '{track_id}' == id; + """ + cursor.execute(query, (file, path, genre)) + connection.commit() + + init_db(cursor=cursor, connection=connection, reset_anyways=False) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) - # get_track(["track.downloaded == 0", "track.isrc IS NOT NULL"]) - # for track in get_tracks_without_isrc(): print(track['track'], [artist['name'] for artist in track['artists']]) - - # print(get_track_metadata("a85d5ed5-20e5-4f95-8034-d204d81a36dd")) diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql index 9f7fc29..d4d0e9f 100644 --- a/src/metadata/database_structure.sql +++ b/src/metadata/database_structure.sql @@ -47,7 +47,9 @@ CREATE TABLE track ( release_id TEXT NOT NULL, track TEXT, isrc TEXT, - filepath TEXT, + genre TEXT, + path TEXT, + file TEXT, url TEXT, src TEXT ); diff --git a/src/url_to_path.py b/src/url_to_path.py index 301741c..3862df5 100644 --- a/src/url_to_path.py +++ b/src/url_to_path.py @@ -1,29 +1,19 @@ import os.path -import shlex -import pandas as pd import json +from metadata import database + class UrlPath: def __init__(self, genre: str, temp: str = "temp", file: str = ".cache3.csv", step_two_file: str = ".cache2.csv"): self.temp = temp self.file = file - self.metadata = pd.read_csv(os.path.join(self.temp, step_two_file), index_col=0) self.genre = genre - new_metadata = [] - - for idx, row in self.metadata.iterrows(): + for row in database.get_tracks_without_filepath(): file, path = self.get_path_from_row(row) - new_row = dict(row) - new_row['path'] = path - new_row['file'] = file - new_row['genre'] = self.genre - new_metadata.append(new_row) - - new_df = pd.DataFrame(new_metadata) - new_df.to_csv(os.path.join(self.temp, self.file)) + database.set_filepath(row['id'], file, path, genre) def get_path_from_row(self, row): @@ -45,7 +35,7 @@ class UrlPath: return self.escape_part(row['album']) def get_artist(self, row): - artists = json.loads(row['artist'].replace("'", '"')) + artists = [artist['name'] for artist in row['artists']] return self.escape_part(artists[0]) def get_song(self, row): From 9f05d4302249c8807f6fdd377a7036158fae141e Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 31 Oct 2022 00:56:32 +0100 Subject: [PATCH 14/35] implemented the file and folder paths in the db --- src/download_links.py | 36 ++++++++++++------------------------ src/metadata/database.py | 3 +++ src/musify.py | 8 ++++---- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/download_links.py b/src/download_links.py index 1e196d2..d9fceb8 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -1,57 +1,45 @@ -import json -import os.path import requests import logging import musify import youtube_music +from metadata import database class Download: - def __init__(self, metadata_csv: str = ".cache1.csv", proxies: dict = None, - file: str = ".cache2.csv", temp: str = "temp") -> None: + def __init__(self, metadata_csv: str = ".cache1.csv", proxies: dict = None) -> None: if proxies is not None: musify.set_proxy(proxies) - self.temp = temp - self.metadata = pd.read_csv(os.path.join(self.temp, metadata_csv), index_col=0) - self.urls = [] - for idx, row in self.metadata.iterrows(): - row['artist'] = json.loads(row['artist'].replace("'", '"')) + for row in database.get_tracks_to_download(): + row['artists'] = [artist['name'] for artist in row['artists']] + + id_ = row['id'] # check musify musify_url = musify.get_musify_url(row) if musify_url is not None: - self.add_url(musify_url, 'musify', dict(row)) + self.add_url(musify_url, 'musify', id_) continue # check YouTube youtube_url = youtube_music.get_youtube_url(row) if youtube_url is not None: - self.add_url(youtube_url, 'youtube', dict(row)) + self.add_url(youtube_url, 'youtube', id_) continue # check musify again, but with a different methode that takes longer musify_url = musify.get_musify_url_slow(row) if musify_url is not None: - self.add_url(musify_url, 'musify', dict(row)) + self.add_url(musify_url, 'musify', id_) continue logging.warning(f"Didn't find any sources for {row['title']}") - self.dump_urls(file) - - def add_url(self, url: str, src: str, row: dict): - row['url'] = url - row['src'] = src - - self.urls.append(row) - - def dump_urls(self, file: str = ".cache2.csv"): - df = pd.DataFrame(self.urls) - df.to_csv(os.path.join(self.temp, file)) + def add_url(self, url: str, src: str, id_: str): + database.set_download_data(id_, url, src) if __name__ == "__main__": @@ -62,4 +50,4 @@ if __name__ == "__main__": s = requests.Session() s.proxies = proxies - download = Download(session=s) + download = Download() diff --git a/src/metadata/database.py b/src/metadata/database.py index 9c42439..2063ffa 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -198,6 +198,9 @@ def get_tracks_to_download(): return get_custom_track(["track.downloaded == 0"]) +def get_tracks_without_src(): + return get_custom_track(["(track.url IS NULL OR track.src IS NULL)"]) + def get_tracks_without_isrc(): return get_custom_track(["track.isrc IS NULL"]) diff --git a/src/musify.py b/src/musify.py index 5a652e1..69d534a 100644 --- a/src/musify.py +++ b/src/musify.py @@ -16,8 +16,8 @@ def set_proxy(proxies): def get_musify_url(row): - title = row.title - artists = row.artist + title = row['title'] + artists = row['artists'] url = f"https://musify.club/search/suggestions?term={artists[0]} - {title}" @@ -74,8 +74,8 @@ def get_soup_of_search(query: str): def search_for_track(row): - track = row.title - artist = row.artist + track = row['title'] + artist = row['artists'] soup = get_soup_of_search(f"{artist[0]} - {track}") tracklist_container_soup = soup.find_all("div", {"class": "playlist"}) From a59244e82d5133a90787680885800c88f0660ed1 Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 3 Nov 2022 17:05:52 +0100 Subject: [PATCH 15/35] implemented the prev changes in the downloader --- src/download.py | 25 ++++++++++++------------- src/main.py | 14 ++++++++------ src/metadata/database.py | 7 ++++--- src/metadata/download.py | 4 ++-- src/metadata/metadata.py | 8 ++++---- src/metadata/options.py | 14 ++++++++------ 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/download.py b/src/download.py index 342c945..126c900 100644 --- a/src/download.py +++ b/src/download.py @@ -1,12 +1,11 @@ import mutagen.id3 import requests import os.path -import pandas as pd from mutagen.easyid3 import EasyID3 from pydub import AudioSegment -import json import logging +from metadata import database import musify import youtube_music @@ -21,6 +20,10 @@ print(EasyID3.valid_keys.keys()) def write_metadata(row, file_path): + if not os.path.exists(file_path): + logging.warning("something went really wrong") + return False + # only convert the file to the proper format if mutagen doesn't work with it due to time try: audiofile = EasyID3(file_path) @@ -31,10 +34,12 @@ def write_metadata(row, file_path): valid_keys = list(EasyID3.valid_keys.keys()) for key in list(row.keys()): - if type(row[key]) == list or key in valid_keys and not pd.isna(row[key]): - if type(row[key]) == int or type(row[key]) == float: + if key in valid_keys and row[key] is not None: + if type(row[key]) != list: row[key] = str(row[key]) audiofile[key] = row[key] + else: + logging.warning(key) logging.info("saving") audiofile.save(file_path, v1=2) @@ -50,18 +55,12 @@ def path_stuff(path: str, file_: str): class Download: - def __init__(self, proxies: dict = None, file: str = ".cache3.csv", temp: str = "temp", - base_path: str = ""): + def __init__(self, proxies: dict = None, base_path: str = ""): if proxies is not None: musify.set_proxy(proxies) - self.temp = temp - self.file = file - - self.dataframe = pd.read_csv(os.path.join(self.temp, self.file), index_col=0) - - for idx, row in self.dataframe.iterrows(): - row['artist'] = json.loads(row['artist'].replace("'", '"')) + for row in database.get_tracks_to_download(): + row['artist'] = [i['name'] for i in row['artists']] row['file'] = os.path.join(base_path, row['file']) row['path'] = os.path.join(base_path, row['path']) diff --git a/src/main.py b/src/main.py index 507beab..edc800a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,4 @@ +import metadata.download import metadata.metadata import download_links import url_to_path @@ -14,7 +15,7 @@ STEP_THREE_CACHE = ".cache3.csv" NOT_A_GENRE = ".", "..", "misc_scripts", "Music", "script", ".git", ".idea" MUSIC_DIR = os.path.expanduser('~/Music') -TOR = True +TOR = False logging.basicConfig(level=logging.INFO) @@ -39,7 +40,7 @@ def search_for_metadata(query: str): if input_ == "q": exit(0) if input_ == "ok": - return search + return search.current_chosen_option if input_ == ".": print(search.options) continue @@ -84,20 +85,21 @@ def cli(start_at: int = 0): if start_at <= 0: search = search_for_metadata(query=input("initial query: ")) logging.info("Starting Downloading of metadata") - search.download(file=STEP_ONE_CACHE) + metadata.download.download(search) if start_at <= 1: logging.info("Fetching Download Links") - download_links.Download(file=STEP_TWO_CACHE, metadata_csv=STEP_ONE_CACHE, temp=TEMP, proxies=proxies) + download_links.Download(proxies=proxies) if start_at <= 2: logging.info("creating Paths") + print(genre) url_to_path.UrlPath(genre=genre) if start_at <= 3: logging.info("starting to download the mp3's") - download.Download(proxies=proxies, file=STEP_THREE_CACHE, temp=TEMP, base_path=MUSIC_DIR) + download.Download(proxies=proxies, base_path=MUSIC_DIR) if __name__ == "__main__": - cli(start_at=0) + cli(start_at=2) diff --git a/src/metadata/database.py b/src/metadata/database.py index 2063ffa..f669999 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -14,8 +14,8 @@ def get_temp_dir(): return temp_dir -DATABASE_STRUCTURE_FILE = "database_structure.sql" -# DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" +# DATABASE_STRUCTURE_FILE = "database_structure.sql" +DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" TEMP_DIR = get_temp_dir() DATABASE_FILE = "metadata.db" db_path = os.path.join(TEMP_DIR, DATABASE_FILE) @@ -195,12 +195,13 @@ def get_track_metadata(musicbrainz_releasetrackid: str): def get_tracks_to_download(): - return get_custom_track(["track.downloaded == 0"]) + return get_custom_track(['track.downloaded == 0']) def get_tracks_without_src(): return get_custom_track(["(track.url IS NULL OR track.src IS NULL)"]) + def get_tracks_without_isrc(): return get_custom_track(["track.isrc IS NULL"]) diff --git a/src/metadata/download.py b/src/metadata/download.py index 64ea8a7..24b1dde 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -3,8 +3,8 @@ import musicbrainzngs import pandas as pd import logging -from object_handeling import get_elem_from_obj, parse_music_brainz_date -import database +from metadata.object_handeling import get_elem_from_obj, parse_music_brainz_date +from metadata import database # I don't know if it would be feesable to set up my own mb instance # https://github.com/metabrainz/musicbrainz-docker diff --git a/src/metadata/metadata.py b/src/metadata/metadata.py index 25c6680..5567688 100644 --- a/src/metadata/metadata.py +++ b/src/metadata/metadata.py @@ -1,8 +1,7 @@ import logging import musicbrainzngs -import options -from object_handeling import get_elem_from_obj +from metadata import options mb_log = logging.getLogger("musicbrainzngs") mb_log.setLevel(logging.WARNING) @@ -58,12 +57,13 @@ class Search: if not self.current_options.choose(index): return self.current_options - self.current_chosen_option = self.current_options.get_current_option() - kind = self.current_chosen_option['kind'] + self.current_chosen_option = self.current_options.get_current_option(komplex=True) + kind = self.current_chosen_option['type'] if kind == 'artist': return self.browse_artist(self.current_chosen_option, limit=limit) if kind == 'release': release_limit = limit if not ignore_limit_for_tracklist else 100 + release_limit = 100 return self.browse_release(self.current_chosen_option, limit=release_limit) if kind == 'track': track_limit = limit if not ignore_limit_for_tracklist else 100 diff --git a/src/metadata/options.py b/src/metadata/options.py index 07ae634..4a385cf 100644 --- a/src/metadata/options.py +++ b/src/metadata/options.py @@ -24,7 +24,7 @@ def get_string_for_tracks(tracks: dict) -> str: def get_string_for_option(option: dict) -> str: - kind = option['kind'] + kind = option['type'] if kind == "artist": return get_string_for_artist(option) if kind == "release": @@ -37,6 +37,7 @@ def get_string_for_option(option: dict) -> str: class Options: def __init__(self, results: list): self.results = results + print(results) self.artist_count = 0 self.release_count = 0 @@ -56,7 +57,7 @@ class Options: komplex_information = self.result_list[self.current_option_ind] return { 'id': komplex_information['id'], - 'type': komplex_information['kind'] + 'type': komplex_information['type'] } def choose(self, index: int) -> bool: @@ -68,7 +69,8 @@ class Options: def __str__(self) -> str: string = f"artists: {self.artist_count}; releases {self.release_count}; tracks {self.track_count}\n" for i, option in enumerate(self.result_list): - string += f"{i})\t{option['kind']}:\t" + get_string_for_option(option) + print(option) + string += f"{i})\t{option['type']}:\t" + get_string_for_option(option) return string def set_options_values(self): @@ -86,19 +88,19 @@ class Options: def set_artist_values(self, option_set: dict): self.artist_count += option_set['artist-count'] for artist in option_set['artist-list']: - artist['kind'] = "artist" + artist['type'] = "artist" self.result_list.append(artist) def set_release_values(self, option_set: dict): self.release_count += option_set['release-count'] for release in option_set['release-list']: - release['kind'] = "release" + release['type'] = "release" self.result_list.append(release) def set_track_values(self, option_set: dict): self.track_count += option_set['recording-count'] for track in option_set['recording-list']: - track['kind'] = "track" + track['type'] = "track" self.result_list.append(track) """ From c37fa68937fce82e09ab8067a028503b17f2c4d0 Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 6 Nov 2022 18:10:00 +0100 Subject: [PATCH 16/35] refactor --- src/main.py | 5 +- src/metadata/database.py | 320 ++++++++-------- src/metadata/database_structure.sql | 55 --- src/metadata/download.py | 546 +++++++++++++++------------- 4 files changed, 446 insertions(+), 480 deletions(-) delete mode 100644 src/metadata/database_structure.sql diff --git a/src/main.py b/src/main.py index edc800a..6cbdc20 100644 --- a/src/main.py +++ b/src/main.py @@ -17,7 +17,8 @@ NOT_A_GENRE = ".", "..", "misc_scripts", "Music", "script", ".git", ".idea" MUSIC_DIR = os.path.expanduser('~/Music') TOR = False -logging.basicConfig(level=logging.INFO) +logger = logging.getLogger() +logger.level = logging.DEBUG def get_existing_genre(): @@ -102,4 +103,4 @@ def cli(start_at: int = 0): if __name__ == "__main__": - cli(start_at=2) + cli(start_at=0) diff --git a/src/metadata/database.py b/src/metadata/database.py index f669999..3a0ce5e 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -4,140 +4,130 @@ import logging import json -def get_temp_dir(): - import tempfile +class Database: + def __init__(self, path_to_db: str, db_structure: str, logger: logging.Logger, reset_anyways: bool = False): + self.logger = logger + self.path_to_db = path_to_db - temp_folder = "music-downloader" - temp_dir = os.path.join(tempfile.gettempdir(), temp_folder) - if not os.path.exists(temp_dir): - os.mkdir(temp_dir) - return temp_dir + self.connection = sqlite3.connect(self.path_to_db) + self.cursor = self.connection.cursor() + # init database + self.init_db(database_structure=db_structure, reset_anyways=reset_anyways) -# DATABASE_STRUCTURE_FILE = "database_structure.sql" -DATABASE_STRUCTURE_FILE = "src/metadata/database_structure.sql" -TEMP_DIR = get_temp_dir() -DATABASE_FILE = "metadata.db" -db_path = os.path.join(TEMP_DIR, DATABASE_FILE) + def init_db(self, database_structure: str, reset_anyways: bool = False): + # check if db exists + exists = True + try: + query = 'SELECT * FROM track;' + self.cursor.execute(query) + _ = self.cursor.fetchall() + except sqlite3.OperationalError: + exists = False -connection = sqlite3.connect(db_path) -# connection.row_factory = sqlite3.Row -cursor = connection.cursor() + if not exists: + self.logger.info("Database does not exist yet.") + if reset_anyways or not exists: + # reset the database if reset_anyways is true or if an error has been thrown previously. + self.logger.info("Creating/Reseting Database.") -def init_db(cursor, connection, reset_anyways: bool = False): - # check if db exists - exists = True - try: - query = 'SELECT * FROM track;' - cursor.execute(query) - _ = cursor.fetchall() - except sqlite3.OperationalError: - exists = False + # read the file + with open(database_structure, "r") as database_structure_file: + query = database_structure_file.read() + self.cursor.executescript(query) + self.connection.commit() - if not exists: - logging.info("Database does not exist yet.") + def add_artist( + self, + musicbrainz_artistid: str, + artist: str = None + ): + query = "INSERT OR REPLACE INTO artist (id, name) VALUES (?, ?);" + values = musicbrainz_artistid, artist - if reset_anyways or not exists: - # reset the database if reset_anyways is true or if an error has been thrown previously. - logging.info("Creating/Reseting Database.") + self.cursor.execute(query, values) + self.connection.commit() - # read the file - with open(DATABASE_STRUCTURE_FILE, "r") as database_structure_file: - query = database_structure_file.read() - cursor.executescript(query) - connection.commit() + def add_release_group( + self, + musicbrainz_releasegroupid: str, + artist_ids: list, + albumartist: str = None, + albumsort: int = None, + musicbrainz_albumtype: str = None, + compilation: str = None, + album_artist_id: str = None + ): + # add adjacency + adjacency_list = [] + for artist_id in artist_ids: + adjacency_list.append((artist_id, musicbrainz_releasegroupid)) + adjacency_values = tuple(adjacency_list) + adjacency_query = "INSERT OR REPLACE INTO artist_release_group (artist_id, release_group_id) VALUES (?, ?);" + self.cursor.executemany(adjacency_query, adjacency_values) + self.connection.commit() + # add release group + query = "INSERT OR REPLACE INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id) VALUES (?, ?, ?, ?, ?, ?);" + values = musicbrainz_releasegroupid, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id + self.cursor.execute(query, values) + self.connection.commit() -def add_artist( - musicbrainz_artistid: str, - artist: str = None -): - query = "INSERT OR REPLACE INTO artist (id, name) VALUES (?, ?);" - values = musicbrainz_artistid, artist + def add_release( + self, + musicbrainz_albumid: str, + release_group_id: str, + title: str = None, + copyright_: str = None, + album_status: str = None, + language: str = None, + year: str = None, + date: str = None, + country: str = None, + barcode: str = None + ): + query = "INSERT OR REPLACE INTO release_ (id, release_group_id, title, copyright, album_status, language, year, date, country, barcode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" + values = musicbrainz_albumid, release_group_id, title, copyright_, album_status, language, year, date, country, barcode - cursor.execute(query, values) - connection.commit() + self.cursor.execute(query, values) + self.connection.commit() + def add_track( + self, + musicbrainz_releasetrackid: str, + musicbrainz_albumid: str, + feature_aritsts: list, + track: str = None, + isrc: str = None + ): + # add adjacency + adjacency_list = [] + for artist_id in feature_aritsts: + adjacency_list.append((artist_id, musicbrainz_releasetrackid)) + adjacency_values = tuple(adjacency_list) + adjacency_query = "INSERT OR REPLACE INTO artist_track (artist_id, track_id) VALUES (?, ?);" + self.cursor.executemany(adjacency_query, adjacency_values) + self.connection.commit() -def add_release_group( - musicbrainz_releasegroupid: str, - artist_ids: list, - albumartist: str = None, - albumsort: int = None, - musicbrainz_albumtype: str = None, - compilation: str = None, - album_artist_id: str = None -): - # add adjacency - adjacency_list = [] - for artist_id in artist_ids: - adjacency_list.append((artist_id, musicbrainz_releasegroupid)) - adjacency_values = tuple(adjacency_list) - adjacency_query = "INSERT OR REPLACE INTO artist_release_group (artist_id, release_group_id) VALUES (?, ?);" - cursor.executemany(adjacency_query, adjacency_values) - connection.commit() + # add track + query = "INSERT OR REPLACE INTO track (id, release_id, track, isrc) VALUES (?, ?, ?, ?);" + values = musicbrainz_releasetrackid, musicbrainz_albumid, track, isrc + self.cursor.execute(query, values) + self.connection.commit() - # add release group - query = "INSERT OR REPLACE INTO release_group (id, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id) VALUES (?, ?, ?, ?, ?, ?);" - values = musicbrainz_releasegroupid, albumartist, albumsort, musicbrainz_albumtype, compilation, album_artist_id - cursor.execute(query, values) - connection.commit() + @staticmethod + def get_custom_track_query(custom_where: list) -> str: + where_args = [ + "track.release_id == release_.id", + "release_group.id == release_.release_group_id", + "artist_track.artist_id == artist.id", + "artist_track.track_id == track.id" + ] + where_args.extend(custom_where) - -def add_release( - musicbrainz_albumid: str, - release_group_id: str, - title: str = None, - copyright_: str = None, - album_status: str = None, - language: str = None, - year: str = None, - date: str = None, - country: str = None, - barcode: str = None -): - query = "INSERT OR REPLACE INTO release_ (id, release_group_id, title, copyright, album_status, language, year, date, country, barcode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" - values = musicbrainz_albumid, release_group_id, title, copyright_, album_status, language, year, date, country, barcode - - cursor.execute(query, values) - connection.commit() - - -def add_track( - musicbrainz_releasetrackid: str, - musicbrainz_albumid: str, - feature_aritsts: list, - track: str = None, - isrc: str = None -): - # add adjacency - adjacency_list = [] - for artist_id in feature_aritsts: - adjacency_list.append((artist_id, musicbrainz_releasetrackid)) - adjacency_values = tuple(adjacency_list) - adjacency_query = "INSERT OR REPLACE INTO artist_track (artist_id, track_id) VALUES (?, ?);" - cursor.executemany(adjacency_query, adjacency_values) - connection.commit() - - # add track - query = "INSERT OR REPLACE INTO track (id, release_id, track, isrc) VALUES (?, ?, ?, ?);" - values = musicbrainz_releasetrackid, musicbrainz_albumid, track, isrc - cursor.execute(query, values) - connection.commit() - - -def get_custom_track_querry(custom_where: list) -> str: - where_args = [ - "track.release_id == release_.id", - "release_group.id == release_.release_group_id", - "artist_track.artist_id == artist.id", - "artist_track.track_id == track.id" - ] - where_args.extend(custom_where) - - where_arg = " AND ".join(where_args) - query = f""" + where_arg = " AND ".join(where_args) + query = f""" SELECT DISTINCT json_object( 'artists', json_group_array( @@ -176,71 +166,75 @@ FROM track, release_, release_group,artist, artist_track WHERE {where_arg} GROUP BY track.id; - """ - return query + """ + return query + def get_custom_track(self, custom_where: list): + query = Database.get_custom_track_query(custom_where=custom_where) + return [json.loads(i[0]) for i in self.cursor.execute(query)] -def get_custom_track(custom_where: list): - query = get_custom_track_querry(custom_where=custom_where) - return [json.loads(i[0]) for i in cursor.execute(query)] + def get_track_metadata(self, musicbrainz_releasetrackid: str): + # this would be vulnerable if musicbrainz_releasetrackid would be user input + resulting_tracks = self.get_custom_track([f'track.id == "{musicbrainz_releasetrackid}"']) + if len(resulting_tracks) != 1: + return -1 + return resulting_tracks[0] -def get_track_metadata(musicbrainz_releasetrackid: str): - # this would be vulnerable if musicbrainz_releasetrackid would be user input - resulting_tracks = get_custom_track([f'track.id == "{musicbrainz_releasetrackid}"']) - if len(resulting_tracks) != 1: - return -1 + def get_tracks_to_download(self): + return self.get_custom_track(['track.downloaded == 0']) - return resulting_tracks[0] + def get_tracks_without_src(self): + return self.get_custom_track(["(track.url IS NULL OR track.src IS NULL)"]) + def get_tracks_without_isrc(self): + return self.get_custom_track(["track.isrc IS NULL"]) -def get_tracks_to_download(): - return get_custom_track(['track.downloaded == 0']) + def get_tracks_without_filepath(self): + return self.get_custom_track(["(track.file IS NULL OR track.path IS NULL OR track.genre IS NULL)"]) + def update_download_status(self, track_id: str): + pass -def get_tracks_without_src(): - return get_custom_track(["(track.url IS NULL OR track.src IS NULL)"]) - - -def get_tracks_without_isrc(): - return get_custom_track(["track.isrc IS NULL"]) - - -def get_tracks_without_filepath(): - return get_custom_track(["(track.file IS NULL OR track.path IS NULL OR track.genre IS NULL)"]) - - -def update_download_status(track_id: str): - pass - - -def set_download_data(track_id: str, url: str, src: str): - query = f""" + def set_download_data(self, track_id: str, url: str, src: str): + query = f""" UPDATE track SET url = ?, src = ? WHERE '{track_id}' == id; - """ - cursor.execute(query, (url, src)) - connection.commit() + """ + self.cursor.execute(query, (url, src)) + self.connection.commit() - -def set_filepath(track_id: str, file: str, path: str, genre: str): - query = f""" + def set_filepath(self, track_id: str, file: str, path: str, genre: str): + query = f""" UPDATE track SET file = ?, path = ?, genre = ? WHERE '{track_id}' == id; - """ - cursor.execute(query, (file, path, genre)) - connection.commit() + """ + self.cursor.execute(query, (file, path, genre)) + self.connection.commit() -init_db(cursor=cursor, connection=connection, reset_anyways=False) - if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) + import tempfile - for track in get_tracks_without_isrc(): - print(track['track'], [artist['name'] for artist in track['artists']]) + temp_folder = "music-downloader" + temp_dir = os.path.join(tempfile.gettempdir(), temp_folder) + if not os.path.exists(temp_dir): + os.mkdir(temp_dir) + + temp_dir = get_temp_dir() + DATABASE_FILE = "metadata.db" + DATABASE_STRUCTURE_FILE = "database_structure.sql" + db_path = os.path.join(TEMP_DIR, DATABASE_FILE) + + logging.basicConfig() + + logger = logging.getLogger("database") + logger.setLevel(logging.DEBUG) + + database = Database(os.path.join(temp_dir, "metadata.db"), os.path.join(temp_dir, "database_structure.sql"), logger, + reset_anyways=True) diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql deleted file mode 100644 index d4d0e9f..0000000 --- a/src/metadata/database_structure.sql +++ /dev/null @@ -1,55 +0,0 @@ -DROP TABLE IF EXISTS artist; -CREATE TABLE artist ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT -); - -DROP TABLE IF EXISTS artist_release_group; -CREATE TABLE artist_release_group ( - artist_id TEXT NOT NULL, - release_group_id TEXT NOT NULL -); - -DROP TABLE IF EXISTS artist_track; -CREATE TABLE artist_track ( - artist_id TEXT NOT NULL, - track_id TEXT NOT NULL -); - -DROP TABLE IF EXISTS release_group; -CREATE TABLE release_group ( - id TEXT PRIMARY KEY NOT NULL, - albumartist TEXT, - albumsort INT, - musicbrainz_albumtype TEXT, - compilation TEXT, - album_artist_id TEXT -); - -DROP TABLE IF EXISTS release_; -CREATE TABLE release_ ( - id TEXT PRIMARY KEY NOT NULL, - release_group_id TEXT NOT NULL, - title TEXT, - copyright TEXT, - album_status TEXT, - language TEXT, - year TEXT, - date TEXT, - country TEXT, - barcode TEXT -); - -DROP TABLE IF EXISTS track; -CREATE TABLE track ( - id TEXT PRIMARY KEY NOT NULL, - downloaded BOOLEAN NOT NULL DEFAULT 0, - release_id TEXT NOT NULL, - track TEXT, - isrc TEXT, - genre TEXT, - path TEXT, - file TEXT, - url TEXT, - src TEXT -); diff --git a/src/metadata/download.py b/src/metadata/download.py index 24b1dde..330687f 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -1,10 +1,8 @@ from typing import List import musicbrainzngs -import pandas as pd import logging -from metadata.object_handeling import get_elem_from_obj, parse_music_brainz_date -from metadata import database +from object_handeling import get_elem_from_obj, parse_music_brainz_date # I don't know if it would be feesable to set up my own mb instance # https://github.com/metabrainz/musicbrainz-docker @@ -17,287 +15,315 @@ musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeI # IMPORTANT DOCUMENTATION WHICH CONTAINS FOR EXAMPLE THE INCLUDES # https://python-musicbrainzngs.readthedocs.io/en/v0.7.1/api/#getting-data -class Artist: - def __init__( - self, - musicbrainz_artistid: str, - release_groups: List = [], - new_release_groups: bool = True - ): - """ - release_groups: list - """ - self.release_groups = release_groups - self.musicbrainz_artistid = musicbrainz_artistid +class MetadataDownloader: + def __init__(self, database, logger: logging.Logger): + self.database = database + self.logger = logger - result = musicbrainzngs.get_artist_by_id(self.musicbrainz_artistid, includes=["release-groups", "releases"]) - artist_data = get_elem_from_obj(result, ['artist'], return_if_none={}) + class Artist: + def __init__( + self, + database, + logger, + musicbrainz_artistid: str, + release_groups: List = [], + new_release_groups: bool = True + ): + self.database = database + self.logger = logger + """ + release_groups: list + """ + self.release_groups = release_groups - self.artist = get_elem_from_obj(artist_data, ['name']) + self.musicbrainz_artistid = musicbrainz_artistid - self.save() + result = musicbrainzngs.get_artist_by_id(self.musicbrainz_artistid, includes=["release-groups", "releases"]) + artist_data = get_elem_from_obj(result, ['artist'], return_if_none={}) - # STARTING TO FETCH' RELEASE GROUPS. IMPORTANT: DON'T WRITE ANYTHING BESIDES THAT HERE - if not new_release_groups: - return - # sort all release groups by date and add album sort to have them in chronological order. - release_groups = artist_data['release-group-list'] - for i, release_group in enumerate(release_groups): - release_groups[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) - release_groups.sort(key=lambda x: x['first-release-date']) + self.artist = get_elem_from_obj(artist_data, ['name']) - for i, release_group in enumerate(release_groups): - self.release_groups.append(ReleaseGroup( - musicbrainz_releasegroupid=release_group['id'], - artists=[self], - albumsort=i + 1 - )) + self.save() - def __str__(self): - newline = "\n" - return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" + # STARTING TO FETCH' RELEASE GROUPS. IMPORTANT: DON'T WRITE ANYTHING BESIDES THAT HERE + if not new_release_groups: + return + # sort all release groups by date and add album sort to have them in chronological order. + release_groups = artist_data['release-group-list'] + for i, release_group in enumerate(release_groups): + release_groups[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) + release_groups.sort(key=lambda x: x['first-release-date']) - def save(self): - logging.info(f"artist: {self}") - database.add_artist( - musicbrainz_artistid=self.musicbrainz_artistid, - artist=self.artist - ) + for i, release_group in enumerate(release_groups): + self.release_groups.append(MetadataDownloader.ReleaseGroup( + self.database, + self.logger, + musicbrainz_releasegroupid=release_group['id'], + artists=[self], + albumsort=i + 1 + )) + def __str__(self): + newline = "\n" + return f"id: {self.musicbrainz_artistid}\nname: {self.artist}\n{newline.join([str(release_group) for release_group in self.release_groups])}" -class ReleaseGroup: - def __init__( - self, - musicbrainz_releasegroupid: str, - artists: List[Artist] = [], - albumsort: int = None, - only_download_distinct_releases: bool = True - ): - """ - split_artists: list -> if len > 1: album_artist=VariousArtists - releases: list - """ + def save(self): + self.logger.info(f"artist: {self}") + self.database.add_artist( + musicbrainz_artistid=self.musicbrainz_artistid, + artist=self.artist + ) - self.musicbrainz_releasegroupid = musicbrainz_releasegroupid - self.artists = artists - self.releases = [] + class ReleaseGroup: + def __init__( + self, + database, + logger, + musicbrainz_releasegroupid: str, + artists = [], + albumsort: int = None, + only_download_distinct_releases: bool = True + ): + self.database = database + self.logger = logger + """ + split_artists: list -> if len > 1: album_artist=VariousArtists + releases: list + """ - result = musicbrainzngs.get_release_group_by_id(musicbrainz_releasegroupid, - includes=["artist-credits", "releases"]) - release_group_data = get_elem_from_obj(result, ['release-group'], return_if_none={}) - artist_datas = get_elem_from_obj(release_group_data, ['artist-credit'], return_if_none={}) - release_datas = get_elem_from_obj(release_group_data, ['release-list'], return_if_none={}) + self.musicbrainz_releasegroupid = musicbrainz_releasegroupid + self.artists = artists + self.releases = [] - for artist_data in artist_datas: - artist_id = get_elem_from_obj(artist_data, ['artist', 'id']) + result = musicbrainzngs.get_release_group_by_id(musicbrainz_releasegroupid, + includes=["artist-credits", "releases"]) + release_group_data = get_elem_from_obj(result, ['release-group'], return_if_none={}) + artist_datas = get_elem_from_obj(release_group_data, ['artist-credit'], return_if_none={}) + release_datas = get_elem_from_obj(release_group_data, ['release-list'], return_if_none={}) + + for artist_data in artist_datas: + artist_id = get_elem_from_obj(artist_data, ['artist', 'id']) + if artist_id is None: + continue + self.append_artist(artist_id) + self.albumartist = "Various Artists" if len(self.artists) > 1 else self.artists[0].artist + self.album_artist_id = None if self.albumartist == "Various Artists" else self.artists[ + 0].musicbrainz_artistid + + self.albumsort = albumsort + self.musicbrainz_albumtype = get_elem_from_obj(release_group_data, ['primary-type']) + self.compilation = "1" if self.musicbrainz_albumtype == "Compilation" else None + + self.save() + + if only_download_distinct_releases: + self.append_distinct_releases(release_datas) + else: + self.append_all_releases(release_datas) + + def __str__(self): + newline = "\n" + return f"{newline.join([str(release_group) for release_group in self.releases])}" + + def save(self): + self.logger.info(f"caching release_group {self}") + self.database.add_release_group( + musicbrainz_releasegroupid=self.musicbrainz_releasegroupid, + artist_ids=[artist.musicbrainz_artistid for artist in self.artists], + albumartist=self.albumartist, + albumsort=self.albumsort, + musicbrainz_albumtype=self.musicbrainz_albumtype, + compilation=self.compilation, + album_artist_id=self.album_artist_id + ) + + def append_artist(self, artist_id: str): + for existing_artist in self.artists: + if artist_id == existing_artist.musicbrainz_artistid: + return existing_artist + new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) + self.artists.append(new_artist) + return new_artist + + def append_release(self, release_data: dict): + musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) + if musicbrainz_albumid is None: + return + self.releases.append(MetadataDownloader.Release(self.database, self.logger, musicbrainz_albumid, release_group=self)) + + def append_distinct_releases(self, release_datas: List[dict]): + titles = {} + + for release_data in release_datas: + title = get_elem_from_obj(release_data, ['title']) + if title is None: + continue + titles[title] = release_data + + for key in titles: + self.append_release(titles[key]) + + def append_all_releases(self, release_datas: List[dict]): + for release_data in release_datas: + self.append_release(release_data) + + class Release: + def __init__( + self, + database, + logger, + musicbrainz_albumid: str, + release_group = None + ): + self.database = database + self.logger = logger + """ + release_group: ReleaseGroup + tracks: list + """ + self.musicbrainz_albumid = musicbrainz_albumid + self.release_group = release_group + self.tracklist = [] + + result = musicbrainzngs.get_release_by_id(self.musicbrainz_albumid, includes=["recordings", "labels"]) + release_data = get_elem_from_obj(result, ['release'], return_if_none={}) + label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={}) + recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[]) + + self.title = get_elem_from_obj(release_data, ['title']) + self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) + + self.album_status = get_elem_from_obj(release_data, ['status']) + self.language = get_elem_from_obj(release_data, ['text-representation', 'language']) + self.year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) + self.date = get_elem_from_obj(release_data, ['date']) + self.country = get_elem_from_obj(release_data, ['country']) + self.barcode = get_elem_from_obj(release_data, ['barcode']) + + self.save() + self.append_recordings(recording_datas) + + def __str__(self): + return f"{self.title} ©{self.copyright} {self.album_status}" + + def save(self): + self.logger.info(f"caching release {self}") + self.database.add_release( + musicbrainz_albumid=self.musicbrainz_albumid, + release_group_id=self.release_group.musicbrainz_releasegroupid, + title=self.title, + copyright_=self.copyright, + album_status=self.album_status, + language=self.language, + year=self.year, + date=self.date, + country=self.country, + barcode=self.barcode + ) + + def append_recordings(self, recording_datas: dict): + for recording_data in recording_datas: + musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['recording', 'id']) + if musicbrainz_releasetrackid is None: + continue + + self.tracklist.append(MetadataDownloader.Track(self.database, self.logger, musicbrainz_releasetrackid, self)) + + class Track: + def __init__( + self, + database, + logger, + musicbrainz_releasetrackid: str, + release = None + ): + self.database = database + self.logger = logger + """ + release: Release + feature_artists: list + """ + + self.musicbrainz_releasetrackid = musicbrainz_releasetrackid + self.release = release + self.artists = [] + + result = musicbrainzngs.get_recording_by_id(self.musicbrainz_releasetrackid, + includes=["artists", "releases", "recording-rels", "isrcs", + "work-level-rels"]) + recording_data = result['recording'] + for artist_data in get_elem_from_obj(recording_data, ['artist-credit'], return_if_none=[]): + self.append_artist(get_elem_from_obj(artist_data, ['artist', 'id'])) + + self.isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) + self.title = recording_data['title'] + + self.save() + + def __str__(self): + return f"{self.title}: {self.isrc}" + + def save(self): + self.logger.info(f"caching track {self}") + + self.database.add_track( + musicbrainz_releasetrackid=self.musicbrainz_releasetrackid, + musicbrainz_albumid=self.release.musicbrainz_albumid, + feature_aritsts=[artist.musicbrainz_artistid for artist in self.artists], + track=self.title, + isrc=self.isrc + ) + + def append_artist(self, artist_id: str): if artist_id is None: - continue - self.append_artist(artist_id) - self.albumartist = "Various Artists" if len(self.artists) > 1 else self.artists[0].artist - self.album_artist_id = None if self.albumartist == "Various Artists" else self.artists[0].musicbrainz_artistid + return - self.albumsort = albumsort - self.musicbrainz_albumtype = get_elem_from_obj(release_group_data, ['primary-type']) - self.compilation = "1" if self.musicbrainz_albumtype == "Compilation" else None + for existing_artist in self.artists: + if artist_id == existing_artist.musicbrainz_artistid: + return existing_artist + new_artist = MetadataDownloader.Artist(self.database, self.logger, artist_id, new_release_groups=False) + self.artists.append(new_artist) + return new_artist - self.save() + def download(self, option: dict): + type_ = option['type'] + mb_id = option['id'] - if only_download_distinct_releases: - self.append_distinct_releases(release_datas) - else: - self.append_all_releases(release_datas) - - def __str__(self): - newline = "\n" - return f"{newline.join([str(release_group) for release_group in self.releases])}" - - def save(self): - logging.info(f"caching release_group {self}") - database.add_release_group( - musicbrainz_releasegroupid=self.musicbrainz_releasegroupid, - artist_ids=[artist.musicbrainz_artistid for artist in self.artists], - albumartist=self.albumartist, - albumsort=self.albumsort, - musicbrainz_albumtype=self.musicbrainz_albumtype, - compilation=self.compilation, - album_artist_id=self.album_artist_id - ) - - def append_artist(self, artist_id: str) -> Artist: - for existing_artist in self.artists: - if artist_id == existing_artist.musicbrainz_artistid: - return existing_artist - new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) - self.artists.append(new_artist) - return new_artist - - def append_release(self, release_data: dict): - musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) - if musicbrainz_albumid is None: - return - self.releases.append(Release(musicbrainz_albumid, release_group=self)) - - def append_distinct_releases(self, release_datas: List[dict]): - titles = {} - - for release_data in release_datas: - title = get_elem_from_obj(release_data, ['title']) - if title is None: - continue - titles[title] = release_data - - for key in titles: - self.append_release(titles[key]) - - def append_all_releases(self, release_datas: List[dict]): - for release_data in release_datas: - self.append_release(release_data) - - -class Release: - def __init__( - self, - musicbrainz_albumid: str, - release_group: ReleaseGroup = None - ): - """ - release_group: ReleaseGroup - tracks: list - """ - self.musicbrainz_albumid = musicbrainz_albumid - self.release_group = release_group - self.tracklist = [] - - result = musicbrainzngs.get_release_by_id(self.musicbrainz_albumid, includes=["recordings", "labels"]) - release_data = get_elem_from_obj(result, ['release'], return_if_none={}) - label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={}) - recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[]) - - self.title = get_elem_from_obj(release_data, ['title']) - self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) - - self.album_status = get_elem_from_obj(release_data, ['status']) - self.language = get_elem_from_obj(release_data, ['text-representation', 'language']) - self.year = get_elem_from_obj(release_data, ['date'], lambda x: x.split("-")[0]) - self.date = get_elem_from_obj(release_data, ['date']) - self.country = get_elem_from_obj(release_data, ['country']) - self.barcode = get_elem_from_obj(release_data, ['barcode']) - - self.save() - self.append_recordings(recording_datas) - - def __str__(self): - return f"{self.title} ©{self.copyright} {self.album_status}" - - def save(self): - logging.info(f"caching release {self}") - database.add_release( - musicbrainz_albumid=self.musicbrainz_albumid, - release_group_id=self.release_group.musicbrainz_releasegroupid, - title=self.title, - copyright_=self.copyright, - album_status=self.album_status, - language=self.language, - year=self.year, - date=self.date, - country=self.country, - barcode=self.barcode - ) - - def append_recordings(self, recording_datas: dict): - for recording_data in recording_datas: - musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['recording', 'id']) - if musicbrainz_releasetrackid is None: - continue - - self.tracklist.append(Track(musicbrainz_releasetrackid, self)) - - -class Track: - def __init__( - self, - musicbrainz_releasetrackid: str, - release: Release = None - ): - """ - release: Release - feature_artists: list - """ - - self.musicbrainz_releasetrackid = musicbrainz_releasetrackid - self.release = release - self.artists = [] - - result = musicbrainzngs.get_recording_by_id(self.musicbrainz_releasetrackid, includes=["artists", "releases", "recording-rels", "isrcs", "work-level-rels"]) - recording_data = result['recording'] - for artist_data in get_elem_from_obj(recording_data, ['artist-credit'], return_if_none=[]): - self.append_artist(get_elem_from_obj(artist_data, ['artist', 'id'])) - - self.isrc = get_elem_from_obj(recording_data, ['isrc-list', 0]) - self.title = recording_data['title'] - - self.save() - - def __str__(self): - return f"{self.title}: {self.isrc}" - - def save(self): - logging.info(f"caching track {self}") - - database.add_track( - musicbrainz_releasetrackid=self.musicbrainz_releasetrackid, - musicbrainz_albumid=self.release.musicbrainz_albumid, - feature_aritsts=[artist.musicbrainz_artistid for artist in self.artists], - track=self.title, - isrc=self.isrc - ) - - def append_artist(self, artist_id: str) -> Artist: - if artist_id is None: - return - - for existing_artist in self.artists: - if artist_id == existing_artist.musicbrainz_artistid: - return existing_artist - new_artist = Artist(artist_id, new_release_groups=False) - self.artists.append(new_artist) - return new_artist - - -def download(option: dict): - type_ = option['type'] - mb_id = option['id'] - - metadata_list = [] - if type_ == "artist": - artist = Artist(mb_id) - print(artist) - elif type_ == "release": - metadata_list = download_release(mb_id) - elif type_ == "track": - metadata_list = download_track(mb_id) - - print(metadata_list) - metadata_df = pd.DataFrame(metadata_list) - # metadata_df.to_csv(os.path.join(self.temp, file)) - return metadata_df + if type_ == "artist": + self.Artist(self.database, self.logger, mb_id) + elif type_ == "release_group": + self.ReleaseGroup(self.database, self.logger, mb_id) + elif type_ == "release": + self.Release(self.database, self.logger, mb_id) + elif type_ == "track": + self.Track(self.database, self.logger, mb_id) if __name__ == "__main__": - """ import tempfile import os - TEMP_FOLDER = "music-downloader" - TEMP_DIR = os.path.join(tempfile.gettempdir(), TEMP_FOLDER) - if not os.path.exists(TEMP_DIR): - os.mkdir(TEMP_DIR) - """ + temp_folder = "music-downloader" + temp_dir = os.path.join(tempfile.gettempdir(), temp_folder) + if not os.path.exists(temp_dir): + os.mkdir(temp_dir) - logger = logging.getLogger() - logger.setLevel(logging.INFO) + logging.basicConfig(level=logging.DEBUG) + db_logger = logging.getLogger("database") + db_logger.setLevel(logging.DEBUG) - download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) + import database + + database_ = database.Database(os.path.join(temp_dir, "metadata.db"), + os.path.join(temp_dir, "database_structure.sql"), db_logger, + reset_anyways=True) + + download_logger = logging.getLogger("metadata downloader") + download_logger.setLevel(logging.INFO) + + downloader = MetadataDownloader(database_, download_logger) + + downloader.download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) # download({'id': 'c24ed9e7-6df9-44de-8570-975f1a5a75d1', 'type': 'track'}) From ecc0c72cea5671114a52a1fdb7b60ce111d82252 Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 6 Nov 2022 23:01:03 +0100 Subject: [PATCH 17/35] bugfixes --- README.md | 3 ++ src/download.py | 80 +++++++++++++++-------------- src/download_links.py | 11 ++-- src/main.py | 39 ++++++++++---- src/metadata/database.py | 7 ++- src/metadata/database_structure.sql | 56 ++++++++++++++++++++ src/metadata/download.py | 59 ++++++++++++++++----- src/metadata/object_handeling.py | 2 + src/metadata/options.py | 2 - src/musify.py | 14 ++++- src/url_to_path.py | 19 ++++--- 11 files changed, 210 insertions(+), 82 deletions(-) create mode 100644 src/metadata/database_structure.sql diff --git a/README.md b/README.md index 26f48dc..d777837 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,6 @@ There are two bottlenecks with this approach though: **Progress** - There is a great site whith a huge isrc database [https://isrc.soundexchange.com/](https://isrc.soundexchange.com/). + + +https://slavart.gamesdrive.net/ \ No newline at end of file diff --git a/src/download.py b/src/download.py index 126c900..b53b13b 100644 --- a/src/download.py +++ b/src/download.py @@ -5,7 +5,6 @@ from mutagen.easyid3 import EasyID3 from pydub import AudioSegment import logging -from metadata import database import musify import youtube_music @@ -15,57 +14,29 @@ https://mutagen.readthedocs.io/en/latest/user/id3.html # to get all valid keys from mutagen.easyid3 import EasyID3 +print("\n".join(EasyID3.valid_keys.keys())) print(EasyID3.valid_keys.keys()) """ -def write_metadata(row, file_path): - if not os.path.exists(file_path): - logging.warning("something went really wrong") - return False - # only convert the file to the proper format if mutagen doesn't work with it due to time - try: - audiofile = EasyID3(file_path) - except mutagen.id3.ID3NoHeaderError: - AudioSegment.from_file(file_path).export(file_path, format="mp3") - audiofile = EasyID3(file_path) - - valid_keys = list(EasyID3.valid_keys.keys()) - - for key in list(row.keys()): - if key in valid_keys and row[key] is not None: - if type(row[key]) != list: - row[key] = str(row[key]) - audiofile[key] = row[key] - else: - logging.warning(key) - - logging.info("saving") - audiofile.save(file_path, v1=2) - - -def path_stuff(path: str, file_: str): - # returns true if it shouldn't be downloaded - if os.path.exists(file_): - logging.info(f"'{file_}' does already exist, thus not downloading.") - return True - os.makedirs(path, exist_ok=True) - return False class Download: - def __init__(self, proxies: dict = None, base_path: str = ""): + def __init__(self, database, logger: logging.Logger, proxies: dict = None, base_path: str = ""): if proxies is not None: musify.set_proxy(proxies) + self.database = database + self.logger = logger + for row in database.get_tracks_to_download(): row['artist'] = [i['name'] for i in row['artists']] row['file'] = os.path.join(base_path, row['file']) row['path'] = os.path.join(base_path, row['path']) - if path_stuff(row['path'], row['file']): - write_metadata(row, row['file']) + if self.path_stuff(row['path'], row['file']): + self.write_metadata(row, row['file']) continue download_success = None @@ -76,10 +47,43 @@ class Download: download_success = youtube_music.download(row) if download_success == -1: - logging.warning(f"couldn't download {row.url} from {row.src}") + self.logger.warning(f"couldn't download {row['url']} from {row['src']}") continue - write_metadata(row, row['file']) + self.write_metadata(row, row['file']) + + def write_metadata(self, row, file_path): + if not os.path.exists(file_path): + self.logger.warning("something went really wrong") + return False + + # only convert the file to the proper format if mutagen doesn't work with it due to time + try: + audiofile = EasyID3(file_path) + except mutagen.id3.ID3NoHeaderError: + AudioSegment.from_file(file_path).export(file_path, format="mp3") + audiofile = EasyID3(file_path) + + valid_keys = list(EasyID3.valid_keys.keys()) + + for key in list(row.keys()): + if key in valid_keys and row[key] is not None: + if type(row[key]) != list: + row[key] = str(row[key]) + audiofile[key] = row[key] + else: + self.logger.warning(key) + + self.logger.info("saving") + audiofile.save(file_path, v1=2) + + def path_stuff(self, path: str, file_: str): + # returns true if it shouldn't be downloaded + if os.path.exists(file_): + self.logger.info(f"'{file_}' does already exist, thus not downloading.") + return True + os.makedirs(path, exist_ok=True) + return False if __name__ == "__main__": diff --git a/src/download_links.py b/src/download_links.py index d9fceb8..16a8c0d 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -3,17 +3,18 @@ import logging import musify import youtube_music -from metadata import database class Download: - def __init__(self, metadata_csv: str = ".cache1.csv", proxies: dict = None) -> None: + def __init__(self, database, logger: logging.Logger, proxies: dict = None) -> None: + self.database = database + self.logger = logger if proxies is not None: musify.set_proxy(proxies) self.urls = [] - for row in database.get_tracks_to_download(): + for row in self.database.get_tracks_to_download(): row['artists'] = [artist['name'] for artist in row['artists']] id_ = row['id'] @@ -36,10 +37,10 @@ class Download: self.add_url(musify_url, 'musify', id_) continue - logging.warning(f"Didn't find any sources for {row['title']}") + self.logger.warning(f"Didn't find any sources for {row['title']}") def add_url(self, url: str, src: str, id_: str): - database.set_download_data(id_, url, src) + self.database.set_download_data(id_, url, src) if __name__ == "__main__": diff --git a/src/main.py b/src/main.py index 6cbdc20..5162a32 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,5 @@ +from metadata.database import Database +from metadata.download import MetadataDownloader import metadata.download import metadata.metadata import download_links @@ -6,12 +8,19 @@ import download import logging import os +import tempfile +logging.basicConfig(level=logging.INFO) -TEMP = "temp" -STEP_ONE_CACHE = ".cache1.csv" -STEP_TWO_CACHE = ".cache2.csv" -STEP_THREE_CACHE = ".cache3.csv" +TEMP_FOLDER = "music-downloader" +DATABASE_FILE = "metadata.db" +DATABASE_STRUCTURE_FILE = "database_structure.sql" + +DATABASE_LOGGER = logging.getLogger("database") +METADATA_DOWNLOAD_LOGGER = logging.getLogger("metadata-download") +URL_DOWNLOAD_LOGGER = logging.getLogger("ling-download") +PATH_LOGGER = logging.getLogger("create-paths") +DOWNLOAD_LOGGER = logging.getLogger("download") NOT_A_GENRE = ".", "..", "misc_scripts", "Music", "script", ".git", ".idea" MUSIC_DIR = os.path.expanduser('~/Music') @@ -20,6 +29,16 @@ TOR = False logger = logging.getLogger() logger.level = logging.DEBUG +temp_dir = os.path.join(tempfile.gettempdir(), TEMP_FOLDER) +if not os.path.exists(temp_dir): + os.mkdir(temp_dir) + +database = Database(os.path.join(temp_dir, DATABASE_FILE), + os.path.join(temp_dir, DATABASE_STRUCTURE_FILE), DATABASE_LOGGER, + reset_anyways=False) + + + def get_existing_genre(): valid_directories = [] @@ -31,7 +50,7 @@ def get_existing_genre(): def search_for_metadata(query: str): - search = metadata.metadata.Search(query=query, temp=TEMP) + search = metadata.metadata.Search(query=query) print(search.options) while True: @@ -86,20 +105,20 @@ def cli(start_at: int = 0): if start_at <= 0: search = search_for_metadata(query=input("initial query: ")) logging.info("Starting Downloading of metadata") - metadata.download.download(search) + metadata_downloader = MetadataDownloader(database, METADATA_DOWNLOAD_LOGGER) + metadata_downloader.download(search) if start_at <= 1: logging.info("Fetching Download Links") - download_links.Download(proxies=proxies) + download_links.Download(database, METADATA_DOWNLOAD_LOGGER, proxies=proxies) if start_at <= 2: logging.info("creating Paths") - print(genre) - url_to_path.UrlPath(genre=genre) + url_to_path.UrlPath(database, PATH_LOGGER, genre=genre) if start_at <= 3: logging.info("starting to download the mp3's") - download.Download(proxies=proxies, base_path=MUSIC_DIR) + download.Download(database, DOWNLOAD_LOGGER, proxies=proxies, base_path=MUSIC_DIR) if __name__ == "__main__": diff --git a/src/metadata/database.py b/src/metadata/database.py index 3a0ce5e..df5f7b4 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -98,6 +98,7 @@ class Database: musicbrainz_releasetrackid: str, musicbrainz_albumid: str, feature_aritsts: list, + tracknumber: str = None, track: str = None, isrc: str = None ): @@ -111,8 +112,8 @@ class Database: self.connection.commit() # add track - query = "INSERT OR REPLACE INTO track (id, release_id, track, isrc) VALUES (?, ?, ?, ?);" - values = musicbrainz_releasetrackid, musicbrainz_albumid, track, isrc + query = "INSERT OR REPLACE INTO track (id, release_id, track, isrc, tracknumber) VALUES (?, ?, ?, ?, ?);" + values = musicbrainz_releasetrackid, musicbrainz_albumid, track, isrc, tracknumber self.cursor.execute(query, values) self.connection.commit() @@ -139,6 +140,8 @@ SELECT DISTINCT ) ), 'id', track.id, + 'tracknumber', track.tracknumber, + 'titlesort ', track.tracknumber, 'musicbrainz_releasetrackid', track.id, 'musicbrainz_albumid', release_.id, 'title', track.track, diff --git a/src/metadata/database_structure.sql b/src/metadata/database_structure.sql new file mode 100644 index 0000000..d5262c7 --- /dev/null +++ b/src/metadata/database_structure.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS artist; +CREATE TABLE artist ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT +); + +DROP TABLE IF EXISTS artist_release_group; +CREATE TABLE artist_release_group ( + artist_id TEXT NOT NULL, + release_group_id TEXT NOT NULL +); + +DROP TABLE IF EXISTS artist_track; +CREATE TABLE artist_track ( + artist_id TEXT NOT NULL, + track_id TEXT NOT NULL +); + +DROP TABLE IF EXISTS release_group; +CREATE TABLE release_group ( + id TEXT PRIMARY KEY NOT NULL, + albumartist TEXT, + albumsort INT, + musicbrainz_albumtype TEXT, + compilation TEXT, + album_artist_id TEXT +); + +DROP TABLE IF EXISTS release_; +CREATE TABLE release_ ( + id TEXT PRIMARY KEY NOT NULL, + release_group_id TEXT NOT NULL, + title TEXT, + copyright TEXT, + album_status TEXT, + language TEXT, + year TEXT, + date TEXT, + country TEXT, + barcode TEXT +); + +DROP TABLE IF EXISTS track; +CREATE TABLE track ( + id TEXT PRIMARY KEY NOT NULL, + downloaded BOOLEAN NOT NULL DEFAULT 0, + release_id TEXT NOT NULL, + track TEXT, + tracknumber TEXT, + isrc TEXT, + genre TEXT, + path TEXT, + file TEXT, + url TEXT, + src TEXT +); diff --git a/src/metadata/download.py b/src/metadata/download.py index 330687f..9b7d888 100644 --- a/src/metadata/download.py +++ b/src/metadata/download.py @@ -2,7 +2,11 @@ from typing import List import musicbrainzngs import logging -from object_handeling import get_elem_from_obj, parse_music_brainz_date +try: + from object_handeling import get_elem_from_obj, parse_music_brainz_date + +except ModuleNotFoundError: + from metadata.object_handeling import get_elem_from_obj, parse_music_brainz_date # I don't know if it would be feesable to set up my own mb instance # https://github.com/metabrainz/musicbrainz-docker @@ -81,9 +85,10 @@ class MetadataDownloader: database, logger, musicbrainz_releasegroupid: str, - artists = [], + artists=[], albumsort: int = None, - only_download_distinct_releases: bool = True + only_download_distinct_releases: bool = True, + fetch_further: bool = True ): self.database = database self.logger = logger @@ -117,6 +122,9 @@ class MetadataDownloader: self.save() + if not fetch_further: + return + if only_download_distinct_releases: self.append_distinct_releases(release_datas) else: @@ -142,7 +150,8 @@ class MetadataDownloader: for existing_artist in self.artists: if artist_id == existing_artist.musicbrainz_artistid: return existing_artist - new_artist = Artist(artist_id, release_groups=[self], new_release_groups=False) + new_artist = MetadataDownloader.Artist(self.database, self.logger, artist_id, release_groups=[self], + new_release_groups=False) self.artists.append(new_artist) return new_artist @@ -150,7 +159,8 @@ class MetadataDownloader: musicbrainz_albumid = get_elem_from_obj(release_data, ['id']) if musicbrainz_albumid is None: return - self.releases.append(MetadataDownloader.Release(self.database, self.logger, musicbrainz_albumid, release_group=self)) + self.releases.append( + MetadataDownloader.Release(self.database, self.logger, musicbrainz_albumid, release_group=self)) def append_distinct_releases(self, release_datas: List[dict]): titles = {} @@ -174,7 +184,8 @@ class MetadataDownloader: database, logger, musicbrainz_albumid: str, - release_group = None + release_group=None, + fetch_furter: bool = True ): self.database = database self.logger = logger @@ -186,10 +197,16 @@ class MetadataDownloader: self.release_group = release_group self.tracklist = [] - result = musicbrainzngs.get_release_by_id(self.musicbrainz_albumid, includes=["recordings", "labels"]) + result = musicbrainzngs.get_release_by_id(self.musicbrainz_albumid, + includes=["recordings", "labels", "release-groups"]) release_data = get_elem_from_obj(result, ['release'], return_if_none={}) label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={}) recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[]) + release_group_data = get_elem_from_obj(release_data, ['release-group'], return_if_none={}) + if self.release_group is None: + self.release_group = MetadataDownloader.ReleaseGroup(self.database, self.logger, + musicbrainz_releasegroupid=get_elem_from_obj( + release_group_data, ['id']), fetch_further=False) self.title = get_elem_from_obj(release_data, ['title']) self.copyright = get_elem_from_obj(label_data, [0, 'label', 'name']) @@ -202,7 +219,8 @@ class MetadataDownloader: self.barcode = get_elem_from_obj(release_data, ['barcode']) self.save() - self.append_recordings(recording_datas) + if fetch_furter: + self.append_recordings(recording_datas) def __str__(self): return f"{self.title} ©{self.copyright} {self.album_status}" @@ -223,12 +241,14 @@ class MetadataDownloader: ) def append_recordings(self, recording_datas: dict): - for recording_data in recording_datas: + for i, recording_data in enumerate(recording_datas): musicbrainz_releasetrackid = get_elem_from_obj(recording_data, ['recording', 'id']) if musicbrainz_releasetrackid is None: continue - self.tracklist.append(MetadataDownloader.Track(self.database, self.logger, musicbrainz_releasetrackid, self)) + self.tracklist.append( + MetadataDownloader.Track(self.database, self.logger, musicbrainz_releasetrackid, self, + track_number=str(i + 1))) class Track: def __init__( @@ -236,7 +256,8 @@ class MetadataDownloader: database, logger, musicbrainz_releasetrackid: str, - release = None + release=None, + track_number: str = None ): self.database = database self.logger = logger @@ -249,10 +270,18 @@ class MetadataDownloader: self.release = release self.artists = [] + self.track_number = track_number + result = musicbrainzngs.get_recording_by_id(self.musicbrainz_releasetrackid, includes=["artists", "releases", "recording-rels", "isrcs", "work-level-rels"]) recording_data = result['recording'] + release_data = get_elem_from_obj(recording_data, ['release-list', -1]) + if self.release is None: + self.release = MetadataDownloader.Release(self.database, self.logger, + get_elem_from_obj(release_data, ['id']), fetch_furter=False) + + for artist_data in get_elem_from_obj(recording_data, ['artist-credit'], return_if_none=[]): self.append_artist(get_elem_from_obj(artist_data, ['artist', 'id'])) @@ -271,6 +300,7 @@ class MetadataDownloader: musicbrainz_releasetrackid=self.musicbrainz_releasetrackid, musicbrainz_albumid=self.release.musicbrainz_albumid, feature_aritsts=[artist.musicbrainz_artistid for artist in self.artists], + tracknumber=self.track_number, track=self.title, isrc=self.isrc ) @@ -316,14 +346,15 @@ if __name__ == "__main__": import database database_ = database.Database(os.path.join(temp_dir, "metadata.db"), - os.path.join(temp_dir, "database_structure.sql"), db_logger, - reset_anyways=True) + os.path.join(temp_dir, "database_structure.sql"), db_logger, + reset_anyways=True) download_logger = logging.getLogger("metadata downloader") download_logger.setLevel(logging.INFO) downloader = MetadataDownloader(database_, download_logger) - downloader.download({'id': '5cfecbe4-f600-45e5-9038-ce820eedf3d1', 'type': 'artist'}) + downloader.download({'id': 'd2006339-9e98-4624-a386-d503328eb854', 'type': 'track'}) + # downloader.download({'id': 'cdd16860-35fd-46af-bd8c-5de7b15ebc31', 'type': 'release'}) # download({'id': '4b9af532-ef7e-42ab-8b26-c466327cb5e0', 'type': 'release'}) # download({'id': 'c24ed9e7-6df9-44de-8570-975f1a5a75d1', 'type': 'track'}) diff --git a/src/metadata/object_handeling.py b/src/metadata/object_handeling.py index 3830c8e..57a3b4d 100644 --- a/src/metadata/object_handeling.py +++ b/src/metadata/object_handeling.py @@ -1,5 +1,6 @@ from datetime import date + def get_elem_from_obj(current_object, keys: list, after_process=lambda x: x, return_if_none=None): current_object = current_object for key in keys: @@ -9,6 +10,7 @@ def get_elem_from_obj(current_object, keys: list, after_process=lambda x: x, ret return return_if_none return after_process(current_object) + def parse_music_brainz_date(mb_date: str) -> date: year = 1 month = 1 diff --git a/src/metadata/options.py b/src/metadata/options.py index 4a385cf..dea4882 100644 --- a/src/metadata/options.py +++ b/src/metadata/options.py @@ -37,7 +37,6 @@ def get_string_for_option(option: dict) -> str: class Options: def __init__(self, results: list): self.results = results - print(results) self.artist_count = 0 self.release_count = 0 @@ -69,7 +68,6 @@ class Options: def __str__(self) -> str: string = f"artists: {self.artist_count}; releases {self.release_count}; tracks {self.track_count}\n" for i, option in enumerate(self.result_list): - print(option) string += f"{i})\t{option['type']}:\t" + get_string_for_option(option) return string diff --git a/src/musify.py b/src/musify.py index 69d534a..7a0e23b 100644 --- a/src/musify.py +++ b/src/musify.py @@ -1,9 +1,14 @@ import logging +import time + import requests import bs4 import phonetic_compares +TRIES = 5 +TIMEOUT = 10 + session = requests.Session() session.headers = { "Connection": "keep-alive", @@ -64,11 +69,18 @@ def download(row): return download_from_musify(file_, url) -def get_soup_of_search(query: str): +def get_soup_of_search(query: str, trie=0): url = f"https://musify.club/search?searchText={query}" logging.debug(f"Trying to get soup from {url}") r = session.get(url) if r.status_code != 200: + if r.status_code in [503] and trie < TRIES: + logging.warning(f"youtube blocked downloading. ({trie}-{TRIES})") + logging.warning(f"retrying in {TIMEOUT} seconds again") + time.sleep(TIMEOUT) + return get_soup_of_search(query, trie=trie+1) + + logging.warning("too many tries, returning") raise ConnectionError(f"{r.url} returned {r.status_code}:\n{r.content}") return bs4.BeautifulSoup(r.content, features="html.parser") diff --git a/src/url_to_path.py b/src/url_to_path.py index 3862df5..dd4a28f 100644 --- a/src/url_to_path.py +++ b/src/url_to_path.py @@ -1,20 +1,17 @@ import os.path -import json - -from metadata import database +import logging class UrlPath: - def __init__(self, genre: str, temp: str = "temp", file: str = ".cache3.csv", step_two_file: str = ".cache2.csv"): - self.temp = temp - self.file = file + def __init__(self, database, logger: logging.Logger, genre: str): + self.database = database + self.logger = logger self.genre = genre - for row in database.get_tracks_without_filepath(): + for row in self.database.get_tracks_without_filepath(): file, path = self.get_path_from_row(row) - database.set_filepath(row['id'], file, path, genre) - + self.database.set_filepath(row['id'], file, path, genre) def get_path_from_row(self, row): """ @@ -23,7 +20,9 @@ class UrlPath: :param row: :return: path: """ - return os.path.join(self.get_genre(), self.get_artist(row), self.get_album(row), f"{self.get_song(row)}.mp3"), os.path.join(self.get_genre(), self.get_artist(row), self.get_album(row)) + return os.path.join(self.get_genre(), self.get_artist(row), self.get_album(row), + f"{self.get_song(row)}.mp3"), os.path.join(self.get_genre(), self.get_artist(row), + self.get_album(row)) def escape_part(self, part: str): return part.replace("/", " ") From ebb6af795972a71fc6cfa549346d4b69e3e2346d Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 6 Nov 2022 23:02:44 +0100 Subject: [PATCH 18/35] prioritized youtube over musify to make 1. my downloader more legal, 2. To get the reliability of ISRC's --- src/download_links.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/download_links.py b/src/download_links.py index 16a8c0d..b3e1e80 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -19,18 +19,18 @@ class Download: id_ = row['id'] - # check musify - musify_url = musify.get_musify_url(row) - if musify_url is not None: - self.add_url(musify_url, 'musify', id_) - continue - # check YouTube youtube_url = youtube_music.get_youtube_url(row) if youtube_url is not None: self.add_url(youtube_url, 'youtube', id_) continue + # check musify + musify_url = musify.get_musify_url(row) + if musify_url is not None: + self.add_url(musify_url, 'musify', id_) + continue + # check musify again, but with a different methode that takes longer musify_url = musify.get_musify_url_slow(row) if musify_url is not None: From 5a2892855ce8f084f2a53071ef6ed7e62fa739e6 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 02:08:10 +0100 Subject: [PATCH 19/35] prioritized youtube over musify to make 1. my downloader more legal, 2. To get the reliability of ISRC's --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 5162a32..62d855e 100644 --- a/src/main.py +++ b/src/main.py @@ -35,7 +35,7 @@ if not os.path.exists(temp_dir): database = Database(os.path.join(temp_dir, DATABASE_FILE), os.path.join(temp_dir, DATABASE_STRUCTURE_FILE), DATABASE_LOGGER, - reset_anyways=False) + reset_anyways=True) From 7ba880ec1d36e5a54e99dbdbea0ed2b648139d6b Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Mon, 7 Nov 2022 11:18:22 +0100 Subject: [PATCH 20/35] addet assets folder --- assets/database_structure.sql | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 assets/database_structure.sql diff --git a/assets/database_structure.sql b/assets/database_structure.sql new file mode 100644 index 0000000..d5262c7 --- /dev/null +++ b/assets/database_structure.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS artist; +CREATE TABLE artist ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT +); + +DROP TABLE IF EXISTS artist_release_group; +CREATE TABLE artist_release_group ( + artist_id TEXT NOT NULL, + release_group_id TEXT NOT NULL +); + +DROP TABLE IF EXISTS artist_track; +CREATE TABLE artist_track ( + artist_id TEXT NOT NULL, + track_id TEXT NOT NULL +); + +DROP TABLE IF EXISTS release_group; +CREATE TABLE release_group ( + id TEXT PRIMARY KEY NOT NULL, + albumartist TEXT, + albumsort INT, + musicbrainz_albumtype TEXT, + compilation TEXT, + album_artist_id TEXT +); + +DROP TABLE IF EXISTS release_; +CREATE TABLE release_ ( + id TEXT PRIMARY KEY NOT NULL, + release_group_id TEXT NOT NULL, + title TEXT, + copyright TEXT, + album_status TEXT, + language TEXT, + year TEXT, + date TEXT, + country TEXT, + barcode TEXT +); + +DROP TABLE IF EXISTS track; +CREATE TABLE track ( + id TEXT PRIMARY KEY NOT NULL, + downloaded BOOLEAN NOT NULL DEFAULT 0, + release_id TEXT NOT NULL, + track TEXT, + tracknumber TEXT, + isrc TEXT, + genre TEXT, + path TEXT, + file TEXT, + url TEXT, + src TEXT +); From 8801ce18872f9e3cabe5094296ac0ba1f9d23276 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Mon, 7 Nov 2022 15:04:08 +0100 Subject: [PATCH 21/35] fdsafafdaf --- .VSCodeCounter/2022-11-07_11-51-29/details.md | 43 +++++++++++++ .../2022-11-07_11-51-29/diff-details.md | 31 +++++++++ .VSCodeCounter/2022-11-07_11-51-29/diff.csv | 18 ++++++ .VSCodeCounter/2022-11-07_11-51-29/diff.md | 29 +++++++++ .VSCodeCounter/2022-11-07_11-51-29/diff.txt | 48 ++++++++++++++ .../2022-11-07_11-51-29/results.csv | 30 +++++++++ .../2022-11-07_11-51-29/results.json | 1 + .VSCodeCounter/2022-11-07_11-51-29/results.md | 33 ++++++++++ .../2022-11-07_11-51-29/results.txt | 64 +++++++++++++++++++ README.md | 25 +++++++- slavart.py | 12 ++++ src/main.py | 5 +- src/metadata/database.py | 14 +++- 13 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/details.md create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/diff-details.md create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/diff.csv create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/diff.md create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/diff.txt create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/results.csv create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/results.json create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/results.md create mode 100644 .VSCodeCounter/2022-11-07_11-51-29/results.txt create mode 100644 slavart.py diff --git a/.VSCodeCounter/2022-11-07_11-51-29/details.md b/.VSCodeCounter/2022-11-07_11-51-29/details.md new file mode 100644 index 0000000..cb5a02e --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/details.md @@ -0,0 +1,43 @@ +# Details + +Date : 2022-11-07 11:51:29 + +Directory /home/lars/Projects/music-downloader + +Total : 28 files, 1366 codes, 127 comments, 388 blanks, all 1881 lines + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md) | Markdown | 31 | 0 | 6 | 37 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md) | Markdown | 9 | 0 | 6 | 15 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md) | Markdown | 12 | 0 | 7 | 19 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json) | JSON | 1 | 0 | 0 | 1 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md) | Markdown | 22 | 0 | 7 | 29 | +| [music-downloader/.idea/dataSources.xml](/music-downloader/.idea/dataSources.xml) | XML | 12 | 0 | 0 | 12 | +| [music-downloader/.idea/inspectionProfiles/profiles_settings.xml](/music-downloader/.idea/inspectionProfiles/profiles_settings.xml) | XML | 6 | 0 | 0 | 6 | +| [music-downloader/.idea/misc.xml](/music-downloader/.idea/misc.xml) | XML | 4 | 0 | 0 | 4 | +| [music-downloader/.idea/modules.xml](/music-downloader/.idea/modules.xml) | XML | 8 | 0 | 0 | 8 | +| [music-downloader/.idea/music-downloader.iml](/music-downloader/.idea/music-downloader.iml) | XML | 10 | 0 | 0 | 10 | +| [music-downloader/.idea/vcs.xml](/music-downloader/.idea/vcs.xml) | XML | 6 | 0 | 0 | 6 | +| [music-downloader/README.md](/music-downloader/README.md) | Markdown | 76 | 0 | 35 | 111 | +| [music-downloader/assets/database_structure.sql](/music-downloader/assets/database_structure.sql) | SQLite | 51 | 0 | 6 | 57 | +| [music-downloader/requirements.txt](/music-downloader/requirements.txt) | pip requirements | 8 | 0 | 0 | 8 | +| [music-downloader/src/download.py](/music-downloader/src/download.py) | Python | 60 | 11 | 22 | 93 | +| [music-downloader/src/download_links.py](/music-downloader/src/download_links.py) | Python | 37 | 3 | 15 | 55 | +| [music-downloader/src/main.py](/music-downloader/src/main.py) | Python | 98 | 0 | 31 | 129 | +| [music-downloader/src/metadata/database.py](/music-downloader/src/metadata/database.py) | Python | 153 | 61 | 38 | 252 | +| [music-downloader/src/metadata/database_structure.sql](/music-downloader/src/metadata/database_structure.sql) | SQLite | 51 | 0 | 6 | 57 | +| [music-downloader/src/metadata/download.py](/music-downloader/src/metadata/download.py) | Python | 270 | 24 | 67 | 361 | +| [music-downloader/src/metadata/metadata.py](/music-downloader/src/metadata/metadata.py) | Python | 106 | 9 | 28 | 143 | +| [music-downloader/src/metadata/object_handeling.py](/music-downloader/src/metadata/object_handeling.py) | Python | 19 | 0 | 6 | 25 | +| [music-downloader/src/metadata/options.py](/music-downloader/src/metadata/options.py) | Python | 87 | 8 | 24 | 119 | +| [music-downloader/src/musify.py](/music-downloader/src/musify.py) | Python | 106 | 2 | 40 | 148 | +| [music-downloader/src/phonetic_compares.py](/music-downloader/src/phonetic_compares.py) | Python | 15 | 0 | 8 | 23 | +| [music-downloader/src/test.py](/music-downloader/src/test.py) | Python | 18 | 1 | 6 | 25 | +| [music-downloader/src/url_to_path.py](/music-downloader/src/url_to_path.py) | Python | 27 | 6 | 13 | 46 | +| [music-downloader/src/youtube_music.py](/music-downloader/src/youtube_music.py) | Python | 63 | 2 | 17 | 82 | + +[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/diff-details.md b/.VSCodeCounter/2022-11-07_11-51-29/diff-details.md new file mode 100644 index 0000000..d089907 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/diff-details.md @@ -0,0 +1,31 @@ +# Diff Details + +Date : 2022-11-07 11:51:29 + +Directory /home/lars/Projects/music-downloader + +Total : 16 files, 234 codes, 18 comments, 48 blanks, all 300 lines + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details + +## Files +| filename | language | code | comment | blank | total | +| :--- | :--- | ---: | ---: | ---: | ---: | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md) | Markdown | 31 | 0 | 6 | 37 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md) | Markdown | 9 | 0 | 6 | 15 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md) | Markdown | 12 | 0 | 7 | 19 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json) | JSON | 1 | 0 | 0 | 1 | +| [music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md](/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md) | Markdown | 22 | 0 | 7 | 29 | +| [music-downloader/README.md](/music-downloader/README.md) | Markdown | 1 | 0 | 1 | 2 | +| [music-downloader/assets/database_structure.sql](/music-downloader/assets/database_structure.sql) | SQLite | 51 | 0 | 6 | 57 | +| [music-downloader/src/download.py](/music-downloader/src/download.py) | Python | 1 | 1 | 1 | 3 | +| [music-downloader/src/download_links.py](/music-downloader/src/download_links.py) | Python | -9 | 0 | -3 | -12 | +| [music-downloader/src/main.py](/music-downloader/src/main.py) | Python | 20 | 0 | 5 | 25 | +| [music-downloader/src/metadata/database.py](/music-downloader/src/metadata/database.py) | Python | 32 | 26 | 3 | 61 | +| [music-downloader/src/metadata/database_structure.sql](/music-downloader/src/metadata/database_structure.sql) | SQLite | 3 | 0 | 0 | 3 | +| [music-downloader/src/metadata/download.py](/music-downloader/src/metadata/download.py) | Python | 59 | -9 | 7 | 57 | +| [music-downloader/src/metadata/object_handeling.py](/music-downloader/src/metadata/object_handeling.py) | Python | 0 | 0 | 2 | 2 | +| [music-downloader/src/musify.py](/music-downloader/src/musify.py) | Python | 9 | 0 | 3 | 12 | +| [music-downloader/src/url_to_path.py](/music-downloader/src/url_to_path.py) | Python | -8 | 0 | -3 | -11 | + +[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/diff.csv b/.VSCodeCounter/2022-11-07_11-51-29/diff.csv new file mode 100644 index 0000000..dfd7279 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/diff.csv @@ -0,0 +1,18 @@ +"filename", "language", "Python", "SQLite", "Markdown", "JSON", "comment", "blank", "total" +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md", "Markdown", 0, 0, 31, 0, 0, 6, 37 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md", "Markdown", 0, 0, 9, 0, 0, 6, 15 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md", "Markdown", 0, 0, 12, 0, 0, 7, 19 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json", "JSON", 0, 0, 0, 1, 0, 0, 1 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md", "Markdown", 0, 0, 22, 0, 0, 7, 29 +"/home/lars/Projects/music-downloader/README.md", "Markdown", 0, 0, 1, 0, 0, 1, 2 +"/home/lars/Projects/music-downloader/assets/database_structure.sql", "SQLite", 0, 51, 0, 0, 0, 6, 57 +"/home/lars/Projects/music-downloader/src/download.py", "Python", 1, 0, 0, 0, 1, 1, 3 +"/home/lars/Projects/music-downloader/src/download_links.py", "Python", -9, 0, 0, 0, 0, -3, -12 +"/home/lars/Projects/music-downloader/src/main.py", "Python", 20, 0, 0, 0, 0, 5, 25 +"/home/lars/Projects/music-downloader/src/metadata/database.py", "Python", 32, 0, 0, 0, 26, 3, 61 +"/home/lars/Projects/music-downloader/src/metadata/database_structure.sql", "SQLite", 0, 3, 0, 0, 0, 0, 3 +"/home/lars/Projects/music-downloader/src/metadata/download.py", "Python", 59, 0, 0, 0, -9, 7, 57 +"/home/lars/Projects/music-downloader/src/metadata/object_handeling.py", "Python", 0, 0, 0, 0, 0, 2, 2 +"/home/lars/Projects/music-downloader/src/musify.py", "Python", 9, 0, 0, 0, 0, 3, 12 +"/home/lars/Projects/music-downloader/src/url_to_path.py", "Python", -8, 0, 0, 0, 0, -3, -11 +"Total", "-", 104, 54, 75, 1, 18, 48, 300 \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/diff.md b/.VSCodeCounter/2022-11-07_11-51-29/diff.md new file mode 100644 index 0000000..16fafb6 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/diff.md @@ -0,0 +1,29 @@ +# Diff Summary + +Date : 2022-11-07 11:51:29 + +Directory /home/lars/Projects/music-downloader + +Total : 16 files, 234 codes, 18 comments, 48 blanks, all 300 lines + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| Python | 8 | 104 | 18 | 15 | 137 | +| Markdown | 5 | 75 | 0 | 27 | 102 | +| SQLite | 2 | 54 | 0 | 6 | 60 | +| JSON | 1 | 1 | 0 | 0 | 1 | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| . | 16 | 234 | 18 | 48 | 300 | +| .VSCodeCounter | 5 | 75 | 0 | 26 | 101 | +| .VSCodeCounter/2022-10-28_12-39-22 | 5 | 75 | 0 | 26 | 101 | +| assets | 1 | 51 | 0 | 6 | 57 | +| src | 9 | 107 | 18 | 15 | 140 | +| src/metadata | 4 | 94 | 17 | 12 | 123 | + +[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/diff.txt b/.VSCodeCounter/2022-11-07_11-51-29/diff.txt new file mode 100644 index 0000000..46b6206 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/diff.txt @@ -0,0 +1,48 @@ +Date : 2022-11-07 11:51:29 +Directory : /home/lars/Projects/music-downloader +Total : 16 files, 234 codes, 18 comments, 48 blanks, all 300 lines + +Languages ++----------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++----------+------------+------------+------------+------------+------------+ +| Python | 8 | 104 | 18 | 15 | 137 | +| Markdown | 5 | 75 | 0 | 27 | 102 | +| SQLite | 2 | 54 | 0 | 6 | 60 | +| JSON | 1 | 1 | 0 | 0 | 1 | ++----------+------------+------------+------------+------------+------------+ + +Directories ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| . | 16 | 234 | 18 | 48 | 300 | +| .VSCodeCounter | 5 | 75 | 0 | 26 | 101 | +| .VSCodeCounter/2022-10-28_12-39-22 | 5 | 75 | 0 | 26 | 101 | +| assets | 1 | 51 | 0 | 6 | 57 | +| src | 9 | 107 | 18 | 15 | 140 | +| src/metadata | 4 | 94 | 17 | 12 | 123 | ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ + +Files ++-----------------------------------------------------------------------------------------+----------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++-----------------------------------------------------------------------------------------+----------+------------+------------+------------+------------+ +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md | Markdown | 31 | 0 | 6 | 37 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md | Markdown | 9 | 0 | 6 | 15 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md | Markdown | 12 | 0 | 7 | 19 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json | JSON | 1 | 0 | 0 | 1 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md | Markdown | 22 | 0 | 7 | 29 | +| /home/lars/Projects/music-downloader/README.md | Markdown | 1 | 0 | 1 | 2 | +| /home/lars/Projects/music-downloader/assets/database_structure.sql | SQLite | 51 | 0 | 6 | 57 | +| /home/lars/Projects/music-downloader/src/download.py | Python | 1 | 1 | 1 | 3 | +| /home/lars/Projects/music-downloader/src/download_links.py | Python | -9 | 0 | -3 | -12 | +| /home/lars/Projects/music-downloader/src/main.py | Python | 20 | 0 | 5 | 25 | +| /home/lars/Projects/music-downloader/src/metadata/database.py | Python | 32 | 26 | 3 | 61 | +| /home/lars/Projects/music-downloader/src/metadata/database_structure.sql | SQLite | 3 | 0 | 0 | 3 | +| /home/lars/Projects/music-downloader/src/metadata/download.py | Python | 59 | -9 | 7 | 57 | +| /home/lars/Projects/music-downloader/src/metadata/object_handeling.py | Python | 0 | 0 | 2 | 2 | +| /home/lars/Projects/music-downloader/src/musify.py | Python | 9 | 0 | 3 | 12 | +| /home/lars/Projects/music-downloader/src/url_to_path.py | Python | -8 | 0 | -3 | -11 | +| Total | | 234 | 18 | 48 | 300 | ++-----------------------------------------------------------------------------------------+----------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/results.csv b/.VSCodeCounter/2022-11-07_11-51-29/results.csv new file mode 100644 index 0000000..0e9db89 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/results.csv @@ -0,0 +1,30 @@ +"filename", "language", "Python", "SQLite", "XML", "pip requirements", "Markdown", "JSON", "comment", "blank", "total" +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md", "Markdown", 0, 0, 0, 0, 31, 0, 0, 6, 37 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md", "Markdown", 0, 0, 0, 0, 9, 0, 0, 6, 15 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md", "Markdown", 0, 0, 0, 0, 12, 0, 0, 7, 19 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json", "JSON", 0, 0, 0, 0, 0, 1, 0, 0, 1 +"/home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md", "Markdown", 0, 0, 0, 0, 22, 0, 0, 7, 29 +"/home/lars/Projects/music-downloader/.idea/dataSources.xml", "XML", 0, 0, 12, 0, 0, 0, 0, 0, 12 +"/home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml", "XML", 0, 0, 6, 0, 0, 0, 0, 0, 6 +"/home/lars/Projects/music-downloader/.idea/misc.xml", "XML", 0, 0, 4, 0, 0, 0, 0, 0, 4 +"/home/lars/Projects/music-downloader/.idea/modules.xml", "XML", 0, 0, 8, 0, 0, 0, 0, 0, 8 +"/home/lars/Projects/music-downloader/.idea/music-downloader.iml", "XML", 0, 0, 10, 0, 0, 0, 0, 0, 10 +"/home/lars/Projects/music-downloader/.idea/vcs.xml", "XML", 0, 0, 6, 0, 0, 0, 0, 0, 6 +"/home/lars/Projects/music-downloader/README.md", "Markdown", 0, 0, 0, 0, 76, 0, 0, 35, 111 +"/home/lars/Projects/music-downloader/assets/database_structure.sql", "SQLite", 0, 51, 0, 0, 0, 0, 0, 6, 57 +"/home/lars/Projects/music-downloader/requirements.txt", "pip requirements", 0, 0, 0, 8, 0, 0, 0, 0, 8 +"/home/lars/Projects/music-downloader/src/download.py", "Python", 60, 0, 0, 0, 0, 0, 11, 22, 93 +"/home/lars/Projects/music-downloader/src/download_links.py", "Python", 37, 0, 0, 0, 0, 0, 3, 15, 55 +"/home/lars/Projects/music-downloader/src/main.py", "Python", 98, 0, 0, 0, 0, 0, 0, 31, 129 +"/home/lars/Projects/music-downloader/src/metadata/database.py", "Python", 153, 0, 0, 0, 0, 0, 61, 38, 252 +"/home/lars/Projects/music-downloader/src/metadata/database_structure.sql", "SQLite", 0, 51, 0, 0, 0, 0, 0, 6, 57 +"/home/lars/Projects/music-downloader/src/metadata/download.py", "Python", 270, 0, 0, 0, 0, 0, 24, 67, 361 +"/home/lars/Projects/music-downloader/src/metadata/metadata.py", "Python", 106, 0, 0, 0, 0, 0, 9, 28, 143 +"/home/lars/Projects/music-downloader/src/metadata/object_handeling.py", "Python", 19, 0, 0, 0, 0, 0, 0, 6, 25 +"/home/lars/Projects/music-downloader/src/metadata/options.py", "Python", 87, 0, 0, 0, 0, 0, 8, 24, 119 +"/home/lars/Projects/music-downloader/src/musify.py", "Python", 106, 0, 0, 0, 0, 0, 2, 40, 148 +"/home/lars/Projects/music-downloader/src/phonetic_compares.py", "Python", 15, 0, 0, 0, 0, 0, 0, 8, 23 +"/home/lars/Projects/music-downloader/src/test.py", "Python", 18, 0, 0, 0, 0, 0, 1, 6, 25 +"/home/lars/Projects/music-downloader/src/url_to_path.py", "Python", 27, 0, 0, 0, 0, 0, 6, 13, 46 +"/home/lars/Projects/music-downloader/src/youtube_music.py", "Python", 63, 0, 0, 0, 0, 0, 2, 17, 82 +"Total", "-", 1059, 102, 46, 8, 150, 1, 127, 388, 1881 \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/results.json b/.VSCodeCounter/2022-11-07_11-51-29/results.json new file mode 100644 index 0000000..d531c5e --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/results.json @@ -0,0 +1 @@ +{"file:///home/lars/Projects/music-downloader/src/metadata/database.py":{"language":"Python","code":153,"comment":61,"blank":38},"file:///home/lars/Projects/music-downloader/src/url_to_path.py":{"language":"Python","code":27,"comment":6,"blank":13},"file:///home/lars/Projects/music-downloader/src/metadata/download.py":{"language":"Python","code":270,"comment":24,"blank":67},"file:///home/lars/Projects/music-downloader/src/metadata/metadata.py":{"language":"Python","code":106,"comment":9,"blank":28},"file:///home/lars/Projects/music-downloader/src/main.py":{"language":"Python","code":98,"comment":0,"blank":31},"file:///home/lars/Projects/music-downloader/src/metadata/object_handeling.py":{"language":"Python","code":19,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/src/download.py":{"language":"Python","code":60,"comment":11,"blank":22},"file:///home/lars/Projects/music-downloader/src/test.py":{"language":"Python","code":18,"comment":1,"blank":6},"file:///home/lars/Projects/music-downloader/src/metadata/database_structure.sql":{"language":"SQLite","code":51,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/src/metadata/options.py":{"language":"Python","code":87,"comment":8,"blank":24},"file:///home/lars/Projects/music-downloader/src/download_links.py":{"language":"Python","code":37,"comment":3,"blank":15},"file:///home/lars/Projects/music-downloader/assets/database_structure.sql":{"language":"SQLite","code":51,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/src/youtube_music.py":{"language":"Python","code":63,"comment":2,"blank":17},"file:///home/lars/Projects/music-downloader/src/phonetic_compares.py":{"language":"Python","code":15,"comment":0,"blank":8},"file:///home/lars/Projects/music-downloader/src/musify.py":{"language":"Python","code":106,"comment":2,"blank":40},"file:///home/lars/Projects/music-downloader/.idea/dataSources.xml":{"language":"XML","code":12,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/requirements.txt":{"language":"pip requirements","code":8,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/README.md":{"language":"Markdown","code":76,"comment":0,"blank":35},"file:///home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md":{"language":"Markdown","code":9,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/.idea/music-downloader.iml":{"language":"XML","code":10,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.idea/vcs.xml":{"language":"XML","code":6,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md":{"language":"Markdown","code":22,"comment":0,"blank":7},"file:///home/lars/Projects/music-downloader/.idea/misc.xml":{"language":"XML","code":4,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.idea/modules.xml":{"language":"XML","code":8,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md":{"language":"Markdown","code":31,"comment":0,"blank":6},"file:///home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json":{"language":"JSON","code":1,"comment":0,"blank":0},"file:///home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md":{"language":"Markdown","code":12,"comment":0,"blank":7},"file:///home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml":{"language":"XML","code":6,"comment":0,"blank":0}} \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/results.md b/.VSCodeCounter/2022-11-07_11-51-29/results.md new file mode 100644 index 0000000..03d1839 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/results.md @@ -0,0 +1,33 @@ +# Summary + +Date : 2022-11-07 11:51:29 + +Directory /home/lars/Projects/music-downloader + +Total : 28 files, 1366 codes, 127 comments, 388 blanks, all 1881 lines + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) + +## Languages +| language | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| Python | 13 | 1,059 | 127 | 315 | 1,501 | +| Markdown | 5 | 150 | 0 | 61 | 211 | +| SQLite | 2 | 102 | 0 | 12 | 114 | +| XML | 6 | 46 | 0 | 0 | 46 | +| pip requirements | 1 | 8 | 0 | 0 | 8 | +| JSON | 1 | 1 | 0 | 0 | 1 | + +## Directories +| path | files | code | comment | blank | total | +| :--- | ---: | ---: | ---: | ---: | ---: | +| . | 28 | 1,366 | 127 | 388 | 1,881 | +| .VSCodeCounter | 5 | 75 | 0 | 26 | 101 | +| .VSCodeCounter/2022-10-28_12-39-22 | 5 | 75 | 0 | 26 | 101 | +| .idea | 6 | 46 | 0 | 0 | 46 | +| .idea/inspectionProfiles | 1 | 6 | 0 | 0 | 6 | +| assets | 1 | 51 | 0 | 6 | 57 | +| src | 14 | 1,110 | 127 | 321 | 1,558 | +| src/metadata | 6 | 686 | 102 | 169 | 957 | + +Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md) \ No newline at end of file diff --git a/.VSCodeCounter/2022-11-07_11-51-29/results.txt b/.VSCodeCounter/2022-11-07_11-51-29/results.txt new file mode 100644 index 0000000..7a20f12 --- /dev/null +++ b/.VSCodeCounter/2022-11-07_11-51-29/results.txt @@ -0,0 +1,64 @@ +Date : 2022-11-07 11:51:29 +Directory : /home/lars/Projects/music-downloader +Total : 28 files, 1366 codes, 127 comments, 388 blanks, all 1881 lines + +Languages ++------------------+------------+------------+------------+------------+------------+ +| language | files | code | comment | blank | total | ++------------------+------------+------------+------------+------------+------------+ +| Python | 13 | 1,059 | 127 | 315 | 1,501 | +| Markdown | 5 | 150 | 0 | 61 | 211 | +| SQLite | 2 | 102 | 0 | 12 | 114 | +| XML | 6 | 46 | 0 | 0 | 46 | +| pip requirements | 1 | 8 | 0 | 0 | 8 | +| JSON | 1 | 1 | 0 | 0 | 1 | ++------------------+------------+------------+------------+------------+------------+ + +Directories ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| path | files | code | comment | blank | total | ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ +| . | 28 | 1,366 | 127 | 388 | 1,881 | +| .VSCodeCounter | 5 | 75 | 0 | 26 | 101 | +| .VSCodeCounter/2022-10-28_12-39-22 | 5 | 75 | 0 | 26 | 101 | +| .idea | 6 | 46 | 0 | 0 | 46 | +| .idea/inspectionProfiles | 1 | 6 | 0 | 0 | 6 | +| assets | 1 | 51 | 0 | 6 | 57 | +| src | 14 | 1,110 | 127 | 321 | 1,558 | +| src/metadata | 6 | 686 | 102 | 169 | 957 | ++-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+ + +Files ++-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ +| filename | language | code | comment | blank | total | ++-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/details.md | Markdown | 31 | 0 | 6 | 37 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff-details.md | Markdown | 9 | 0 | 6 | 15 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/diff.md | Markdown | 12 | 0 | 7 | 19 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.json | JSON | 1 | 0 | 0 | 1 | +| /home/lars/Projects/music-downloader/.VSCodeCounter/2022-10-28_12-39-22/results.md | Markdown | 22 | 0 | 7 | 29 | +| /home/lars/Projects/music-downloader/.idea/dataSources.xml | XML | 12 | 0 | 0 | 12 | +| /home/lars/Projects/music-downloader/.idea/inspectionProfiles/profiles_settings.xml | XML | 6 | 0 | 0 | 6 | +| /home/lars/Projects/music-downloader/.idea/misc.xml | XML | 4 | 0 | 0 | 4 | +| /home/lars/Projects/music-downloader/.idea/modules.xml | XML | 8 | 0 | 0 | 8 | +| /home/lars/Projects/music-downloader/.idea/music-downloader.iml | XML | 10 | 0 | 0 | 10 | +| /home/lars/Projects/music-downloader/.idea/vcs.xml | XML | 6 | 0 | 0 | 6 | +| /home/lars/Projects/music-downloader/README.md | Markdown | 76 | 0 | 35 | 111 | +| /home/lars/Projects/music-downloader/assets/database_structure.sql | SQLite | 51 | 0 | 6 | 57 | +| /home/lars/Projects/music-downloader/requirements.txt | pip requirements | 8 | 0 | 0 | 8 | +| /home/lars/Projects/music-downloader/src/download.py | Python | 60 | 11 | 22 | 93 | +| /home/lars/Projects/music-downloader/src/download_links.py | Python | 37 | 3 | 15 | 55 | +| /home/lars/Projects/music-downloader/src/main.py | Python | 98 | 0 | 31 | 129 | +| /home/lars/Projects/music-downloader/src/metadata/database.py | Python | 153 | 61 | 38 | 252 | +| /home/lars/Projects/music-downloader/src/metadata/database_structure.sql | SQLite | 51 | 0 | 6 | 57 | +| /home/lars/Projects/music-downloader/src/metadata/download.py | Python | 270 | 24 | 67 | 361 | +| /home/lars/Projects/music-downloader/src/metadata/metadata.py | Python | 106 | 9 | 28 | 143 | +| /home/lars/Projects/music-downloader/src/metadata/object_handeling.py | Python | 19 | 0 | 6 | 25 | +| /home/lars/Projects/music-downloader/src/metadata/options.py | Python | 87 | 8 | 24 | 119 | +| /home/lars/Projects/music-downloader/src/musify.py | Python | 106 | 2 | 40 | 148 | +| /home/lars/Projects/music-downloader/src/phonetic_compares.py | Python | 15 | 0 | 8 | 23 | +| /home/lars/Projects/music-downloader/src/test.py | Python | 18 | 1 | 6 | 25 | +| /home/lars/Projects/music-downloader/src/url_to_path.py | Python | 27 | 6 | 13 | 46 | +| /home/lars/Projects/music-downloader/src/youtube_music.py | Python | 63 | 2 | 17 | 82 | +| Total | | 1,366 | 127 | 388 | 1,881 | ++-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/README.md b/README.md index d777837..52ae6aa 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,27 @@ There are two bottlenecks with this approach though: - There is a great site whith a huge isrc database [https://isrc.soundexchange.com/](https://isrc.soundexchange.com/). -https://slavart.gamesdrive.net/ \ No newline at end of file +https://slavart.gamesdrive.net/ +https://getmetal.club/ +https://newalbumreleases.net/ +http://download-soundtracks.com/ +https://scnlog.me/ +https://intmusic.net/ +https://www.pluspremieres.ws/ +https://music4newgen.org/ +https://takemetal.org/ +https://coreradio.ru/ +https://alterportal.net/ +https://vk.com/mdcore +https://vk.com/mdrock +https://sophiesfloorboard.blogspot.com/ +https://funkysouls.org/ +https://www.deadpulpit.com/ +https://vk.com/filter_rock +https://en.metal-tracker.com/ +https://thelastdisaster.org/ +https://vk.com/phc +https://free-mp3-download.net/ requires recaptcha +https://vk.com/filter_rock +https://t.me/ffilternews telegram? +https://justanothermusic.site/index.php requires login diff --git a/slavart.py b/slavart.py new file mode 100644 index 0000000..614239d --- /dev/null +++ b/slavart.py @@ -0,0 +1,12 @@ +import requests + +API_ENDPOINT = "https://slavart.gamesdrive.net/api/search?q=Tekkno" +DOWNLOAD_ENDPOINT = "https://slavart-api.gamesdrive.net/api/download/track?id=153182274" + + +if __name__ == "__main__": + r = requests.get(DOWNLOAD_ENDPOINT, headers={ + "Access-Control-Allow-Origin": "https://slavart.gamesdrive.net/" + }) + print(r.status_code) + print(r.text) diff --git a/src/main.py b/src/main.py index 62d855e..80a8e83 100644 --- a/src/main.py +++ b/src/main.py @@ -15,6 +15,7 @@ logging.basicConfig(level=logging.INFO) TEMP_FOLDER = "music-downloader" DATABASE_FILE = "metadata.db" DATABASE_STRUCTURE_FILE = "database_structure.sql" +DATABASE_STRUCTURE_FALLBACK = "https://raw.githubusercontent.com/HeIIow2/music-downloader/new_metadata/assets/database_structure.sql" DATABASE_LOGGER = logging.getLogger("database") METADATA_DOWNLOAD_LOGGER = logging.getLogger("metadata-download") @@ -34,7 +35,9 @@ if not os.path.exists(temp_dir): os.mkdir(temp_dir) database = Database(os.path.join(temp_dir, DATABASE_FILE), - os.path.join(temp_dir, DATABASE_STRUCTURE_FILE), DATABASE_LOGGER, + os.path.join(temp_dir, DATABASE_STRUCTURE_FILE), + DATABASE_STRUCTURE_FALLBACK, + DATABASE_LOGGER, reset_anyways=True) diff --git a/src/metadata/database.py b/src/metadata/database.py index df5f7b4..f10ef22 100644 --- a/src/metadata/database.py +++ b/src/metadata/database.py @@ -2,10 +2,11 @@ import sqlite3 import os import logging import json +import requests class Database: - def __init__(self, path_to_db: str, db_structure: str, logger: logging.Logger, reset_anyways: bool = False): + def __init__(self, path_to_db: str, db_structure: str, db_structure_fallback: str, logger: logging.Logger, reset_anyways: bool = False): self.logger = logger self.path_to_db = path_to_db @@ -13,9 +14,9 @@ class Database: self.cursor = self.connection.cursor() # init database - self.init_db(database_structure=db_structure, reset_anyways=reset_anyways) + self.init_db(database_structure=db_structure, database_structure_fallback=db_structure_fallback, reset_anyways=reset_anyways) - def init_db(self, database_structure: str, reset_anyways: bool = False): + def init_db(self, database_structure: str, database_structure_fallback: str, reset_anyways: bool = False): # check if db exists exists = True try: @@ -32,6 +33,13 @@ class Database: # reset the database if reset_anyways is true or if an error has been thrown previously. self.logger.info("Creating/Reseting Database.") + if not os.path.exists(database_structure): + self.logger.info("database structure file doesn't exist yet, fetching from github") + r = requests.get(database_structure_fallback) + + with open(database_structure, "w") as f: + f.write(r.text) + # read the file with open(database_structure, "r") as database_structure_file: query = database_structure_file.read() From 0b5e4d60b6c9f022bdd89daf9545269c1b8c2ad3 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Mon, 7 Nov 2022 17:14:09 +0100 Subject: [PATCH 22/35] huge refactoring of search --- src/metadata/search.py | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/metadata/search.py diff --git a/src/metadata/search.py b/src/metadata/search.py new file mode 100644 index 0000000..30d01d7 --- /dev/null +++ b/src/metadata/search.py @@ -0,0 +1,92 @@ +import logging +import musicbrainzngs + +try: + from object_handeling import get_elem_from_obj, parse_music_brainz_date + +except ModuleNotFoundError: + from metadata.object_handeling import get_elem_from_obj, parse_music_brainz_date + + +mb_log = logging.getLogger("musicbrainzngs") +mb_log.setLevel(logging.WARNING) +musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") + + +OPTION_TYPES = ['artist', 'release_group', 'release', 'recording'] + +class Option: + def __init__(self, type_: str, id: str, name: str, additional_info: str = "") -> None: + if type_ not in OPTION_TYPES: + raise ValueError(f"type: {type_} doesn't exist. Leagal Values: {OPTION_TYPES}") + self.type = type_ + self.name = name + self.id = str + + self.additional_info = additional_info + + def __repr__(self) -> str: + type_repr = { + 'artist': 'artist\t\t', + 'release_group': 'release group\t', + 'release': 'release\t\t', + 'recording': 'recording\t' + } + return f"{type_repr[self.type]}: \"{self.name}\"{self.additional_info}" + + +class Search: + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + + self.options_history = [] + + def search_recording_from_text(self, artist: str = None, release_group: str = None, recording: str = None): + result = musicbrainzngs.search_recordings(artist=artist, release=release_group, recording=recording) + recording_list = get_elem_from_obj(result, ['recording-list'], return_if_none=[]) + print(recording_list[0]) + resulting_options = [Option("recording", get_elem_from_obj(recording_, ['id']), get_elem_from_obj(recording_, ['title']), additional_info=f"") for recording_ in recording_list] + return resulting_options + + def search_release_group_from_text(self, artist: str = None, release_group: str = None): + result = musicbrainzngs.search_release_groups(artist=artist, releasegroup=release_group) + release_group_list = get_elem_from_obj(result, ['release-group-list'], return_if_none=[]) + + resulting_options = [Option("release_group", get_elem_from_obj(release_group_, ['id']), get_elem_from_obj(release_group_, ['title']), additional_info=f" by {get_elem_from_obj(release_group_, ['artist-credit', 0, 'name'])}") for release_group_ in release_group_list] + return resulting_options + + def search_artist_from_text(self, artist: str = None): + result = musicbrainzngs.search_artists(artist=artist) + artist_list = get_elem_from_obj(result, ['artist-list'], return_if_none=[]) + + print(artist_list[0]) + + resulting_options = [Option("artist", get_elem_from_obj(artist_, ['id']), get_elem_from_obj(artist_, ['name']), additional_info=f": {', '.join([i['name'] for i in get_elem_from_obj(artist_, ['tag-list'], return_if_none=[])])}") for artist_ in artist_list] + return resulting_options + + + def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None): + if artist is None and release_group is None and recording is None: + self.logger.error("either artist, release group or recording has to be set") + return -1 + + results = [] + if recording is not None: + results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) + elif release_group is not None: + results = self.search_release_group_from_text(artist=artist, release_group=release_group) + else: + results = self.search_artist_from_text(artist=artist) + + for res in results: + print(res) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + logger_ = logging.getLogger("test") + + search = Search(logger=logger_) + # search.search_from_text(artist="I Prevail") + # search.search_from_text(artist="I Prevail", release_group="TRAUMA") + search.search_from_text(artist="I Prevail", release_group="TRAUMA", recording="breaking down") From a9ddea54d22d396857f809ceda48be3e7240a839 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 19:05:34 +0100 Subject: [PATCH 23/35] added fetching of many things in th search function --- src/metadata/search.py | 197 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 177 insertions(+), 20 deletions(-) diff --git a/src/metadata/search.py b/src/metadata/search.py index 30d01d7..a787c31 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -1,5 +1,7 @@ +from typing import List import logging import musicbrainzngs +import optional try: from object_handeling import get_elem_from_obj, parse_music_brainz_date @@ -7,29 +9,28 @@ try: except ModuleNotFoundError: from metadata.object_handeling import get_elem_from_obj, parse_music_brainz_date - mb_log = logging.getLogger("musicbrainzngs") mb_log.setLevel(logging.WARNING) musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") - OPTION_TYPES = ['artist', 'release_group', 'release', 'recording'] + class Option: - def __init__(self, type_: str, id: str, name: str, additional_info: str = "") -> None: + def __init__(self, type_: str, id_: str, name: str, additional_info: str = "") -> None: if type_ not in OPTION_TYPES: raise ValueError(f"type: {type_} doesn't exist. Leagal Values: {OPTION_TYPES}") self.type = type_ self.name = name - self.id = str + self.id = id_ self.additional_info = additional_info def __repr__(self) -> str: type_repr = { 'artist': 'artist\t\t', - 'release_group': 'release group\t', - 'release': 'release\t\t', + 'release_group': 'release group\t', + 'release': 'release\t\t', 'recording': 'recording\t' } return f"{type_repr[self.type]}: \"{self.name}\"{self.additional_info}" @@ -40,37 +41,184 @@ class Search: self.logger = logger self.options_history = [] + self.current_option: Option - def search_recording_from_text(self, artist: str = None, release_group: str = None, recording: str = None): + def append_new_choices(self, new_choices: List[Option]): + print() + for i, choice in enumerate(new_choices): + print(f"{str(i).zfill(2)}) {choice}") + + self.options_history.append(new_choices) + + @staticmethod + def fetch_new_options_from_artist(artist: Option): + """ + returning list of artist and every release group + """ + result = musicbrainzngs.get_artist_by_id(artist.id, includes=["release-groups", "releases"]) + artist_data = get_elem_from_obj(result, ['artist'], return_if_none={}) + + result = [artist] + + # sort all release groups by date and add album sort to have them in chronological order. + release_group_list = artist_data['release-group-list'] + for i, release_group in enumerate(release_group_list): + release_group_list[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date']) + release_group_list.sort(key=lambda x: x['first-release-date']) + release_group_list = [Option("release_group", get_elem_from_obj(release_group_, ['id']), + get_elem_from_obj(release_group_, ['title']), + additional_info=f" ({get_elem_from_obj(release_group_, ['type'])}) from {get_elem_from_obj(release_group_, ['first-release-date'])}") + for release_group_ in release_group_list] + + result.extend(release_group_list) + return result + + @staticmethod + def fetch_new_options_from_release_group(release_group: Option): + """ + returning list including the artists, the releases and the tracklist of the first release + """ + results = [] + + result = musicbrainzngs.get_release_group_by_id(release_group.id, + includes=["artist-credits", "releases"]) + release_group_data = get_elem_from_obj(result, ['release-group'], return_if_none={}) + artist_datas = get_elem_from_obj(release_group_data, ['artist-credit'], return_if_none={}) + release_datas = get_elem_from_obj(release_group_data, ['release-list'], return_if_none={}) + + # appending all the artists to results + for artist_data in artist_datas: + results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']), + get_elem_from_obj(artist_data, ['artist', 'name']))) + + # appending initial release group + results.append(release_group) + + # appending all releases + first_release = None + for i, release_data in enumerate(release_datas): + results.append( + Option('release', get_elem_from_obj(release_data, ['id']), get_elem_from_obj(release_data, ['title']), + additional_info=f" ({get_elem_from_obj(release_data, ['status'])})")) + if i == 0: + first_release = results[-1] + + # append tracklist of first release + if first_release is not None: + results.extend(Search.fetch_new_options_from_release(first_release, only_tracklist=True)) + + return results + + @staticmethod + def fetch_new_options_from_release(release: Option, only_tracklist: bool = False): + """ + artists + release group + release + tracklist + """ + results = [] + result = musicbrainzngs.get_release_by_id(release.id, + includes=["recordings", "labels", "release-groups", "artist-credits"]) + release_data = get_elem_from_obj(result, ['release'], return_if_none={}) + label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={}) + recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[]) + release_group_data = get_elem_from_obj(release_data, ['release-group'], return_if_none={}) + artist_datas = get_elem_from_obj(release_data, ['artist-credit'], return_if_none={}) + + # appending all the artists to results + for artist_data in artist_datas: + results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']), + get_elem_from_obj(artist_data, ['artist', 'name']))) + + # appending the according release group + results.append(Option("release_group", get_elem_from_obj(release_group_data, ['id']), + get_elem_from_obj(release_group_data, ['title']), + additional_info=f" ({get_elem_from_obj(release_group_data, ['type'])}) from {get_elem_from_obj(release_group_data, ['first-release-date'])}")) + + # appending the release + results.append(release) + + # appending the tracklist, but first putting it in a list, in case of only_tracklist being True to + # return this instead + tracklist = [] + for i, recording_data in enumerate(recording_datas): + recording_data = recording_data['recording'] + tracklist.append(Option('recording', get_elem_from_obj(recording_data, ['id']), + get_elem_from_obj(recording_data, ['title']), + f" ({get_elem_from_obj(recording_data, ['length'])}) from {get_elem_from_obj(recording_data, ['artist-credit-phrase'])}")) + + if only_tracklist: + return tracklist + results.extend(tracklist) + return results + + def fetch_new_options(self): + if self.current_option is None: + return -1 + + result = [] + if self.current_option.type == 'artist': + result = self.fetch_new_options_from_artist(self.current_option) + elif self.current_option.type == 'release_group': + result = self.fetch_new_options_from_release_group(self.current_option) + elif self.current_option.type == 'release': + result = self.fetch_new_options_from_release(self.current_option) + + self.append_new_choices(result) + + def choose(self, index: int): + if len(self.options_history) == 0: + logging.error("initial query neaded before choosing") + return -1 + + latest_options = self.options_history[-1] + if index >= len(latest_options): + logging.error("index outside of options") + return -1 + + self.current_option = latest_options[index] + return self.fetch_new_options() + + @staticmethod + def search_recording_from_text(artist: str = None, release_group: str = None, recording: str = None): result = musicbrainzngs.search_recordings(artist=artist, release=release_group, recording=recording) recording_list = get_elem_from_obj(result, ['recording-list'], return_if_none=[]) - print(recording_list[0]) - resulting_options = [Option("recording", get_elem_from_obj(recording_, ['id']), get_elem_from_obj(recording_, ['title']), additional_info=f"") for recording_ in recording_list] + + resulting_options = [ + Option("recording", get_elem_from_obj(recording_, ['id']), get_elem_from_obj(recording_, ['title']), + additional_info=f" of {get_elem_from_obj(recording_, ['release-list', 0, 'title'])} by {get_elem_from_obj(recording_, ['artist-credit', 0, 'name'])}") + for recording_ in recording_list] return resulting_options - def search_release_group_from_text(self, artist: str = None, release_group: str = None): + @staticmethod + def search_release_group_from_text(artist: str = None, release_group: str = None): result = musicbrainzngs.search_release_groups(artist=artist, releasegroup=release_group) release_group_list = get_elem_from_obj(result, ['release-group-list'], return_if_none=[]) - - resulting_options = [Option("release_group", get_elem_from_obj(release_group_, ['id']), get_elem_from_obj(release_group_, ['title']), additional_info=f" by {get_elem_from_obj(release_group_, ['artist-credit', 0, 'name'])}") for release_group_ in release_group_list] + + resulting_options = [Option("release_group", get_elem_from_obj(release_group_, ['id']), + get_elem_from_obj(release_group_, ['title']), + additional_info=f" by {get_elem_from_obj(release_group_, ['artist-credit', 0, 'name'])}") + for release_group_ in release_group_list] return resulting_options - def search_artist_from_text(self, artist: str = None): + @staticmethod + def search_artist_from_text(artist: str = None): result = musicbrainzngs.search_artists(artist=artist) artist_list = get_elem_from_obj(result, ['artist-list'], return_if_none=[]) print(artist_list[0]) - resulting_options = [Option("artist", get_elem_from_obj(artist_, ['id']), get_elem_from_obj(artist_, ['name']), additional_info=f": {', '.join([i['name'] for i in get_elem_from_obj(artist_, ['tag-list'], return_if_none=[])])}") for artist_ in artist_list] + resulting_options = [Option("artist", get_elem_from_obj(artist_, ['id']), get_elem_from_obj(artist_, ['name']), + additional_info=f": {', '.join([i['name'] for i in get_elem_from_obj(artist_, ['tag-list'], return_if_none=[])])}") + for artist_ in artist_list] return resulting_options - def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None): if artist is None and release_group is None and recording is None: self.logger.error("either artist, release group or recording has to be set") return -1 - results = [] if recording is not None: results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) elif release_group is not None: @@ -78,8 +226,7 @@ class Search: else: results = self.search_artist_from_text(artist=artist) - for res in results: - print(res) + self.append_new_choices(results) if __name__ == "__main__": @@ -87,6 +234,16 @@ if __name__ == "__main__": logger_ = logging.getLogger("test") search = Search(logger=logger_) - # search.search_from_text(artist="I Prevail") + search.search_from_text(artist="I Prevail") + # search.search_from_text(artist="K.I.Z") # search.search_from_text(artist="I Prevail", release_group="TRAUMA") - search.search_from_text(artist="I Prevail", release_group="TRAUMA", recording="breaking down") + # search.search_from_text(artist="I Prevail", release_group="TRAUMA", recording="breaking down") + + # choose an artist + search.choose(0) + # choose a release group + search.choose(9) + # choose a release + search.choose(2) + # choose a recording + search.choose(4) From d5d1b633c56c29193b85cda3d25ca8ac65c2f904 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 19:22:46 +0100 Subject: [PATCH 24/35] added fetching of many things in th search function --- src/metadata/search.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/metadata/search.py b/src/metadata/search.py index a787c31..5874ed1 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -153,6 +153,33 @@ class Search: results.extend(tracklist) return results + @staticmethod + def fetch_new_options_from_record(recording: Option): + """ + artists, release, record + """ + results = [] + + result = musicbrainzngs.get_recording_by_id(recording.id,includes=["artists", "releases"]) + recording_data = result['recording'] + release_datas = get_elem_from_obj(recording_data, ['release-list']) + artist_datas = get_elem_from_obj(recording_data, ['artist-credit'], return_if_none={}) + + # appending all the artists to results + for artist_data in artist_datas: + results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']), + get_elem_from_obj(artist_data, ['artist', 'name']))) + + # appending all releases + for i, release_data in enumerate(release_datas): + results.append( + Option('release', get_elem_from_obj(release_data, ['id']), get_elem_from_obj(release_data, ['title']), + additional_info=f" ({get_elem_from_obj(release_data, ['status'])})")) + + results.append(recording) + + return results + def fetch_new_options(self): if self.current_option is None: return -1 @@ -164,6 +191,8 @@ class Search: result = self.fetch_new_options_from_release_group(self.current_option) elif self.current_option.type == 'release': result = self.fetch_new_options_from_release(self.current_option) + elif self.current_option.type == 'recording': + result = self.fetch_new_options_from_record(self.current_option) self.append_new_choices(result) From 994726e09c37d6e0fee4ca5382c449bdde8c7a05 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 19:39:12 +0100 Subject: [PATCH 25/35] added searching by special querry --- src/metadata/search.py | 52 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/metadata/search.py b/src/metadata/search.py index 5874ed1..c771fad 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -1,7 +1,6 @@ from typing import List import logging import musicbrainzngs -import optional try: from object_handeling import get_elem_from_obj, parse_music_brainz_date @@ -13,6 +12,7 @@ mb_log = logging.getLogger("musicbrainzngs") mb_log.setLevel(logging.WARNING) musicbrainzngs.set_useragent("metadata receiver", "0.1", "https://github.com/HeIIow2/music-downloader") +MAX_PARAMATERS = 3 OPTION_TYPES = ['artist', 'release_group', 'release', 'recording'] @@ -258,15 +258,46 @@ class Search: self.append_new_choices(results) -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - logger_ = logging.getLogger("test") + def search_from_query(self, query: str): + """ + mit # wird ein neuer Parameter gestartet + der Buchstabe dahinter legt die Art des Parameters fest + "#a Psychonaut 4 #r Tired, Numb and #t Drop by Drop" + :param query: + :return: + """ + artist = None + release_group = None + recording = None + + query = query.strip() + parameters = query.split('#') + parameters.remove('') + + if len(parameters) > MAX_PARAMATERS: + raise ValueError(f"too many parameters. Only {MAX_PARAMATERS} are allowed") + + for parameter in parameters: + splitted = parameter.split(" ") + type_ = splitted[0] + input_ = " ".join(splitted[:1]).strip() + + if type_ == "a": + artist = input_ + continue + if type_ == "r": + release_group = input_ + continue + if type_ == "t": + release_group = input_ + continue + + self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) + +def automated_demo(): search = Search(logger=logger_) search.search_from_text(artist="I Prevail") - # search.search_from_text(artist="K.I.Z") - # search.search_from_text(artist="I Prevail", release_group="TRAUMA") - # search.search_from_text(artist="I Prevail", release_group="TRAUMA", recording="breaking down") # choose an artist search.choose(0) @@ -276,3 +307,10 @@ if __name__ == "__main__": search.choose(2) # choose a recording search.choose(4) + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + logger_ = logging.getLogger("test") + + search = Search(logger=logger_) + search.search_from_query("#a Psychonaut 4 #r Tired, Numb and #t Drop by Drop") From 9a771b66731807411ac64c5d6a5ceeb2fc2a537d Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 20:02:38 +0100 Subject: [PATCH 26/35] completed the refactored and improved version of the metadata search module :3 --- src/metadata/search.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/metadata/search.py b/src/metadata/search.py index c771fad..3030ec4 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -50,6 +50,12 @@ class Search: self.options_history.append(new_choices) + def get_previous_options(self): + self.options_history.pop(-1) + print() + for i, choice in enumerate(self.options_history[-1]): + print(f"{str(i).zfill(2)}) {choice}") + @staticmethod def fetch_new_options_from_artist(artist: Option): """ @@ -160,7 +166,7 @@ class Search: """ results = [] - result = musicbrainzngs.get_recording_by_id(recording.id,includes=["artists", "releases"]) + result = musicbrainzngs.get_recording_by_id(recording.id, includes=["artists", "releases"]) recording_data = result['recording'] release_datas = get_elem_from_obj(recording_data, ['release-list']) artist_datas = get_elem_from_obj(recording_data, ['artist-credit'], return_if_none={}) @@ -233,6 +239,7 @@ class Search: @staticmethod def search_artist_from_text(artist: str = None): + print(artist) result = musicbrainzngs.search_artists(artist=artist) artist_list = get_elem_from_obj(result, ['artist-list'], return_if_none=[]) @@ -248,6 +255,8 @@ class Search: self.logger.error("either artist, release group or recording has to be set") return -1 + print() + if recording is not None: results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) elif release_group is not None: @@ -257,7 +266,6 @@ class Search: self.append_new_choices(results) - def search_from_query(self, query: str): """ mit # wird ein neuer Parameter gestartet @@ -281,7 +289,7 @@ class Search: for parameter in parameters: splitted = parameter.split(" ") type_ = splitted[0] - input_ = " ".join(splitted[:1]).strip() + input_ = " ".join(splitted[1:]).strip() if type_ == "a": artist = input_ @@ -290,10 +298,11 @@ class Search: release_group = input_ continue if type_ == "t": - release_group = input_ + recording = input_ continue - self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) + self.search_from_text(artist=artist, release_group=release_group, recording=recording) + def automated_demo(): search = Search(logger=logger_) @@ -308,9 +317,28 @@ def automated_demo(): # choose a recording search.choose(4) + +def interactive_demo(): + search = Search(logger=logger_) + while True: + input_ = input("q to quit, .. for previous options, int for this element, str to search for query, ok to download: ") + input_.strip() + if input_.lower() == "ok": + break + if input_.lower() == "q": + break + if input_.lower() == "..": + search.get_previous_options() + continue + if input_.isdigit(): + search.choose(int(input_)) + continue + search.search_from_query(input_) + + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) logger_ = logging.getLogger("test") - search = Search(logger=logger_) - search.search_from_query("#a Psychonaut 4 #r Tired, Numb and #t Drop by Drop") + interactive_demo() + From b4de6cd639bcc3eb12a75334f3ac951c63391e92 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 7 Nov 2022 22:59:16 +0100 Subject: [PATCH 27/35] completed the refactored and improved version of the metadata search module :3 --- src/main.py | 34 ++++++++++++++------------------ src/metadata/object_handeling.py | 2 +- src/metadata/search.py | 9 +++++++++ 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/main.py b/src/main.py index 80a8e83..a5f25a0 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,7 @@ from metadata.database import Database from metadata.download import MetadataDownloader import metadata.download -import metadata.metadata +import metadata.search import download_links import url_to_path import download @@ -17,6 +17,7 @@ DATABASE_FILE = "metadata.db" DATABASE_STRUCTURE_FILE = "database_structure.sql" DATABASE_STRUCTURE_FALLBACK = "https://raw.githubusercontent.com/HeIIow2/music-downloader/new_metadata/assets/database_structure.sql" +SEARCH_LOGGER = logging.getLogger("mb-cli") DATABASE_LOGGER = logging.getLogger("database") METADATA_DOWNLOAD_LOGGER = logging.getLogger("metadata-download") URL_DOWNLOAD_LOGGER = logging.getLogger("ling-download") @@ -27,9 +28,6 @@ NOT_A_GENRE = ".", "..", "misc_scripts", "Music", "script", ".git", ".idea" MUSIC_DIR = os.path.expanduser('~/Music') TOR = False -logger = logging.getLogger() -logger.level = logging.DEBUG - temp_dir = os.path.join(tempfile.gettempdir(), TEMP_FOLDER) if not os.path.exists(temp_dir): os.mkdir(temp_dir) @@ -52,28 +50,26 @@ def get_existing_genre(): return valid_directories -def search_for_metadata(query: str): - search = metadata.metadata.Search(query=query) +def search_for_metadata(): + search = metadata.search.Search(logger=SEARCH_LOGGER) - print(search.options) while True: input_ = input( - "q to quit, ok to download, .. for previous options, . for current options, int for this element: ").lower() + "q to quit, .. for previous options, int for this element, str to search for query, ok to download\n") input_.strip() - if input_ == "q": - exit(0) - if input_ == "ok": - return search.current_chosen_option - if input_ == ".": - print(search.options) - continue - if input_ == "..": - print(search.get_previous_options()) + if input_.lower() == "ok": + break + if input_.lower() == "q": + break + if input_.lower() == "..": + search.get_previous_options() continue if input_.isdigit(): - print(search.choose(int(input_))) + search.choose(int(input_)) continue + search.search_from_query(input_) + return search.current_option def get_genre(): existing_genres = get_existing_genre() @@ -106,7 +102,7 @@ def cli(start_at: int = 0): logging.info(f"{genre} has been set as genre.") if start_at <= 0: - search = search_for_metadata(query=input("initial query: ")) + search = search_for_metadata() logging.info("Starting Downloading of metadata") metadata_downloader = MetadataDownloader(database, METADATA_DOWNLOAD_LOGGER) metadata_downloader.download(search) diff --git a/src/metadata/object_handeling.py b/src/metadata/object_handeling.py index 57a3b4d..7922603 100644 --- a/src/metadata/object_handeling.py +++ b/src/metadata/object_handeling.py @@ -19,6 +19,6 @@ def parse_music_brainz_date(mb_date: str) -> date: first_release_date = mb_date if first_release_date.count("-") == 2: year, month, day = [int(i) for i in first_release_date.split("-")] - elif first_release_date.count("-") == 0: + elif first_release_date.count("-") == 0 and first_release_date.isdigit(): year = int(first_release_date) return date(year, month, day) diff --git a/src/metadata/search.py b/src/metadata/search.py index 3030ec4..1ffbf47 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -26,6 +26,15 @@ class Option: self.additional_info = additional_info + def __getitem__(self, item): + map_ = { + "id": self.id, + "type": self.type, + "kind": self.type, + "name": self.name + } + return map_[item] + def __repr__(self) -> str: type_repr = { 'artist': 'artist\t\t', From ff468da16f2649e07081d55112a05ed7422b3344 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 09:16:27 +0100 Subject: [PATCH 28/35] added option to search with queries instead of defining artist/release group/recording --- src/metadata/search.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/metadata/search.py b/src/metadata/search.py index 1ffbf47..f3058b8 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -225,8 +225,8 @@ class Search: return self.fetch_new_options() @staticmethod - def search_recording_from_text(artist: str = None, release_group: str = None, recording: str = None): - result = musicbrainzngs.search_recordings(artist=artist, release=release_group, recording=recording) + def search_recording_from_text(artist: str = None, release_group: str = None, recording: str = None, query: str = None): + result = musicbrainzngs.search_recordings(artist=artist, release=release_group, recording=recording, query=query) recording_list = get_elem_from_obj(result, ['recording-list'], return_if_none=[]) resulting_options = [ @@ -236,8 +236,8 @@ class Search: return resulting_options @staticmethod - def search_release_group_from_text(artist: str = None, release_group: str = None): - result = musicbrainzngs.search_release_groups(artist=artist, releasegroup=release_group) + def search_release_group_from_text(artist: str = None, release_group: str = None, query: str = None): + result = musicbrainzngs.search_release_groups(artist=artist, releasegroup=release_group, query=query) release_group_list = get_elem_from_obj(result, ['release-group-list'], return_if_none=[]) resulting_options = [Option("release_group", get_elem_from_obj(release_group_, ['id']), @@ -247,13 +247,10 @@ class Search: return resulting_options @staticmethod - def search_artist_from_text(artist: str = None): - print(artist) - result = musicbrainzngs.search_artists(artist=artist) + def search_artist_from_text(artist: str = None, query: str = None): + result = musicbrainzngs.search_artists(artist=artist, query=query) artist_list = get_elem_from_obj(result, ['artist-list'], return_if_none=[]) - print(artist_list[0]) - resulting_options = [Option("artist", get_elem_from_obj(artist_, ['id']), get_elem_from_obj(artist_, ['name']), additional_info=f": {', '.join([i['name'] for i in get_elem_from_obj(artist_, ['tag-list'], return_if_none=[])])}") for artist_ in artist_list] @@ -275,15 +272,30 @@ class Search: self.append_new_choices(results) + def search_from_text_unspecified(self, query: str): + results = [] + results.extend(self.search_artist_from_text(query=query)) + results.extend(self.search_release_group_from_text(query=query)) + results.extend(self.search_recording_from_text(query=query)) + + self.append_new_choices(results) + def search_from_query(self, query: str): + if query is None: + return """ mit # wird ein neuer Parameter gestartet der Buchstabe dahinter legt die Art des Parameters fest "#a Psychonaut 4 #r Tired, Numb and #t Drop by Drop" + if no # is in the query it gets treated as "unspecified query" :param query: :return: """ + if not '#' in query: + self.search_from_text_unspecified(query) + return + artist = None release_group = None recording = None From d8b0a771cf736996cd312cc2cd35fc43432fd057 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 09:21:28 +0100 Subject: [PATCH 29/35] addet logging messages to searching --- src/metadata/search.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/metadata/search.py b/src/metadata/search.py index f3058b8..280a09b 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -257,6 +257,7 @@ class Search: return resulting_options def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None): + self.logger.info(f"searching specified artist: \"{artist}\", release group: \"{release_group}\", recording: \"{recording}\"") if artist is None and release_group is None and recording is None: self.logger.error("either artist, release group or recording has to be set") return -1 @@ -264,15 +265,20 @@ class Search: print() if recording is not None: + self.logger.info("search for recording") results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) elif release_group is not None: + self.logger.info("search for release group") results = self.search_release_group_from_text(artist=artist, release_group=release_group) else: + self.logger.info("search for artist") results = self.search_artist_from_text(artist=artist) self.append_new_choices(results) def search_from_text_unspecified(self, query: str): + self.logger.info(f"searching unspecified: \"{query}\"") + results = [] results.extend(self.search_artist_from_text(query=query)) results.extend(self.search_release_group_from_text(query=query)) From 273f94e81499f35602be1dc106ba69a823357717 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 10:39:27 +0100 Subject: [PATCH 30/35] refactored to print the search in the main file for easyer editing of cli things --- src/main.py | 9 +++++--- src/metadata/search.py | 47 +++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/main.py b/src/main.py index a5f25a0..759299d 100644 --- a/src/main.py +++ b/src/main.py @@ -62,12 +62,15 @@ def search_for_metadata(): if input_.lower() == "q": break if input_.lower() == "..": - search.get_previous_options() + print() + print(search.get_previous_options()) continue if input_.isdigit(): - search.choose(int(input_)) + print() + print(search.choose(int(input_))) continue - search.search_from_query(input_) + print() + print(search.search_from_query(input_)) return search.current_option diff --git a/src/metadata/search.py b/src/metadata/search.py index 280a09b..158fb31 100644 --- a/src/metadata/search.py +++ b/src/metadata/search.py @@ -44,6 +44,13 @@ class Option: } return f"{type_repr[self.type]}: \"{self.name}\"{self.additional_info}" +class MultipleOptions: + def __init__(self, option_list: List[Option]) -> None: + self.option_list = option_list + + def __repr__(self) -> str: + return "\n".join([f"{str(i).zfill(2)}) {choice.__repr__()}" for i, choice in enumerate(self.option_list)]) + class Search: def __init__(self, logger: logging.Logger) -> None: @@ -52,18 +59,13 @@ class Search: self.options_history = [] self.current_option: Option - def append_new_choices(self, new_choices: List[Option]): - print() - for i, choice in enumerate(new_choices): - print(f"{str(i).zfill(2)}) {choice}") - + def append_new_choices(self, new_choices: List[Option]) -> MultipleOptions: self.options_history.append(new_choices) + return MultipleOptions(new_choices) def get_previous_options(self): self.options_history.pop(-1) - print() - for i, choice in enumerate(self.options_history[-1]): - print(f"{str(i).zfill(2)}) {choice}") + return MultipleOptions(self.options_history[-1]) @staticmethod def fetch_new_options_from_artist(artist: Option): @@ -195,7 +197,7 @@ class Search: return results - def fetch_new_options(self): + def fetch_new_options(self) -> MultipleOptions: if self.current_option is None: return -1 @@ -209,17 +211,17 @@ class Search: elif self.current_option.type == 'recording': result = self.fetch_new_options_from_record(self.current_option) - self.append_new_choices(result) + return self.append_new_choices(result) - def choose(self, index: int): + def choose(self, index: int) -> MultipleOptions: if len(self.options_history) == 0: logging.error("initial query neaded before choosing") - return -1 + return MultipleOptions([]) latest_options = self.options_history[-1] if index >= len(latest_options): logging.error("index outside of options") - return -1 + return MultipleOptions([]) self.current_option = latest_options[index] return self.fetch_new_options() @@ -256,14 +258,12 @@ class Search: for artist_ in artist_list] return resulting_options - def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None): + def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None) -> MultipleOptions: self.logger.info(f"searching specified artist: \"{artist}\", release group: \"{release_group}\", recording: \"{recording}\"") if artist is None and release_group is None and recording is None: self.logger.error("either artist, release group or recording has to be set") return -1 - print() - if recording is not None: self.logger.info("search for recording") results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording) @@ -274,9 +274,9 @@ class Search: self.logger.info("search for artist") results = self.search_artist_from_text(artist=artist) - self.append_new_choices(results) + return self.append_new_choices(results) - def search_from_text_unspecified(self, query: str): + def search_from_text_unspecified(self, query: str) -> MultipleOptions: self.logger.info(f"searching unspecified: \"{query}\"") results = [] @@ -284,11 +284,11 @@ class Search: results.extend(self.search_release_group_from_text(query=query)) results.extend(self.search_recording_from_text(query=query)) - self.append_new_choices(results) + return self.append_new_choices(results) - def search_from_query(self, query: str): + def search_from_query(self, query: str) -> MultipleOptions: if query is None: - return + return MultipleOptions([]) """ mit # wird ein neuer Parameter gestartet der Buchstabe dahinter legt die Art des Parameters fest @@ -299,8 +299,7 @@ class Search: """ if not '#' in query: - self.search_from_text_unspecified(query) - return + return self.search_from_text_unspecified(query) artist = None release_group = None @@ -328,7 +327,7 @@ class Search: recording = input_ continue - self.search_from_text(artist=artist, release_group=release_group, recording=recording) + return self.search_from_text(artist=artist, release_group=release_group, recording=recording) def automated_demo(): From a6df17ab862fe70e358b096758e4795582040bfb Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 10:48:40 +0100 Subject: [PATCH 31/35] removed spamm of warnings --- src/download.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/download.py b/src/download.py index b53b13b..fcc7e7d 100644 --- a/src/download.py +++ b/src/download.py @@ -71,8 +71,6 @@ class Download: if type(row[key]) != list: row[key] = str(row[key]) audiofile[key] = row[key] - else: - self.logger.warning(key) self.logger.info("saving") audiofile.save(file_path, v1=2) From 452c1b5eeaaaab7b9bd9211028fbe73c247a1e19 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 11:30:54 +0100 Subject: [PATCH 32/35] removed unnecesarry file --- slavart.py | 12 ------------ src/download.py | 3 +-- src/download_links.py | 7 +++++-- src/main.py | 8 ++++---- src/{ => scraping}/musify.py | 5 ++++- src/{ => scraping}/phonetic_compares.py | 0 src/{ => scraping}/youtube_music.py | 5 ++++- 7 files changed, 18 insertions(+), 22 deletions(-) delete mode 100644 slavart.py rename src/{ => scraping}/musify.py (97%) rename src/{ => scraping}/phonetic_compares.py (100%) rename src/{ => scraping}/youtube_music.py (95%) diff --git a/slavart.py b/slavart.py deleted file mode 100644 index 614239d..0000000 --- a/slavart.py +++ /dev/null @@ -1,12 +0,0 @@ -import requests - -API_ENDPOINT = "https://slavart.gamesdrive.net/api/search?q=Tekkno" -DOWNLOAD_ENDPOINT = "https://slavart-api.gamesdrive.net/api/download/track?id=153182274" - - -if __name__ == "__main__": - r = requests.get(DOWNLOAD_ENDPOINT, headers={ - "Access-Control-Allow-Origin": "https://slavart.gamesdrive.net/" - }) - print(r.status_code) - print(r.text) diff --git a/src/download.py b/src/download.py index fcc7e7d..72ee974 100644 --- a/src/download.py +++ b/src/download.py @@ -5,8 +5,7 @@ from mutagen.easyid3 import EasyID3 from pydub import AudioSegment import logging -import musify -import youtube_music +from scraping import musify, youtube_music """ https://en.wikipedia.org/wiki/ID3 diff --git a/src/download_links.py b/src/download_links.py index b3e1e80..04eadde 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -1,8 +1,8 @@ import requests +import os import logging -import musify -import youtube_music +from scraping import musify, youtube_music class Download: @@ -18,6 +18,9 @@ class Download: row['artists'] = [artist['name'] for artist in row['artists']] id_ = row['id'] + if os.path.exists(os.path.join(os.path.expanduser('~/Music'), row['file'])): + self.logger.info(f"skipping the fetching of the download links, cuz {row['file']} already exists.") + continue # check YouTube youtube_url = youtube_music.get_youtube_url(row) diff --git a/src/main.py b/src/main.py index 759299d..3d2f378 100644 --- a/src/main.py +++ b/src/main.py @@ -111,13 +111,13 @@ def cli(start_at: int = 0): metadata_downloader.download(search) if start_at <= 1: - logging.info("Fetching Download Links") - download_links.Download(database, METADATA_DOWNLOAD_LOGGER, proxies=proxies) - - if start_at <= 2: logging.info("creating Paths") url_to_path.UrlPath(database, PATH_LOGGER, genre=genre) + if start_at <= 2: + logging.info("Fetching Download Links") + download_links.Download(database, METADATA_DOWNLOAD_LOGGER, proxies=proxies) + if start_at <= 3: logging.info("starting to download the mp3's") download.Download(database, DOWNLOAD_LOGGER, proxies=proxies, base_path=MUSIC_DIR) diff --git a/src/musify.py b/src/scraping/musify.py similarity index 97% rename from src/musify.py rename to src/scraping/musify.py index 7a0e23b..59771e7 100644 --- a/src/musify.py +++ b/src/scraping/musify.py @@ -4,7 +4,10 @@ import time import requests import bs4 -import phonetic_compares +try: + import phonetic_compares +except ModuleNotFoundError: + from scraping import phonetic_compares TRIES = 5 TIMEOUT = 10 diff --git a/src/phonetic_compares.py b/src/scraping/phonetic_compares.py similarity index 100% rename from src/phonetic_compares.py rename to src/scraping/phonetic_compares.py diff --git a/src/youtube_music.py b/src/scraping/youtube_music.py similarity index 95% rename from src/youtube_music.py rename to src/scraping/youtube_music.py index a138e2a..940f6be 100644 --- a/src/youtube_music.py +++ b/src/scraping/youtube_music.py @@ -3,7 +3,10 @@ import pandas as pd import logging import time -import phonetic_compares +try: + import phonetic_compares +except ModuleNotFoundError: + from scraping import phonetic_compares YDL_OPTIONS = {'format': 'bestaudio', 'noplaylist': 'True'} YOUTUBE_URL_KEY = 'webpage_url' From 39692901596ed2c5710d0d3eedc9754296cd41f8 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 12:08:36 +0100 Subject: [PATCH 33/35] removed unnecesarry file --- src/download.py | 3 --- src/download_links.py | 5 +++-- src/main.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/download.py b/src/download.py index 72ee974..f0c13e4 100644 --- a/src/download.py +++ b/src/download.py @@ -18,9 +18,6 @@ print(EasyID3.valid_keys.keys()) """ - - - class Download: def __init__(self, database, logger: logging.Logger, proxies: dict = None, base_path: str = ""): if proxies is not None: diff --git a/src/download_links.py b/src/download_links.py index 04eadde..73f0a2f 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -6,7 +6,8 @@ from scraping import musify, youtube_music class Download: - def __init__(self, database, logger: logging.Logger, proxies: dict = None) -> None: + def __init__(self, database, logger: logging.Logger, music_dir: str, proxies: dict = None) -> None: + self.music_dir = music_dir self.database = database self.logger = logger if proxies is not None: @@ -18,7 +19,7 @@ class Download: row['artists'] = [artist['name'] for artist in row['artists']] id_ = row['id'] - if os.path.exists(os.path.join(os.path.expanduser('~/Music'), row['file'])): + if os.path.exists(os.path.join(self.music_dir, row['file'])): self.logger.info(f"skipping the fetching of the download links, cuz {row['file']} already exists.") continue diff --git a/src/main.py b/src/main.py index 3d2f378..87ab696 100644 --- a/src/main.py +++ b/src/main.py @@ -116,7 +116,7 @@ def cli(start_at: int = 0): if start_at <= 2: logging.info("Fetching Download Links") - download_links.Download(database, METADATA_DOWNLOAD_LOGGER, proxies=proxies) + download_links.Download(database, METADATA_DOWNLOAD_LOGGER, MUSIC_DIR, proxies=proxies) if start_at <= 3: logging.info("starting to download the mp3's") From 2f1d97b67bc790e665ac223f37ff416c9a91eb5d Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 12:29:11 +0100 Subject: [PATCH 34/35] removed unnecesarry file --- src/download_links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download_links.py b/src/download_links.py index 73f0a2f..01eca5c 100644 --- a/src/download_links.py +++ b/src/download_links.py @@ -15,7 +15,7 @@ class Download: self.urls = [] - for row in self.database.get_tracks_to_download(): + for row in self.database.get_tracks_without_src(): row['artists'] = [artist['name'] for artist in row['artists']] id_ = row['id'] From 63f30bffbae20ec3fc368a6093b28e56f0230318 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Tue, 8 Nov 2022 14:44:33 +0100 Subject: [PATCH 35/35] asdf --- src/scraping/musify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scraping/musify.py b/src/scraping/musify.py index 59771e7..1d68ffb 100644 --- a/src/scraping/musify.py +++ b/src/scraping/musify.py @@ -52,7 +52,10 @@ def get_download_link(default_url): def download_from_musify(file, url): logging.info(f"downloading: '{url}'") - r = session.get(url) + try: + r = session.get(url, timeout=15) + except requests.exceptions.ConnectionError: + return -1 if r.status_code != 200: if r.status_code == 404: logging.warning(f"{r.url} was not found")