From 04da522b64e32a10a32243f807a797bafe25fbd1 Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:14:12 +0300 Subject: [PATCH 01/18] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..508172a --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# spotifyFetcher +loads all data through web api From 506258f9b12ad3ddccdeb637ebdc3b84d111133c Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:14:12 +0300 Subject: [PATCH 02/18] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..508172a --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# spotifyFetcher +loads all data through web api From bb5453b59eb87d2d8c4d6b9a46852247019b7cf4 Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:14:29 +0300 Subject: [PATCH 03/18] initial --- .gitignore | 4 ++ README.md | 3 +- common.py | 25 ++++++++ loadArtwork.py | 45 +++++++++++++++ main.py | 106 ++++++++++++++++++++++++++++++++++ spotifyAuth.py | 122 ++++++++++++++++++++++++++++++++++++++++ spotifyFetch.py | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 common.py create mode 100644 loadArtwork.py create mode 100644 main.py create mode 100644 spotifyAuth.py create mode 100644 spotifyFetch.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2803b17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +prefetched* +__pycache__ +.idea +secret diff --git a/README.md b/README.md index 508172a..68b9ba5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # spotifyFetcher -loads all data through web api +loads all data through web api
+client secret - ***REDACTED*** diff --git a/common.py b/common.py new file mode 100644 index 0000000..bb08d3e --- /dev/null +++ b/common.py @@ -0,0 +1,25 @@ +import json +import os + +data_dir = 'prefetched' + + +def get_data(name): + file_path = os.path.join(data_dir, f"{name}.json") + try: + with open(file_path, 'r') as file: + data = json.load(file) + return data + except FileNotFoundError: + return f"File {name}.json not found." + except json.JSONDecodeError: + return f"Error decoding JSON from {name}.json." + + +def save_data(data, name): + os.makedirs(data_dir, exist_ok=True) + + file_path = os.path.join(data_dir, f"{name}.json") + with open(file_path, 'w') as file: + json.dump(data, file, indent=4) + return f"Data saved to {name}.json." \ No newline at end of file diff --git a/loadArtwork.py b/loadArtwork.py new file mode 100644 index 0000000..4d2dc38 --- /dev/null +++ b/loadArtwork.py @@ -0,0 +1,45 @@ +import requests +import os +from PIL import Image +from io import BytesIO + +from common import * + + +def save_image_from_url(url, name, image_dir="playlist_covers"): + response = requests.get(url) + + if response.status_code != 200: + raise "Cannot fetch the playlist cover" + + image = Image.open(BytesIO(response.content)) + + directory = os.path.join(data_dir, image_dir) + os.makedirs(directory, exist_ok=True) + file_path = os.path.join(directory, f"{name}.jpg") + print(f"Image saved {file_path}") + image.save(file_path) + + +def load_playlist_covers(): + playlists = get_data('playlists') + + for pl in playlists: + if len(pl['images']): + url = pl['images'][0]['url'] + save_image_from_url(url, pl['name']) + + +def load_user_cover(): + user_data = get_data("user_data") + url = user_data['images'][1]['url'] + save_image_from_url(url, "user", ".") + + +def load(): + load_user_cover() + load_playlist_covers() + + +if __name__ == "__main__": + load() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c88776b --- /dev/null +++ b/main.py @@ -0,0 +1,106 @@ +from common import * +from spotifyFetch import fetch_data +import cmd + +playlists = get_data('playlistTracks') +top_artists = get_data('top_artists') +top_tracks = get_data('top_tracks') +user_tracks = get_data('tracks') + + +def get_playlist_names(): + return [name for name, items in playlists.items()] + + +def get_artists_str(artists_pack): + track_artists = [artist['name'] for artist in artists_pack] + artists = " ".join(track_artists) + return artists + + +def print_playlist_tracks(name): + print(name) + for track in playlists[name]: + track_data = track['track'] + track_name = track_data['name'] + artists = get_artists_str(track_data['artists']) + print(f" '{track_name}' - '{artists}' ") + + +def print_top_artists(): + for artist in top_artists: + name = artist['name'] + print(f" '{name}'") + + +def print_top_tracks(): + for track in top_tracks: + track_name = track['name'] + artists = get_artists_str(track['artists']) + print(f" '{track_name}' - '{artists}' ") + + +def print_stats(): + print(f" tracks - {len(user_tracks)}") + print(f" playlists - {len(playlists)}") + + for name, tracks in playlists.items(): + print(f" '{name}' - {len(tracks)}") + + print(f" top tracks - {len(top_tracks)}") + print(f" top artists - {len(top_artists)}") + + +def print_tracks(): + for trackItem in user_tracks: + track = trackItem['track'] + print(f" '{track['name']}' - {get_artists_str(track['artists'])}") + + +class Interpreter(cmd.Cmd): + intro = "Welcome to the interactive command loop. Type help or ? to list commands.\n" + prompt = "(spotify) " + + def do_playlists(self, arg): + """prints user playlists""" + print("\n".join(get_playlist_names())) + + def do_playlist_tracks(self, arg): + """prints playlist [name]""" + try: + name = arg.split()[0] + print_playlist_tracks(name) + except IndexError: + print("Invalid input") + + def do_top_artists(self, arg): + """Top artists""" + print_top_artists() + + def do_top_tracks(self, arg): + """Top tracks""" + print_top_tracks() + + def do_stat(self, arg): + """Print stats""" + print_stats() + + def do_tracks(self, arg): + """Print tracks""" + print_tracks() + + def do_fetch(self, arg): + """Fetch all spotify data""" + try: + fetch_data() + except ValueError: + print("Can not fetch the data.") + + def do_exit(self, arg): + """Exit the command loop: exit""" + print("Goodbye!") + return True + + +if __name__ == '__main__': + Interpreter().cmdloop() diff --git a/spotifyAuth.py b/spotifyAuth.py new file mode 100644 index 0000000..64a4b2c --- /dev/null +++ b/spotifyAuth.py @@ -0,0 +1,122 @@ +import base64 + +import requests +import webbrowser +from http.server import BaseHTTPRequestHandler, HTTPServer +import urllib.parse +import threading +import time +import os.path + +client_id = '***REDACTED***' +client_secret = None +token = None + + +def resolve_client_secret(): + global client_secret + if os.path.exists('secret'): + with open('secret', 'r') as file: + client_secret = file.readline().strip() + else: + client_secret = input("Enter the application secret: ") + with open('secret', 'w') as file: + file.write(client_secret) + + +def retrieve_web_api_token(authorization_code): + global token + + token_url = 'https://accounts.spotify.com/api/token' + + token_data = { + "grant_type": "authorization_code", + "client_secret": client_secret, + "client_id": client_id, + "code": authorization_code, + 'redirect_uri': 'http://localhost:8888', + } + + print("\nAuthenticating WEB API\n") + + auth_string = f'{client_id}:{client_secret}' + b64_auth_string = base64.b64encode(auth_string.encode()).decode() + + # Define the headers + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {b64_auth_string}' + } + + token_response = requests.post(token_url, data=token_data, headers=headers) + + if token_response.status_code not in range(200, 299): + print(f"Failed to retrieve access token: {token_response.status_code}") + print(token_response.json()) + raise ValueError + + token = token_response.json()['access_token'] + + +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + + parsed_url = urllib.parse.urlparse(self.path) + query_params = urllib.parse.parse_qs(parsed_url.query) + + if 'code' in query_params: + authorization_code = query_params['code'][0] + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b'Authorization code received. You can close this window.') + + retrieve_web_api_token(authorization_code) + + threading.Thread(target=self.server.shutdown).start() + + else: + self.send_response(400) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b'Authorization code not found.') + + +def run_redirect_server(): + httpd = HTTPServer(('localhost', 8888), RequestHandler) + print('Starting HTTP redirection server') + httpd.serve_forever() + + +def open_authenticate_page(): + authorize_url = 'https://accounts.spotify.com/authorize' + + params = { + 'client_id': client_id, + 'response_type': 'code', + 'redirect_uri': 'http://localhost:8888', + "scope": "user-top-read playlist-read-collaborative user-read-email user-library-read user-read-private playlist-read-private", + } + + auth_url = f"{authorize_url}?client_id={params['client_id']}&response_type={params['response_type']}&redirect_uri={params['redirect_uri']}&scope={params['scope']}" + webbrowser.open(auth_url) + + +def authenticate(): + resolve_client_secret() + + redirect_server = threading.Thread(target=run_redirect_server) + redirect_server.start() + + time.sleep(1) + open_authenticate_page() + + redirect_server.join() + + return token + + +if __name__ == "__main__": + authenticate() + diff --git a/spotifyFetch.py b/spotifyFetch.py new file mode 100644 index 0000000..ec590bc --- /dev/null +++ b/spotifyFetch.py @@ -0,0 +1,147 @@ +import requests +from loadArtwork import load +from spotifyAuth import authenticate +from common import * + +FETCH_STEP = 50 +token = '' + + +def fetch(endpoint, method='GET', body=None): + url = f'https://api.spotify.com/{endpoint}' + headers = { + 'Authorization': f'Bearer {token}', + } + + response = requests.request(method, url, headers=headers, json=body) + response.raise_for_status() + return response.json() + + +def fetch_playlists(user_id): + print("Fetching playlists") + endpoint = f"v1/me/playlists" + pl_total = fetch(f'{endpoint}?limit=1')['total'] + pl_count = 0 + playlists = [] + + while pl_count < pl_total: + res = fetch(f'{endpoint}?limit={FETCH_STEP}&offset={pl_count}') + playlists += res['items'] + pl_count += FETCH_STEP + + return playlists + + +def fetch_playlist_items(playlist_id): + endpoint = f"v1/playlists/{playlist_id}/tracks" + items = fetch(f'{endpoint}?fields=total')['total'] + loaded = 0 + tracks = [] + + while loaded < items: + res = fetch(f'{endpoint}?limit={FETCH_STEP}&offset={loaded}') + + tracks += res['items'] + loaded += FETCH_STEP + + return tracks + + +def fetch_tracks(user_id): + print("Fetching tracks") + endpoint = f"v1/me/tracks" + total = fetch(f"{endpoint}?limit=1&market=ES")['total'] + current = 0 + tracks = [] + + while total > current: + res = fetch(f"{endpoint}?limit={FETCH_STEP}&offset={current}&market=ES") + tracks += res['items'] + current += FETCH_STEP + + save_data(tracks, "tracks") + return tracks + + +def fetch_tracks_top(): + print("Fetching top tracks") + total = fetch(f'v1/me/top/tracks?fields=total')['total'] + current = 0 + tracks = [] + + while current < total: + res = fetch(f'v1/me/top/tracks?limit={FETCH_STEP}&offset={current}&time_range=long_term') + tracks += res['items'] + current += FETCH_STEP + + save_data(tracks, "top_tracks") + return tracks + + +def fetch_artists_top(): + print("Fetching top artists") + + total = fetch(f'v1/me/top/artists?fields=total')['total'] + current = 0 + artists = [] + + while current < total: + res = fetch(f'v1/me/top/artists?limit={FETCH_STEP}&offset={current}&time_range=long_term') + artists += res['items'] + current += FETCH_STEP + + save_data(artists, "top_artists") + return artists + + +def fetch_all_playlist_data(user_id): + playlists = fetch_playlists(user_id) + + save_data(playlists, 'playlists') + + print(len(playlists)) + + playlist_items = {} + + print("Fetching playlists items") + + index = 0 + for pl in playlists: + pl_id = pl['id'] + name = pl['name'] + playlist_items[name] = fetch_playlist_items(pl_id) + print(f"{index} - {len(playlist_items[name])}") + index += 1 + + save_data(playlist_items, "playlistTracks") + + +def get_user_data(): + print("Fetching user data") + + user_profile = fetch(f'v1/me') + user_id = user_profile['id'] + user_data = fetch(f'v1/users/{user_id}') + + save_data(user_profile, "user_profile") + save_data(user_data, "user_data") + return user_id + + +def fetch_data(): + global token + token = authenticate() + + user_id = get_user_data() + + fetch_tracks(user_id) + fetch_all_playlist_data(user_id) + fetch_tracks_top() + fetch_artists_top() + + load() + + +if __name__ == "__main__": + fetch_data() From 6703fdd9681e80ca07adcbc7f28a98a107cd0086 Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:14:29 +0300 Subject: [PATCH 04/18] initial --- .gitignore | 4 ++ README.md | 3 +- common.py | 25 ++++++++ loadArtwork.py | 45 +++++++++++++++ main.py | 106 ++++++++++++++++++++++++++++++++++ spotifyAuth.py | 122 ++++++++++++++++++++++++++++++++++++++++ spotifyFetch.py | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 common.py create mode 100644 loadArtwork.py create mode 100644 main.py create mode 100644 spotifyAuth.py create mode 100644 spotifyFetch.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2803b17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +prefetched* +__pycache__ +.idea +secret diff --git a/README.md b/README.md index 508172a..4a45af3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # spotifyFetcher -loads all data through web api +loads all data through web api
+client secret - 4ced4e4575094b749834a60996680c6c diff --git a/common.py b/common.py new file mode 100644 index 0000000..bb08d3e --- /dev/null +++ b/common.py @@ -0,0 +1,25 @@ +import json +import os + +data_dir = 'prefetched' + + +def get_data(name): + file_path = os.path.join(data_dir, f"{name}.json") + try: + with open(file_path, 'r') as file: + data = json.load(file) + return data + except FileNotFoundError: + return f"File {name}.json not found." + except json.JSONDecodeError: + return f"Error decoding JSON from {name}.json." + + +def save_data(data, name): + os.makedirs(data_dir, exist_ok=True) + + file_path = os.path.join(data_dir, f"{name}.json") + with open(file_path, 'w') as file: + json.dump(data, file, indent=4) + return f"Data saved to {name}.json." \ No newline at end of file diff --git a/loadArtwork.py b/loadArtwork.py new file mode 100644 index 0000000..4d2dc38 --- /dev/null +++ b/loadArtwork.py @@ -0,0 +1,45 @@ +import requests +import os +from PIL import Image +from io import BytesIO + +from common import * + + +def save_image_from_url(url, name, image_dir="playlist_covers"): + response = requests.get(url) + + if response.status_code != 200: + raise "Cannot fetch the playlist cover" + + image = Image.open(BytesIO(response.content)) + + directory = os.path.join(data_dir, image_dir) + os.makedirs(directory, exist_ok=True) + file_path = os.path.join(directory, f"{name}.jpg") + print(f"Image saved {file_path}") + image.save(file_path) + + +def load_playlist_covers(): + playlists = get_data('playlists') + + for pl in playlists: + if len(pl['images']): + url = pl['images'][0]['url'] + save_image_from_url(url, pl['name']) + + +def load_user_cover(): + user_data = get_data("user_data") + url = user_data['images'][1]['url'] + save_image_from_url(url, "user", ".") + + +def load(): + load_user_cover() + load_playlist_covers() + + +if __name__ == "__main__": + load() diff --git a/main.py b/main.py new file mode 100644 index 0000000..c88776b --- /dev/null +++ b/main.py @@ -0,0 +1,106 @@ +from common import * +from spotifyFetch import fetch_data +import cmd + +playlists = get_data('playlistTracks') +top_artists = get_data('top_artists') +top_tracks = get_data('top_tracks') +user_tracks = get_data('tracks') + + +def get_playlist_names(): + return [name for name, items in playlists.items()] + + +def get_artists_str(artists_pack): + track_artists = [artist['name'] for artist in artists_pack] + artists = " ".join(track_artists) + return artists + + +def print_playlist_tracks(name): + print(name) + for track in playlists[name]: + track_data = track['track'] + track_name = track_data['name'] + artists = get_artists_str(track_data['artists']) + print(f" '{track_name}' - '{artists}' ") + + +def print_top_artists(): + for artist in top_artists: + name = artist['name'] + print(f" '{name}'") + + +def print_top_tracks(): + for track in top_tracks: + track_name = track['name'] + artists = get_artists_str(track['artists']) + print(f" '{track_name}' - '{artists}' ") + + +def print_stats(): + print(f" tracks - {len(user_tracks)}") + print(f" playlists - {len(playlists)}") + + for name, tracks in playlists.items(): + print(f" '{name}' - {len(tracks)}") + + print(f" top tracks - {len(top_tracks)}") + print(f" top artists - {len(top_artists)}") + + +def print_tracks(): + for trackItem in user_tracks: + track = trackItem['track'] + print(f" '{track['name']}' - {get_artists_str(track['artists'])}") + + +class Interpreter(cmd.Cmd): + intro = "Welcome to the interactive command loop. Type help or ? to list commands.\n" + prompt = "(spotify) " + + def do_playlists(self, arg): + """prints user playlists""" + print("\n".join(get_playlist_names())) + + def do_playlist_tracks(self, arg): + """prints playlist [name]""" + try: + name = arg.split()[0] + print_playlist_tracks(name) + except IndexError: + print("Invalid input") + + def do_top_artists(self, arg): + """Top artists""" + print_top_artists() + + def do_top_tracks(self, arg): + """Top tracks""" + print_top_tracks() + + def do_stat(self, arg): + """Print stats""" + print_stats() + + def do_tracks(self, arg): + """Print tracks""" + print_tracks() + + def do_fetch(self, arg): + """Fetch all spotify data""" + try: + fetch_data() + except ValueError: + print("Can not fetch the data.") + + def do_exit(self, arg): + """Exit the command loop: exit""" + print("Goodbye!") + return True + + +if __name__ == '__main__': + Interpreter().cmdloop() diff --git a/spotifyAuth.py b/spotifyAuth.py new file mode 100644 index 0000000..529c702 --- /dev/null +++ b/spotifyAuth.py @@ -0,0 +1,122 @@ +import base64 + +import requests +import webbrowser +from http.server import BaseHTTPRequestHandler, HTTPServer +import urllib.parse +import threading +import time +import os.path + +client_id = '3b4db45bb66b45e9a2532989c8034332' +client_secret = None +token = None + + +def resolve_client_secret(): + global client_secret + if os.path.exists('secret'): + with open('secret', 'r') as file: + client_secret = file.readline().strip() + else: + client_secret = input("Enter the application secret: ") + with open('secret', 'w') as file: + file.write(client_secret) + + +def retrieve_web_api_token(authorization_code): + global token + + token_url = 'https://accounts.spotify.com/api/token' + + token_data = { + "grant_type": "authorization_code", + "client_secret": client_secret, + "client_id": client_id, + "code": authorization_code, + 'redirect_uri': 'http://localhost:8888', + } + + print("\nAuthenticating WEB API\n") + + auth_string = f'{client_id}:{client_secret}' + b64_auth_string = base64.b64encode(auth_string.encode()).decode() + + # Define the headers + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {b64_auth_string}' + } + + token_response = requests.post(token_url, data=token_data, headers=headers) + + if token_response.status_code not in range(200, 299): + print(f"Failed to retrieve access token: {token_response.status_code}") + print(token_response.json()) + raise ValueError + + token = token_response.json()['access_token'] + + +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + + parsed_url = urllib.parse.urlparse(self.path) + query_params = urllib.parse.parse_qs(parsed_url.query) + + if 'code' in query_params: + authorization_code = query_params['code'][0] + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b'Authorization code received. You can close this window.') + + retrieve_web_api_token(authorization_code) + + threading.Thread(target=self.server.shutdown).start() + + else: + self.send_response(400) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b'Authorization code not found.') + + +def run_redirect_server(): + httpd = HTTPServer(('localhost', 8888), RequestHandler) + print('Starting HTTP redirection server') + httpd.serve_forever() + + +def open_authenticate_page(): + authorize_url = 'https://accounts.spotify.com/authorize' + + params = { + 'client_id': client_id, + 'response_type': 'code', + 'redirect_uri': 'http://localhost:8888', + "scope": "user-top-read playlist-read-collaborative user-read-email user-library-read user-read-private playlist-read-private", + } + + auth_url = f"{authorize_url}?client_id={params['client_id']}&response_type={params['response_type']}&redirect_uri={params['redirect_uri']}&scope={params['scope']}" + webbrowser.open(auth_url) + + +def authenticate(): + resolve_client_secret() + + redirect_server = threading.Thread(target=run_redirect_server) + redirect_server.start() + + time.sleep(1) + open_authenticate_page() + + redirect_server.join() + + return token + + +if __name__ == "__main__": + authenticate() + diff --git a/spotifyFetch.py b/spotifyFetch.py new file mode 100644 index 0000000..ec590bc --- /dev/null +++ b/spotifyFetch.py @@ -0,0 +1,147 @@ +import requests +from loadArtwork import load +from spotifyAuth import authenticate +from common import * + +FETCH_STEP = 50 +token = '' + + +def fetch(endpoint, method='GET', body=None): + url = f'https://api.spotify.com/{endpoint}' + headers = { + 'Authorization': f'Bearer {token}', + } + + response = requests.request(method, url, headers=headers, json=body) + response.raise_for_status() + return response.json() + + +def fetch_playlists(user_id): + print("Fetching playlists") + endpoint = f"v1/me/playlists" + pl_total = fetch(f'{endpoint}?limit=1')['total'] + pl_count = 0 + playlists = [] + + while pl_count < pl_total: + res = fetch(f'{endpoint}?limit={FETCH_STEP}&offset={pl_count}') + playlists += res['items'] + pl_count += FETCH_STEP + + return playlists + + +def fetch_playlist_items(playlist_id): + endpoint = f"v1/playlists/{playlist_id}/tracks" + items = fetch(f'{endpoint}?fields=total')['total'] + loaded = 0 + tracks = [] + + while loaded < items: + res = fetch(f'{endpoint}?limit={FETCH_STEP}&offset={loaded}') + + tracks += res['items'] + loaded += FETCH_STEP + + return tracks + + +def fetch_tracks(user_id): + print("Fetching tracks") + endpoint = f"v1/me/tracks" + total = fetch(f"{endpoint}?limit=1&market=ES")['total'] + current = 0 + tracks = [] + + while total > current: + res = fetch(f"{endpoint}?limit={FETCH_STEP}&offset={current}&market=ES") + tracks += res['items'] + current += FETCH_STEP + + save_data(tracks, "tracks") + return tracks + + +def fetch_tracks_top(): + print("Fetching top tracks") + total = fetch(f'v1/me/top/tracks?fields=total')['total'] + current = 0 + tracks = [] + + while current < total: + res = fetch(f'v1/me/top/tracks?limit={FETCH_STEP}&offset={current}&time_range=long_term') + tracks += res['items'] + current += FETCH_STEP + + save_data(tracks, "top_tracks") + return tracks + + +def fetch_artists_top(): + print("Fetching top artists") + + total = fetch(f'v1/me/top/artists?fields=total')['total'] + current = 0 + artists = [] + + while current < total: + res = fetch(f'v1/me/top/artists?limit={FETCH_STEP}&offset={current}&time_range=long_term') + artists += res['items'] + current += FETCH_STEP + + save_data(artists, "top_artists") + return artists + + +def fetch_all_playlist_data(user_id): + playlists = fetch_playlists(user_id) + + save_data(playlists, 'playlists') + + print(len(playlists)) + + playlist_items = {} + + print("Fetching playlists items") + + index = 0 + for pl in playlists: + pl_id = pl['id'] + name = pl['name'] + playlist_items[name] = fetch_playlist_items(pl_id) + print(f"{index} - {len(playlist_items[name])}") + index += 1 + + save_data(playlist_items, "playlistTracks") + + +def get_user_data(): + print("Fetching user data") + + user_profile = fetch(f'v1/me') + user_id = user_profile['id'] + user_data = fetch(f'v1/users/{user_id}') + + save_data(user_profile, "user_profile") + save_data(user_data, "user_data") + return user_id + + +def fetch_data(): + global token + token = authenticate() + + user_id = get_user_data() + + fetch_tracks(user_id) + fetch_all_playlist_data(user_id) + fetch_tracks_top() + fetch_artists_top() + + load() + + +if __name__ == "__main__": + fetch_data() From 366860a1ad3efd32572eb16e6c71d3a85cfd6425 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Mon, 8 Jul 2024 23:24:51 +0300 Subject: [PATCH 05/18] atmop --- main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.py b/main.py index c88776b..49af365 100644 --- a/main.py +++ b/main.py @@ -93,6 +93,14 @@ class Interpreter(cmd.Cmd): """Fetch all spotify data""" try: fetch_data() + + global playlists, top_artists, top_tracks, user_tracks + + playlists = get_data('playlistTracks') + top_artists = get_data('top_artists') + top_tracks = get_data('top_tracks') + user_tracks = get_data('tracks') + except ValueError: print("Can not fetch the data.") From 6eea2ec040c53f86ac907651567c1394fc5fb96e Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Mon, 8 Jul 2024 23:24:51 +0300 Subject: [PATCH 06/18] atmop --- main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.py b/main.py index c88776b..49af365 100644 --- a/main.py +++ b/main.py @@ -93,6 +93,14 @@ class Interpreter(cmd.Cmd): """Fetch all spotify data""" try: fetch_data() + + global playlists, top_artists, top_tracks, user_tracks + + playlists = get_data('playlistTracks') + top_artists = get_data('top_artists') + top_tracks = get_data('top_tracks') + user_tracks = get_data('tracks') + except ValueError: print("Can not fetch the data.") From 22e19557d81121badfe7fbeee3b33e16a48653b9 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Wed, 10 Jul 2024 15:42:15 +0300 Subject: [PATCH 07/18] tmp --- common.py | 8 +++- libResolver.py | 97 +++++++++++++++++++++++++++++++++++++++++++ main.py | 6 --- spotifyFetch.py | 26 ++++++++++++ spotifyJsonCleanUp.py | 32 ++++++++++++++ 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 libResolver.py create mode 100644 spotifyJsonCleanUp.py diff --git a/common.py b/common.py index bb08d3e..ba9c296 100644 --- a/common.py +++ b/common.py @@ -22,4 +22,10 @@ def save_data(data, name): file_path = os.path.join(data_dir, f"{name}.json") with open(file_path, 'w') as file: json.dump(data, file, indent=4) - return f"Data saved to {name}.json." \ No newline at end of file + return f"Data saved to {name}.json." + + +def get_artists_str(artists_pack): + track_artists = [artist['name'] for artist in artists_pack] + artists = " ".join(track_artists) + return artists diff --git a/libResolver.py b/libResolver.py new file mode 100644 index 0000000..9d89ffe --- /dev/null +++ b/libResolver.py @@ -0,0 +1,97 @@ +import common +import os +import fnmatch +import json +from fuzzywuzzy import fuzz + + +def get_local_library(root_dir): + music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') + music_files_map = {} + for dir_path, dir_names, filenames in os.walk(root_dir): + for pattern in music_extensions: + for filename in fnmatch.filter(filenames, pattern): + song_name = os.path.splitext(filename)[0] + relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) + music_files_map[song_name] = relative_path + return music_files_map + + +def get_matched(pattern, patterns): + result = {} + for target_pattern in patterns: + similarity_score = fuzz.token_set_ratio(pattern, target_pattern) + result[target_pattern] = similarity_score / 100 + + sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) + return sorted_results + + +def resolve_tracks(source_patterns, target_patterns): + from tqdm import tqdm + + pattern_map = {} + + total = len(source_patterns) + with tqdm(total=total, desc='Progress') as pbar: + for pattern in source_patterns: + res = get_matched(pattern, target_patterns) + matched = {"score": 0, "items": []} + if len(res) and res[0][1] > 0.9: + matched['items'] = res[0:min(len(res), 2)] + matched['score'] = res[0][1] + pattern_map[pattern] = matched + pbar.update(1) + + return pattern_map + + +def resolver1(local_library): + track_descriptions = common.get_data("tracks") + + source_patterns = [] + for track in track_descriptions: + artists = common.get_artists_str(track['track']['artists']) + pattern = f"{track['track']['name']} {artists}" + source_patterns.append(pattern) + + target_patterns = [local for _, local in local_library.items()] + + pattern_map = resolve_tracks(source_patterns, target_patterns) + + with open('prefetched/resolved.json', 'w') as file: + json.dump(pattern_map, file, indent=2) + + +def resolver2(local_library): + found_tracks = common.get_data("local_library_on_spotify") + track_descriptions = common.get_data("tracks") + + user_tracks_ids = {track['track']['id']: track for track in track_descriptions} + + resolved_map = {} + + for name, found in found_tracks.items(): + if not len(found): + continue + track_id = found[0]['id'] + if track_id in user_tracks_ids: + resolved_map[name] = track_id + + with open('prefetched/resolved2.json', 'w') as file: + json.dump(resolved_map, file, indent=2) + +def resolve(root_directory): + local_library = get_local_library(root_directory) + with open('prefetched/local_library.json', 'w') as file: + json.dump(local_library, file, indent=2) + + resolver2(local_library) + + +if __name__ == "__main__": + # root_directory = "/mnt/main/data/music/raw" + #root_directory = input("root: ") + resolve("/mnt/main/data/music/raw") + + diff --git a/main.py b/main.py index 49af365..f74ae1c 100644 --- a/main.py +++ b/main.py @@ -12,12 +12,6 @@ def get_playlist_names(): return [name for name, items in playlists.items()] -def get_artists_str(artists_pack): - track_artists = [artist['name'] for artist in artists_pack] - artists = " ".join(track_artists) - return artists - - def print_playlist_tracks(name): print(name) for track in playlists[name]: diff --git a/spotifyFetch.py b/spotifyFetch.py index ec590bc..6827049 100644 --- a/spotifyFetch.py +++ b/spotifyFetch.py @@ -129,10 +129,36 @@ def get_user_data(): return user_id +def find_song(song_pattern): + tracks = fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] + return tracks + + +def find_local_library(): + with open('prefetched/local_library.json', 'r') as file: + local_library = json.load(file) + + from tqdm import tqdm + + pattern_map = {} + + total = len(local_library) + with tqdm(total=total, desc='Searching local songs on spotify') as pbar: + found_tracks = {} + for track, path in local_library.items(): + found_tracks[track] = find_song(track) + pbar.update(1) + + with open('prefetched/local_library_on_spotify.json', 'w') as file: + json.dump(found_tracks, file, indent=2) + + def fetch_data(): global token token = authenticate() + find_local_library() + user_id = get_user_data() fetch_tracks(user_id) diff --git a/spotifyJsonCleanUp.py b/spotifyJsonCleanUp.py new file mode 100644 index 0000000..7501fbb --- /dev/null +++ b/spotifyJsonCleanUp.py @@ -0,0 +1,32 @@ +import os +import json + + +def process_json_files(directory): + for filename in os.listdir(directory): + if filename.endswith('.json') and ("_mod.json" not in filename): + filepath = os.path.join(directory, filename) + + with open(filepath, 'r') as f: + try: + data = json.load(f) + except json.JSONDecodeError: + continue + + def remove_available_markets(obj): + if isinstance(obj, dict): + return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} + elif isinstance(obj, list): + return [remove_available_markets(item) for item in obj] + else: + return obj + + modified_data = remove_available_markets(data) + + modified_filepath = os.path.join(directory, f"{os.path.splitext(filename)[0]}_mod.json") + with open(modified_filepath, 'w') as f: + json.dump(modified_data, f, indent=4) + + +if __name__ == "__main__": + process_json_files("prefetched") From 1ed118b31baee3eacab7a99d2d97d644421b85dd Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Wed, 10 Jul 2024 15:42:15 +0300 Subject: [PATCH 08/18] tmp --- common.py | 8 +++- libResolver.py | 97 +++++++++++++++++++++++++++++++++++++++++++ main.py | 6 --- spotifyFetch.py | 26 ++++++++++++ spotifyJsonCleanUp.py | 32 ++++++++++++++ 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 libResolver.py create mode 100644 spotifyJsonCleanUp.py diff --git a/common.py b/common.py index bb08d3e..ba9c296 100644 --- a/common.py +++ b/common.py @@ -22,4 +22,10 @@ def save_data(data, name): file_path = os.path.join(data_dir, f"{name}.json") with open(file_path, 'w') as file: json.dump(data, file, indent=4) - return f"Data saved to {name}.json." \ No newline at end of file + return f"Data saved to {name}.json." + + +def get_artists_str(artists_pack): + track_artists = [artist['name'] for artist in artists_pack] + artists = " ".join(track_artists) + return artists diff --git a/libResolver.py b/libResolver.py new file mode 100644 index 0000000..9d89ffe --- /dev/null +++ b/libResolver.py @@ -0,0 +1,97 @@ +import common +import os +import fnmatch +import json +from fuzzywuzzy import fuzz + + +def get_local_library(root_dir): + music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') + music_files_map = {} + for dir_path, dir_names, filenames in os.walk(root_dir): + for pattern in music_extensions: + for filename in fnmatch.filter(filenames, pattern): + song_name = os.path.splitext(filename)[0] + relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) + music_files_map[song_name] = relative_path + return music_files_map + + +def get_matched(pattern, patterns): + result = {} + for target_pattern in patterns: + similarity_score = fuzz.token_set_ratio(pattern, target_pattern) + result[target_pattern] = similarity_score / 100 + + sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) + return sorted_results + + +def resolve_tracks(source_patterns, target_patterns): + from tqdm import tqdm + + pattern_map = {} + + total = len(source_patterns) + with tqdm(total=total, desc='Progress') as pbar: + for pattern in source_patterns: + res = get_matched(pattern, target_patterns) + matched = {"score": 0, "items": []} + if len(res) and res[0][1] > 0.9: + matched['items'] = res[0:min(len(res), 2)] + matched['score'] = res[0][1] + pattern_map[pattern] = matched + pbar.update(1) + + return pattern_map + + +def resolver1(local_library): + track_descriptions = common.get_data("tracks") + + source_patterns = [] + for track in track_descriptions: + artists = common.get_artists_str(track['track']['artists']) + pattern = f"{track['track']['name']} {artists}" + source_patterns.append(pattern) + + target_patterns = [local for _, local in local_library.items()] + + pattern_map = resolve_tracks(source_patterns, target_patterns) + + with open('prefetched/resolved.json', 'w') as file: + json.dump(pattern_map, file, indent=2) + + +def resolver2(local_library): + found_tracks = common.get_data("local_library_on_spotify") + track_descriptions = common.get_data("tracks") + + user_tracks_ids = {track['track']['id']: track for track in track_descriptions} + + resolved_map = {} + + for name, found in found_tracks.items(): + if not len(found): + continue + track_id = found[0]['id'] + if track_id in user_tracks_ids: + resolved_map[name] = track_id + + with open('prefetched/resolved2.json', 'w') as file: + json.dump(resolved_map, file, indent=2) + +def resolve(root_directory): + local_library = get_local_library(root_directory) + with open('prefetched/local_library.json', 'w') as file: + json.dump(local_library, file, indent=2) + + resolver2(local_library) + + +if __name__ == "__main__": + # root_directory = "/mnt/main/data/music/raw" + #root_directory = input("root: ") + resolve("/mnt/main/data/music/raw") + + diff --git a/main.py b/main.py index 49af365..f74ae1c 100644 --- a/main.py +++ b/main.py @@ -12,12 +12,6 @@ def get_playlist_names(): return [name for name, items in playlists.items()] -def get_artists_str(artists_pack): - track_artists = [artist['name'] for artist in artists_pack] - artists = " ".join(track_artists) - return artists - - def print_playlist_tracks(name): print(name) for track in playlists[name]: diff --git a/spotifyFetch.py b/spotifyFetch.py index ec590bc..6827049 100644 --- a/spotifyFetch.py +++ b/spotifyFetch.py @@ -129,10 +129,36 @@ def get_user_data(): return user_id +def find_song(song_pattern): + tracks = fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] + return tracks + + +def find_local_library(): + with open('prefetched/local_library.json', 'r') as file: + local_library = json.load(file) + + from tqdm import tqdm + + pattern_map = {} + + total = len(local_library) + with tqdm(total=total, desc='Searching local songs on spotify') as pbar: + found_tracks = {} + for track, path in local_library.items(): + found_tracks[track] = find_song(track) + pbar.update(1) + + with open('prefetched/local_library_on_spotify.json', 'w') as file: + json.dump(found_tracks, file, indent=2) + + def fetch_data(): global token token = authenticate() + find_local_library() + user_id = get_user_data() fetch_tracks(user_id) diff --git a/spotifyJsonCleanUp.py b/spotifyJsonCleanUp.py new file mode 100644 index 0000000..7501fbb --- /dev/null +++ b/spotifyJsonCleanUp.py @@ -0,0 +1,32 @@ +import os +import json + + +def process_json_files(directory): + for filename in os.listdir(directory): + if filename.endswith('.json') and ("_mod.json" not in filename): + filepath = os.path.join(directory, filename) + + with open(filepath, 'r') as f: + try: + data = json.load(f) + except json.JSONDecodeError: + continue + + def remove_available_markets(obj): + if isinstance(obj, dict): + return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} + elif isinstance(obj, list): + return [remove_available_markets(item) for item in obj] + else: + return obj + + modified_data = remove_available_markets(data) + + modified_filepath = os.path.join(directory, f"{os.path.splitext(filename)[0]}_mod.json") + with open(modified_filepath, 'w') as f: + json.dump(modified_data, f, indent=4) + + +if __name__ == "__main__": + process_json_files("prefetched") From 59a3be110822d256a17d6d7e8e8e20318666b621 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Wed, 10 Jul 2024 20:54:40 +0300 Subject: [PATCH 09/18] tmp --- common.py => DataBase.py | 10 +++ Linker.py | 2 + LinkerPatternGenerator.py | 98 +++++++++++++++++++++++ LocalLibrary.py | 70 ++++++++++++++++ spotifyAuth.py => SpotifyAuthenticator.py | 0 spotifyFetch.py => SpotifyWebAPI.py | 67 ++++++++++------ libResolver.py | 97 ---------------------- loadArtwork.py | 45 ----------- main.py | 10 +-- spotifyJsonCleanUp.py | 32 -------- 10 files changed, 227 insertions(+), 204 deletions(-) rename common.py => DataBase.py (68%) create mode 100644 Linker.py create mode 100644 LinkerPatternGenerator.py create mode 100644 LocalLibrary.py rename spotifyAuth.py => SpotifyAuthenticator.py (100%) rename spotifyFetch.py => SpotifyWebAPI.py (74%) delete mode 100644 libResolver.py delete mode 100644 loadArtwork.py delete mode 100644 spotifyJsonCleanUp.py diff --git a/common.py b/DataBase.py similarity index 68% rename from common.py rename to DataBase.py index ba9c296..5eabc68 100644 --- a/common.py +++ b/DataBase.py @@ -17,6 +17,16 @@ def get_data(name): def save_data(data, name): + def remove_available_markets(obj): + if isinstance(obj, dict): + return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} + elif isinstance(obj, list): + return [remove_available_markets(item) for item in obj] + else: + return obj + + data = remove_available_markets(data) + os.makedirs(data_dir, exist_ok=True) file_path = os.path.join(data_dir, f"{name}.json") diff --git a/Linker.py b/Linker.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/Linker.py @@ -0,0 +1,2 @@ + + diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py new file mode 100644 index 0000000..e81a423 --- /dev/null +++ b/LinkerPatternGenerator.py @@ -0,0 +1,98 @@ +from DataBase import * +from SpotifyWebAPI import find_song, update_access_token + +from fuzzywuzzy import fuzz + + +def spotify_pattern_generator(): + update_access_token() + + local_library = get_data("local_library") + spotify_library = get_data("tracks") + + def find_local_songs_on_spotify(local_lib): + from tqdm import tqdm + total = len(local_library) + found_tracks_map = {} + with tqdm(total=total, desc='Searching local songs on spotify') as pbar: + for track_id, track in local_lib.items(): + search_pattern = f"{track['name']} {track['artist']} {track['album']}" + found_tracks = find_song(search_pattern) + found_tracks_map[track_id] = found_tracks[0:1] + pbar.update(1) + return found_tracks_map + + spotify_found_tracks = find_local_songs_on_spotify(local_library) + spotify_user_track = {track['track']['id']: {"local_mappings": []} for track in spotify_library} + + spotify_pattern = {} + for local_id, found_items in spotify_found_tracks.items(): + spotify_pattern[local_id] = {"score": 0.0, "items": []} + + if not len(found_items): + continue + + for found_item in found_items: + spotify_track_id = found_item['id'] + if spotify_track_id in spotify_user_track: + spotify_pattern[local_id]['items'].append(spotify_track_id) + spotify_user_track[spotify_track_id]['local_mappings'].append(local_id) + + for spotify_id, track_local_mappings in spotify_user_track.items(): + score = len(track_local_mappings['local_mappings']) + for local_id in track_local_mappings['local_mappings']: + spotify_pattern[local_id]['score'] = 1 / score + + save_data(spotify_pattern, "link_pattern_spotify") + + +def fuzzy_pattern_generator(): + local_library = get_data("local_library") + track_descriptions = get_data("tracks") + + def get_matched(src_pattern, patterns): + result = {} + for target_pattern in patterns: + similarity_score = fuzz.token_set_ratio(src_pattern, target_pattern) + result[target_pattern] = similarity_score / 100 + + sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) + return sorted_results + + def resolve_tracks(s_patterns, t_patterns): + from tqdm import tqdm + + pattern_map = {} + + total = len(s_patterns) + with tqdm(total=total, desc='Progress') as pbar: + for s_pattern in s_patterns: + res = get_matched(s_pattern, t_patterns) + matched = {"score": 0, "items": []} + if len(res): + matched['items'] = res[0:min(len(res), 4)] + matched['score'] = res[0][1] + pattern_map[s_pattern] = matched + pbar.update(1) + + return pattern_map + + source_patterns = [] + for track in track_descriptions: + artists = get_artists_str(track['track']['artists']) + pattern = f"{track['track']['name']} {artists}" + source_patterns.append(pattern) + + target_patterns = [local['path'] for _, local in local_library.items()] + + fuzzy_pattern = resolve_tracks(source_patterns, target_patterns) + save_data(fuzzy_pattern, "link_pattern_fuzzy") + + +def run_pattern_generators(): + #fuzzy_pattern_generator() + spotify_pattern_generator() + + +if __name__ == "__main__": + run_pattern_generators() \ No newline at end of file diff --git a/LocalLibrary.py b/LocalLibrary.py new file mode 100644 index 0000000..2d8791d --- /dev/null +++ b/LocalLibrary.py @@ -0,0 +1,70 @@ +import os +import fnmatch + +import mutagen + +from DataBase import * +import eyed3 +from mutagen.flac import FLAC + + +music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') +exclude_directories = ("*mary--*", "*example-word*") + + +def update_local_library(root_dir): + old_library = get_data("local_library") + known_paths = {track['path']: track_id for track_id, track in old_library.items()} + new_library = {} + + song_id = '0' + + def get_new_song_id(prev_id, song_path): + if song_path in known_paths: + return int(known_paths[song_path]) + while (prev_id in old_library) or (prev_id in new_library): + prev_id = str(int(prev_id) + 1) + return prev_id + + for dir_path, dir_names, filenames in os.walk(root_dir): + dir_names[:] = [d for d in dir_names if not any(fnmatch.fnmatch(d, exclude) for exclude in exclude_directories)] + filtered_filenames = [filename for filename in filenames if any(fnmatch.fnmatch(filename, ext) for ext in music_extensions)] + + for filename in filtered_filenames: + song_name, track_type = os.path.splitext(filename) + relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) + song_id = get_new_song_id(song_id, relative_path) + new_library[song_id] = { + "name": song_name, + "path": relative_path, + "type": track_type, + "artist": "", + "album": "" + } + + for track_id, track in new_library.items(): + abs_path = os.path.join(root_dir, track['path']) + match track['type']: + case ".mp3": + tag = eyed3.load(abs_path).tag + track['artist'] = tag.artist + track['name'] = tag.title + track['album'] = tag.album + + case ".flac": + try: + metadata = FLAC(abs_path).tags + if 'artist' in metadata: + track['artist'] = " ".join(metadata['artist']) + if 'TITLE' in metadata: + track['name'] = " ".join(metadata['TITLE']) + if 'album' in metadata: + track['album'] = " ".join(metadata['album']) + except mutagen.MutagenError: + print(f"Invalid flac header {abs_path}") + + save_data(new_library, "local_library") + + +if __name__ == "__main__": + update_local_library("/mnt/main/data/music/raw") diff --git a/spotifyAuth.py b/SpotifyAuthenticator.py similarity index 100% rename from spotifyAuth.py rename to SpotifyAuthenticator.py diff --git a/spotifyFetch.py b/SpotifyWebAPI.py similarity index 74% rename from spotifyFetch.py rename to SpotifyWebAPI.py index 6827049..71ec2f7 100644 --- a/spotifyFetch.py +++ b/SpotifyWebAPI.py @@ -1,7 +1,8 @@ import requests -from loadArtwork import load -from spotifyAuth import authenticate -from common import * +from PIL import Image +from io import BytesIO +from SpotifyAuthenticator import authenticate +from DataBase import * FETCH_STEP = 50 token = '' @@ -18,6 +19,10 @@ def fetch(endpoint, method='GET', body=None): return response.json() +def find_song(song_pattern): + return fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] + + def fetch_playlists(user_id): print("Fetching playlists") endpoint = f"v1/me/playlists" @@ -129,36 +134,47 @@ def get_user_data(): return user_id -def find_song(song_pattern): - tracks = fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] - return tracks +def save_image_from_url(url, name, image_dir="playlist_covers"): + response = requests.get(url) + + if response.status_code != 200: + raise "Cannot fetch the playlist cover" + + image = Image.open(BytesIO(response.content)) + + directory = os.path.join(data_dir, image_dir) + os.makedirs(directory, exist_ok=True) + file_path = os.path.join(directory, f"{name}.jpg") + print(f"Image saved {file_path}") + image.save(file_path) -def find_local_library(): - with open('prefetched/local_library.json', 'r') as file: - local_library = json.load(file) +def load_playlist_covers(): + playlists = get_data('playlists') - from tqdm import tqdm - - pattern_map = {} - - total = len(local_library) - with tqdm(total=total, desc='Searching local songs on spotify') as pbar: - found_tracks = {} - for track, path in local_library.items(): - found_tracks[track] = find_song(track) - pbar.update(1) - - with open('prefetched/local_library_on_spotify.json', 'w') as file: - json.dump(found_tracks, file, indent=2) + for pl in playlists: + if len(pl['images']): + url = pl['images'][0]['url'] + save_image_from_url(url, pl['name']) -def fetch_data(): +def load_user_cover(): + user_data = get_data("user_data") + url = user_data['images'][1]['url'] + save_image_from_url(url, "user", ".") + + +def load_artworks(): + load_user_cover() + load_playlist_covers() + + +def update_access_token(): global token token = authenticate() - find_local_library() +def fetch_data(): user_id = get_user_data() fetch_tracks(user_id) @@ -166,8 +182,9 @@ def fetch_data(): fetch_tracks_top() fetch_artists_top() - load() + load_artworks() if __name__ == "__main__": + update_access_token() fetch_data() diff --git a/libResolver.py b/libResolver.py deleted file mode 100644 index 9d89ffe..0000000 --- a/libResolver.py +++ /dev/null @@ -1,97 +0,0 @@ -import common -import os -import fnmatch -import json -from fuzzywuzzy import fuzz - - -def get_local_library(root_dir): - music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') - music_files_map = {} - for dir_path, dir_names, filenames in os.walk(root_dir): - for pattern in music_extensions: - for filename in fnmatch.filter(filenames, pattern): - song_name = os.path.splitext(filename)[0] - relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) - music_files_map[song_name] = relative_path - return music_files_map - - -def get_matched(pattern, patterns): - result = {} - for target_pattern in patterns: - similarity_score = fuzz.token_set_ratio(pattern, target_pattern) - result[target_pattern] = similarity_score / 100 - - sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) - return sorted_results - - -def resolve_tracks(source_patterns, target_patterns): - from tqdm import tqdm - - pattern_map = {} - - total = len(source_patterns) - with tqdm(total=total, desc='Progress') as pbar: - for pattern in source_patterns: - res = get_matched(pattern, target_patterns) - matched = {"score": 0, "items": []} - if len(res) and res[0][1] > 0.9: - matched['items'] = res[0:min(len(res), 2)] - matched['score'] = res[0][1] - pattern_map[pattern] = matched - pbar.update(1) - - return pattern_map - - -def resolver1(local_library): - track_descriptions = common.get_data("tracks") - - source_patterns = [] - for track in track_descriptions: - artists = common.get_artists_str(track['track']['artists']) - pattern = f"{track['track']['name']} {artists}" - source_patterns.append(pattern) - - target_patterns = [local for _, local in local_library.items()] - - pattern_map = resolve_tracks(source_patterns, target_patterns) - - with open('prefetched/resolved.json', 'w') as file: - json.dump(pattern_map, file, indent=2) - - -def resolver2(local_library): - found_tracks = common.get_data("local_library_on_spotify") - track_descriptions = common.get_data("tracks") - - user_tracks_ids = {track['track']['id']: track for track in track_descriptions} - - resolved_map = {} - - for name, found in found_tracks.items(): - if not len(found): - continue - track_id = found[0]['id'] - if track_id in user_tracks_ids: - resolved_map[name] = track_id - - with open('prefetched/resolved2.json', 'w') as file: - json.dump(resolved_map, file, indent=2) - -def resolve(root_directory): - local_library = get_local_library(root_directory) - with open('prefetched/local_library.json', 'w') as file: - json.dump(local_library, file, indent=2) - - resolver2(local_library) - - -if __name__ == "__main__": - # root_directory = "/mnt/main/data/music/raw" - #root_directory = input("root: ") - resolve("/mnt/main/data/music/raw") - - diff --git a/loadArtwork.py b/loadArtwork.py deleted file mode 100644 index 4d2dc38..0000000 --- a/loadArtwork.py +++ /dev/null @@ -1,45 +0,0 @@ -import requests -import os -from PIL import Image -from io import BytesIO - -from common import * - - -def save_image_from_url(url, name, image_dir="playlist_covers"): - response = requests.get(url) - - if response.status_code != 200: - raise "Cannot fetch the playlist cover" - - image = Image.open(BytesIO(response.content)) - - directory = os.path.join(data_dir, image_dir) - os.makedirs(directory, exist_ok=True) - file_path = os.path.join(directory, f"{name}.jpg") - print(f"Image saved {file_path}") - image.save(file_path) - - -def load_playlist_covers(): - playlists = get_data('playlists') - - for pl in playlists: - if len(pl['images']): - url = pl['images'][0]['url'] - save_image_from_url(url, pl['name']) - - -def load_user_cover(): - user_data = get_data("user_data") - url = user_data['images'][1]['url'] - save_image_from_url(url, "user", ".") - - -def load(): - load_user_cover() - load_playlist_covers() - - -if __name__ == "__main__": - load() diff --git a/main.py b/main.py index f74ae1c..b1b1c79 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -from common import * -from spotifyFetch import fetch_data +from DataBase import * +from SpotifyWebAPI import fetch_data import cmd playlists = get_data('playlistTracks') @@ -8,8 +8,8 @@ top_tracks = get_data('top_tracks') user_tracks = get_data('tracks') -def get_playlist_names(): - return [name for name, items in playlists.items()] +def get_playlist_names(pl): + return [name for name, items in pl.items()] def print_playlist_tracks(name): @@ -57,7 +57,7 @@ class Interpreter(cmd.Cmd): def do_playlists(self, arg): """prints user playlists""" - print("\n".join(get_playlist_names())) + print("\n".join(get_playlist_names(playlists))) def do_playlist_tracks(self, arg): """prints playlist [name]""" diff --git a/spotifyJsonCleanUp.py b/spotifyJsonCleanUp.py deleted file mode 100644 index 7501fbb..0000000 --- a/spotifyJsonCleanUp.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import json - - -def process_json_files(directory): - for filename in os.listdir(directory): - if filename.endswith('.json') and ("_mod.json" not in filename): - filepath = os.path.join(directory, filename) - - with open(filepath, 'r') as f: - try: - data = json.load(f) - except json.JSONDecodeError: - continue - - def remove_available_markets(obj): - if isinstance(obj, dict): - return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} - elif isinstance(obj, list): - return [remove_available_markets(item) for item in obj] - else: - return obj - - modified_data = remove_available_markets(data) - - modified_filepath = os.path.join(directory, f"{os.path.splitext(filename)[0]}_mod.json") - with open(modified_filepath, 'w') as f: - json.dump(modified_data, f, indent=4) - - -if __name__ == "__main__": - process_json_files("prefetched") From b68bda2e1bc185476cad5d6aa23d25845ae38a74 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Wed, 10 Jul 2024 20:54:40 +0300 Subject: [PATCH 10/18] tmp --- common.py => DataBase.py | 10 +++ Linker.py | 2 + LinkerPatternGenerator.py | 98 +++++++++++++++++++++++ LocalLibrary.py | 70 ++++++++++++++++ spotifyAuth.py => SpotifyAuthenticator.py | 0 spotifyFetch.py => SpotifyWebAPI.py | 67 ++++++++++------ libResolver.py | 97 ---------------------- loadArtwork.py | 45 ----------- main.py | 10 +-- spotifyJsonCleanUp.py | 32 -------- 10 files changed, 227 insertions(+), 204 deletions(-) rename common.py => DataBase.py (68%) create mode 100644 Linker.py create mode 100644 LinkerPatternGenerator.py create mode 100644 LocalLibrary.py rename spotifyAuth.py => SpotifyAuthenticator.py (100%) rename spotifyFetch.py => SpotifyWebAPI.py (74%) delete mode 100644 libResolver.py delete mode 100644 loadArtwork.py delete mode 100644 spotifyJsonCleanUp.py diff --git a/common.py b/DataBase.py similarity index 68% rename from common.py rename to DataBase.py index ba9c296..5eabc68 100644 --- a/common.py +++ b/DataBase.py @@ -17,6 +17,16 @@ def get_data(name): def save_data(data, name): + def remove_available_markets(obj): + if isinstance(obj, dict): + return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} + elif isinstance(obj, list): + return [remove_available_markets(item) for item in obj] + else: + return obj + + data = remove_available_markets(data) + os.makedirs(data_dir, exist_ok=True) file_path = os.path.join(data_dir, f"{name}.json") diff --git a/Linker.py b/Linker.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/Linker.py @@ -0,0 +1,2 @@ + + diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py new file mode 100644 index 0000000..e81a423 --- /dev/null +++ b/LinkerPatternGenerator.py @@ -0,0 +1,98 @@ +from DataBase import * +from SpotifyWebAPI import find_song, update_access_token + +from fuzzywuzzy import fuzz + + +def spotify_pattern_generator(): + update_access_token() + + local_library = get_data("local_library") + spotify_library = get_data("tracks") + + def find_local_songs_on_spotify(local_lib): + from tqdm import tqdm + total = len(local_library) + found_tracks_map = {} + with tqdm(total=total, desc='Searching local songs on spotify') as pbar: + for track_id, track in local_lib.items(): + search_pattern = f"{track['name']} {track['artist']} {track['album']}" + found_tracks = find_song(search_pattern) + found_tracks_map[track_id] = found_tracks[0:1] + pbar.update(1) + return found_tracks_map + + spotify_found_tracks = find_local_songs_on_spotify(local_library) + spotify_user_track = {track['track']['id']: {"local_mappings": []} for track in spotify_library} + + spotify_pattern = {} + for local_id, found_items in spotify_found_tracks.items(): + spotify_pattern[local_id] = {"score": 0.0, "items": []} + + if not len(found_items): + continue + + for found_item in found_items: + spotify_track_id = found_item['id'] + if spotify_track_id in spotify_user_track: + spotify_pattern[local_id]['items'].append(spotify_track_id) + spotify_user_track[spotify_track_id]['local_mappings'].append(local_id) + + for spotify_id, track_local_mappings in spotify_user_track.items(): + score = len(track_local_mappings['local_mappings']) + for local_id in track_local_mappings['local_mappings']: + spotify_pattern[local_id]['score'] = 1 / score + + save_data(spotify_pattern, "link_pattern_spotify") + + +def fuzzy_pattern_generator(): + local_library = get_data("local_library") + track_descriptions = get_data("tracks") + + def get_matched(src_pattern, patterns): + result = {} + for target_pattern in patterns: + similarity_score = fuzz.token_set_ratio(src_pattern, target_pattern) + result[target_pattern] = similarity_score / 100 + + sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) + return sorted_results + + def resolve_tracks(s_patterns, t_patterns): + from tqdm import tqdm + + pattern_map = {} + + total = len(s_patterns) + with tqdm(total=total, desc='Progress') as pbar: + for s_pattern in s_patterns: + res = get_matched(s_pattern, t_patterns) + matched = {"score": 0, "items": []} + if len(res): + matched['items'] = res[0:min(len(res), 4)] + matched['score'] = res[0][1] + pattern_map[s_pattern] = matched + pbar.update(1) + + return pattern_map + + source_patterns = [] + for track in track_descriptions: + artists = get_artists_str(track['track']['artists']) + pattern = f"{track['track']['name']} {artists}" + source_patterns.append(pattern) + + target_patterns = [local['path'] for _, local in local_library.items()] + + fuzzy_pattern = resolve_tracks(source_patterns, target_patterns) + save_data(fuzzy_pattern, "link_pattern_fuzzy") + + +def run_pattern_generators(): + #fuzzy_pattern_generator() + spotify_pattern_generator() + + +if __name__ == "__main__": + run_pattern_generators() \ No newline at end of file diff --git a/LocalLibrary.py b/LocalLibrary.py new file mode 100644 index 0000000..2d8791d --- /dev/null +++ b/LocalLibrary.py @@ -0,0 +1,70 @@ +import os +import fnmatch + +import mutagen + +from DataBase import * +import eyed3 +from mutagen.flac import FLAC + + +music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') +exclude_directories = ("*mary--*", "*example-word*") + + +def update_local_library(root_dir): + old_library = get_data("local_library") + known_paths = {track['path']: track_id for track_id, track in old_library.items()} + new_library = {} + + song_id = '0' + + def get_new_song_id(prev_id, song_path): + if song_path in known_paths: + return int(known_paths[song_path]) + while (prev_id in old_library) or (prev_id in new_library): + prev_id = str(int(prev_id) + 1) + return prev_id + + for dir_path, dir_names, filenames in os.walk(root_dir): + dir_names[:] = [d for d in dir_names if not any(fnmatch.fnmatch(d, exclude) for exclude in exclude_directories)] + filtered_filenames = [filename for filename in filenames if any(fnmatch.fnmatch(filename, ext) for ext in music_extensions)] + + for filename in filtered_filenames: + song_name, track_type = os.path.splitext(filename) + relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) + song_id = get_new_song_id(song_id, relative_path) + new_library[song_id] = { + "name": song_name, + "path": relative_path, + "type": track_type, + "artist": "", + "album": "" + } + + for track_id, track in new_library.items(): + abs_path = os.path.join(root_dir, track['path']) + match track['type']: + case ".mp3": + tag = eyed3.load(abs_path).tag + track['artist'] = tag.artist + track['name'] = tag.title + track['album'] = tag.album + + case ".flac": + try: + metadata = FLAC(abs_path).tags + if 'artist' in metadata: + track['artist'] = " ".join(metadata['artist']) + if 'TITLE' in metadata: + track['name'] = " ".join(metadata['TITLE']) + if 'album' in metadata: + track['album'] = " ".join(metadata['album']) + except mutagen.MutagenError: + print(f"Invalid flac header {abs_path}") + + save_data(new_library, "local_library") + + +if __name__ == "__main__": + update_local_library("/mnt/main/data/music/raw") diff --git a/spotifyAuth.py b/SpotifyAuthenticator.py similarity index 100% rename from spotifyAuth.py rename to SpotifyAuthenticator.py diff --git a/spotifyFetch.py b/SpotifyWebAPI.py similarity index 74% rename from spotifyFetch.py rename to SpotifyWebAPI.py index 6827049..71ec2f7 100644 --- a/spotifyFetch.py +++ b/SpotifyWebAPI.py @@ -1,7 +1,8 @@ import requests -from loadArtwork import load -from spotifyAuth import authenticate -from common import * +from PIL import Image +from io import BytesIO +from SpotifyAuthenticator import authenticate +from DataBase import * FETCH_STEP = 50 token = '' @@ -18,6 +19,10 @@ def fetch(endpoint, method='GET', body=None): return response.json() +def find_song(song_pattern): + return fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] + + def fetch_playlists(user_id): print("Fetching playlists") endpoint = f"v1/me/playlists" @@ -129,36 +134,47 @@ def get_user_data(): return user_id -def find_song(song_pattern): - tracks = fetch(f"v1/search/?type=track&track=1&q={song_pattern}")['tracks']['items'] - return tracks +def save_image_from_url(url, name, image_dir="playlist_covers"): + response = requests.get(url) + + if response.status_code != 200: + raise "Cannot fetch the playlist cover" + + image = Image.open(BytesIO(response.content)) + + directory = os.path.join(data_dir, image_dir) + os.makedirs(directory, exist_ok=True) + file_path = os.path.join(directory, f"{name}.jpg") + print(f"Image saved {file_path}") + image.save(file_path) -def find_local_library(): - with open('prefetched/local_library.json', 'r') as file: - local_library = json.load(file) +def load_playlist_covers(): + playlists = get_data('playlists') - from tqdm import tqdm - - pattern_map = {} - - total = len(local_library) - with tqdm(total=total, desc='Searching local songs on spotify') as pbar: - found_tracks = {} - for track, path in local_library.items(): - found_tracks[track] = find_song(track) - pbar.update(1) - - with open('prefetched/local_library_on_spotify.json', 'w') as file: - json.dump(found_tracks, file, indent=2) + for pl in playlists: + if len(pl['images']): + url = pl['images'][0]['url'] + save_image_from_url(url, pl['name']) -def fetch_data(): +def load_user_cover(): + user_data = get_data("user_data") + url = user_data['images'][1]['url'] + save_image_from_url(url, "user", ".") + + +def load_artworks(): + load_user_cover() + load_playlist_covers() + + +def update_access_token(): global token token = authenticate() - find_local_library() +def fetch_data(): user_id = get_user_data() fetch_tracks(user_id) @@ -166,8 +182,9 @@ def fetch_data(): fetch_tracks_top() fetch_artists_top() - load() + load_artworks() if __name__ == "__main__": + update_access_token() fetch_data() diff --git a/libResolver.py b/libResolver.py deleted file mode 100644 index 9d89ffe..0000000 --- a/libResolver.py +++ /dev/null @@ -1,97 +0,0 @@ -import common -import os -import fnmatch -import json -from fuzzywuzzy import fuzz - - -def get_local_library(root_dir): - music_extensions = ('*.mp3', '*.flac', '*.wav', '*.aac', '*.ogg', '*.m4a') - music_files_map = {} - for dir_path, dir_names, filenames in os.walk(root_dir): - for pattern in music_extensions: - for filename in fnmatch.filter(filenames, pattern): - song_name = os.path.splitext(filename)[0] - relative_path = os.path.relpath(os.path.join(dir_path, filename), root_dir) - music_files_map[song_name] = relative_path - return music_files_map - - -def get_matched(pattern, patterns): - result = {} - for target_pattern in patterns: - similarity_score = fuzz.token_set_ratio(pattern, target_pattern) - result[target_pattern] = similarity_score / 100 - - sorted_results = sorted(result.items(), key=lambda item: item[1], reverse=True) - return sorted_results - - -def resolve_tracks(source_patterns, target_patterns): - from tqdm import tqdm - - pattern_map = {} - - total = len(source_patterns) - with tqdm(total=total, desc='Progress') as pbar: - for pattern in source_patterns: - res = get_matched(pattern, target_patterns) - matched = {"score": 0, "items": []} - if len(res) and res[0][1] > 0.9: - matched['items'] = res[0:min(len(res), 2)] - matched['score'] = res[0][1] - pattern_map[pattern] = matched - pbar.update(1) - - return pattern_map - - -def resolver1(local_library): - track_descriptions = common.get_data("tracks") - - source_patterns = [] - for track in track_descriptions: - artists = common.get_artists_str(track['track']['artists']) - pattern = f"{track['track']['name']} {artists}" - source_patterns.append(pattern) - - target_patterns = [local for _, local in local_library.items()] - - pattern_map = resolve_tracks(source_patterns, target_patterns) - - with open('prefetched/resolved.json', 'w') as file: - json.dump(pattern_map, file, indent=2) - - -def resolver2(local_library): - found_tracks = common.get_data("local_library_on_spotify") - track_descriptions = common.get_data("tracks") - - user_tracks_ids = {track['track']['id']: track for track in track_descriptions} - - resolved_map = {} - - for name, found in found_tracks.items(): - if not len(found): - continue - track_id = found[0]['id'] - if track_id in user_tracks_ids: - resolved_map[name] = track_id - - with open('prefetched/resolved2.json', 'w') as file: - json.dump(resolved_map, file, indent=2) - -def resolve(root_directory): - local_library = get_local_library(root_directory) - with open('prefetched/local_library.json', 'w') as file: - json.dump(local_library, file, indent=2) - - resolver2(local_library) - - -if __name__ == "__main__": - # root_directory = "/mnt/main/data/music/raw" - #root_directory = input("root: ") - resolve("/mnt/main/data/music/raw") - - diff --git a/loadArtwork.py b/loadArtwork.py deleted file mode 100644 index 4d2dc38..0000000 --- a/loadArtwork.py +++ /dev/null @@ -1,45 +0,0 @@ -import requests -import os -from PIL import Image -from io import BytesIO - -from common import * - - -def save_image_from_url(url, name, image_dir="playlist_covers"): - response = requests.get(url) - - if response.status_code != 200: - raise "Cannot fetch the playlist cover" - - image = Image.open(BytesIO(response.content)) - - directory = os.path.join(data_dir, image_dir) - os.makedirs(directory, exist_ok=True) - file_path = os.path.join(directory, f"{name}.jpg") - print(f"Image saved {file_path}") - image.save(file_path) - - -def load_playlist_covers(): - playlists = get_data('playlists') - - for pl in playlists: - if len(pl['images']): - url = pl['images'][0]['url'] - save_image_from_url(url, pl['name']) - - -def load_user_cover(): - user_data = get_data("user_data") - url = user_data['images'][1]['url'] - save_image_from_url(url, "user", ".") - - -def load(): - load_user_cover() - load_playlist_covers() - - -if __name__ == "__main__": - load() diff --git a/main.py b/main.py index f74ae1c..b1b1c79 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -from common import * -from spotifyFetch import fetch_data +from DataBase import * +from SpotifyWebAPI import fetch_data import cmd playlists = get_data('playlistTracks') @@ -8,8 +8,8 @@ top_tracks = get_data('top_tracks') user_tracks = get_data('tracks') -def get_playlist_names(): - return [name for name, items in playlists.items()] +def get_playlist_names(pl): + return [name for name, items in pl.items()] def print_playlist_tracks(name): @@ -57,7 +57,7 @@ class Interpreter(cmd.Cmd): def do_playlists(self, arg): """prints user playlists""" - print("\n".join(get_playlist_names())) + print("\n".join(get_playlist_names(playlists))) def do_playlist_tracks(self, arg): """prints playlist [name]""" diff --git a/spotifyJsonCleanUp.py b/spotifyJsonCleanUp.py deleted file mode 100644 index 7501fbb..0000000 --- a/spotifyJsonCleanUp.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import json - - -def process_json_files(directory): - for filename in os.listdir(directory): - if filename.endswith('.json') and ("_mod.json" not in filename): - filepath = os.path.join(directory, filename) - - with open(filepath, 'r') as f: - try: - data = json.load(f) - except json.JSONDecodeError: - continue - - def remove_available_markets(obj): - if isinstance(obj, dict): - return {key: remove_available_markets(value) for key, value in obj.items() if key != 'available_markets'} - elif isinstance(obj, list): - return [remove_available_markets(item) for item in obj] - else: - return obj - - modified_data = remove_available_markets(data) - - modified_filepath = os.path.join(directory, f"{os.path.splitext(filename)[0]}_mod.json") - with open(modified_filepath, 'w') as f: - json.dump(modified_data, f, indent=4) - - -if __name__ == "__main__": - process_json_files("prefetched") From 3744bd1c56473cd36e73716e9df99b028e62f697 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 10:26:27 +0300 Subject: [PATCH 11/18] tmp --- DataBase.py | 7 +++++++ LinkerPatternGenerator.py | 39 ++++++++++++++++++++++++++++++++++++--- main.py | 20 ++++++++++++++------ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/DataBase.py b/DataBase.py index 5eabc68..3132b7b 100644 --- a/DataBase.py +++ b/DataBase.py @@ -4,6 +4,13 @@ import os data_dir = 'prefetched' +class SongTags: + def __init__(self, title='', artist='', album=''): + self.artist = artist + self.title = title + self.album = album + + def get_data(name): file_path = os.path.join(data_dir, f"{name}.json") try: diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index e81a423..bb16839 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -2,6 +2,7 @@ from DataBase import * from SpotifyWebAPI import find_song, update_access_token from fuzzywuzzy import fuzz +from tqdm import tqdm def spotify_pattern_generator(): @@ -11,7 +12,6 @@ def spotify_pattern_generator(): spotify_library = get_data("tracks") def find_local_songs_on_spotify(local_lib): - from tqdm import tqdm total = len(local_library) found_tracks_map = {} with tqdm(total=total, desc='Searching local songs on spotify') as pbar: @@ -60,7 +60,6 @@ def fuzzy_pattern_generator(): return sorted_results def resolve_tracks(s_patterns, t_patterns): - from tqdm import tqdm pattern_map = {} @@ -89,9 +88,43 @@ def fuzzy_pattern_generator(): save_data(fuzzy_pattern, "link_pattern_fuzzy") +def fuzzy_tag_pattern_generator(): + local_library = get_data("local_library") + spotify_library = get_data("tracks") + mappings = {} + + def fuzzy_tags_ratio(first: SongTags, second: SongTags): + title_ratio = fuzz.ratio(first.title, second.title) / 100 + artist_ratio = fuzz.ratio(first.artist, second.artist) / 100 + album_ratio = fuzz.ratio(first.album, second.album) / 100 + return sum([title_ratio, artist_ratio, album_ratio]) / 3 + + with tqdm(total=len(local_library), desc='Searching local songs on spotify') as pbar: + for local_track_id, local_track in local_library.items(): + mapping = mappings.get(local_track_id, {"score": 1.0, "items": []}) + ratios = [] + for remote_track in spotify_library: + track = remote_track['track'] + + tag1 = SongTags(local_track['name'], local_track['artist'], local_track['album']) + tag2 = SongTags(track['name'], get_artists_str(track['artists']), track['album']['name']) + + ratios.append((fuzzy_tags_ratio(tag1, tag2), track['id'])) + + ratios.sort(reverse=True, key=lambda x: x[0]) + if ratios[0][0] > 0.8: + mapping['items'] = [ratios[0][1]] + + mappings[local_track_id] = mapping + pbar.update(1) + + save_data(mappings, "link_pattern_tags") + + def run_pattern_generators(): #fuzzy_pattern_generator() - spotify_pattern_generator() + #spotify_pattern_generator() + fuzzy_tag_pattern_generator() if __name__ == "__main__": diff --git a/main.py b/main.py index b1b1c79..339152f 100644 --- a/main.py +++ b/main.py @@ -12,9 +12,12 @@ def get_playlist_names(pl): return [name for name, items in pl.items()] -def print_playlist_tracks(name): +def print_playlist_tracks(pl_id: int): + pls = list(playlists) + name = pls[pl_id] + pl = playlists[name] print(name) - for track in playlists[name]: + for track in pl: track_data = track['track'] track_name = track_data['name'] artists = get_artists_str(track_data['artists']) @@ -46,9 +49,11 @@ def print_stats(): def print_tracks(): + track_idx = 0 for trackItem in user_tracks: track = trackItem['track'] - print(f" '{track['name']}' - {get_artists_str(track['artists'])}") + print(f" {track_idx}: '{track['name']}' by '{get_artists_str(track['artists'])}'") + track_idx += 1 class Interpreter(cmd.Cmd): @@ -57,13 +62,16 @@ class Interpreter(cmd.Cmd): def do_playlists(self, arg): """prints user playlists""" - print("\n".join(get_playlist_names(playlists))) + pl_id = 0 + for pl in get_playlist_names(playlists): + print(f"{pl_id} - {pl}") + pl_id += 1 def do_playlist_tracks(self, arg): """prints playlist [name]""" try: - name = arg.split()[0] - print_playlist_tracks(name) + pl_id = int(arg.split()[0]) + print_playlist_tracks(pl_id) except IndexError: print("Invalid input") From afc020fdbf0ab01d6f3cd10be91abe229059a916 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 10:26:27 +0300 Subject: [PATCH 12/18] tmp --- DataBase.py | 7 +++++++ LinkerPatternGenerator.py | 39 ++++++++++++++++++++++++++++++++++++--- main.py | 20 ++++++++++++++------ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/DataBase.py b/DataBase.py index 5eabc68..3132b7b 100644 --- a/DataBase.py +++ b/DataBase.py @@ -4,6 +4,13 @@ import os data_dir = 'prefetched' +class SongTags: + def __init__(self, title='', artist='', album=''): + self.artist = artist + self.title = title + self.album = album + + def get_data(name): file_path = os.path.join(data_dir, f"{name}.json") try: diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index e81a423..bb16839 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -2,6 +2,7 @@ from DataBase import * from SpotifyWebAPI import find_song, update_access_token from fuzzywuzzy import fuzz +from tqdm import tqdm def spotify_pattern_generator(): @@ -11,7 +12,6 @@ def spotify_pattern_generator(): spotify_library = get_data("tracks") def find_local_songs_on_spotify(local_lib): - from tqdm import tqdm total = len(local_library) found_tracks_map = {} with tqdm(total=total, desc='Searching local songs on spotify') as pbar: @@ -60,7 +60,6 @@ def fuzzy_pattern_generator(): return sorted_results def resolve_tracks(s_patterns, t_patterns): - from tqdm import tqdm pattern_map = {} @@ -89,9 +88,43 @@ def fuzzy_pattern_generator(): save_data(fuzzy_pattern, "link_pattern_fuzzy") +def fuzzy_tag_pattern_generator(): + local_library = get_data("local_library") + spotify_library = get_data("tracks") + mappings = {} + + def fuzzy_tags_ratio(first: SongTags, second: SongTags): + title_ratio = fuzz.ratio(first.title, second.title) / 100 + artist_ratio = fuzz.ratio(first.artist, second.artist) / 100 + album_ratio = fuzz.ratio(first.album, second.album) / 100 + return sum([title_ratio, artist_ratio, album_ratio]) / 3 + + with tqdm(total=len(local_library), desc='Searching local songs on spotify') as pbar: + for local_track_id, local_track in local_library.items(): + mapping = mappings.get(local_track_id, {"score": 1.0, "items": []}) + ratios = [] + for remote_track in spotify_library: + track = remote_track['track'] + + tag1 = SongTags(local_track['name'], local_track['artist'], local_track['album']) + tag2 = SongTags(track['name'], get_artists_str(track['artists']), track['album']['name']) + + ratios.append((fuzzy_tags_ratio(tag1, tag2), track['id'])) + + ratios.sort(reverse=True, key=lambda x: x[0]) + if ratios[0][0] > 0.8: + mapping['items'] = [ratios[0][1]] + + mappings[local_track_id] = mapping + pbar.update(1) + + save_data(mappings, "link_pattern_tags") + + def run_pattern_generators(): #fuzzy_pattern_generator() - spotify_pattern_generator() + #spotify_pattern_generator() + fuzzy_tag_pattern_generator() if __name__ == "__main__": diff --git a/main.py b/main.py index b1b1c79..339152f 100644 --- a/main.py +++ b/main.py @@ -12,9 +12,12 @@ def get_playlist_names(pl): return [name for name, items in pl.items()] -def print_playlist_tracks(name): +def print_playlist_tracks(pl_id: int): + pls = list(playlists) + name = pls[pl_id] + pl = playlists[name] print(name) - for track in playlists[name]: + for track in pl: track_data = track['track'] track_name = track_data['name'] artists = get_artists_str(track_data['artists']) @@ -46,9 +49,11 @@ def print_stats(): def print_tracks(): + track_idx = 0 for trackItem in user_tracks: track = trackItem['track'] - print(f" '{track['name']}' - {get_artists_str(track['artists'])}") + print(f" {track_idx}: '{track['name']}' by '{get_artists_str(track['artists'])}'") + track_idx += 1 class Interpreter(cmd.Cmd): @@ -57,13 +62,16 @@ class Interpreter(cmd.Cmd): def do_playlists(self, arg): """prints user playlists""" - print("\n".join(get_playlist_names(playlists))) + pl_id = 0 + for pl in get_playlist_names(playlists): + print(f"{pl_id} - {pl}") + pl_id += 1 def do_playlist_tracks(self, arg): """prints playlist [name]""" try: - name = arg.split()[0] - print_playlist_tracks(name) + pl_id = int(arg.split()[0]) + print_playlist_tracks(pl_id) except IndexError: print("Invalid input") From e044b7ffd4bcfd7e6ec2afc3a934968216008ef6 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 11:57:06 +0300 Subject: [PATCH 13/18] tmp --- LinkerPatternGenerator.py | 20 +++++++---- main.py | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index bb16839..c037899 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -94,10 +94,10 @@ def fuzzy_tag_pattern_generator(): mappings = {} def fuzzy_tags_ratio(first: SongTags, second: SongTags): - title_ratio = fuzz.ratio(first.title, second.title) / 100 - artist_ratio = fuzz.ratio(first.artist, second.artist) / 100 - album_ratio = fuzz.ratio(first.album, second.album) / 100 - return sum([title_ratio, artist_ratio, album_ratio]) / 3 + title_ratio = fuzz.token_set_ratio(first.title, second.title) / 100 + artist_ratio = fuzz.token_set_ratio(first.artist, second.artist) / 100 + album_ratio = fuzz.token_set_ratio(first.album, second.album) / 100 + return title_ratio, artist_ratio, album_ratio with tqdm(total=len(local_library), desc='Searching local songs on spotify') as pbar: for local_track_id, local_track in local_library.items(): @@ -111,9 +111,15 @@ def fuzzy_tag_pattern_generator(): ratios.append((fuzzy_tags_ratio(tag1, tag2), track['id'])) - ratios.sort(reverse=True, key=lambda x: x[0]) - if ratios[0][0] > 0.8: - mapping['items'] = [ratios[0][1]] + filtered_ratios = [] + threshold = 0.8 + for ratio in ratios: + if all([val > threshold for val in ratio[0]]): + filtered_ratios.append((sum(i for i in ratio[0]) / len(ratio[0]), ratio[1])) + + filtered_ratios.sort(reverse=True, key=lambda x: x[0]) + if len(filtered_ratios): + mapping['items'] = [filtered_ratios[0][1]] mappings[local_track_id] = mapping pbar.update(1) diff --git a/main.py b/main.py index 339152f..6e04234 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,9 @@ playlists = get_data('playlistTracks') top_artists = get_data('top_artists') top_tracks = get_data('top_tracks') user_tracks = get_data('tracks') +link_patterns = [get_data('link_pattern_tags'), get_data('link_pattern_tags'), get_data('link_pattern_tags')] +local_library = get_data("local_library") +user_tracks_by_id = {track['track']['id']: track['track'] for track in user_tracks} def get_playlist_names(pl): @@ -56,10 +59,80 @@ def print_tracks(): track_idx += 1 +def sort_remote_tracks_by_popularity(track_ids): + out = [] + for top_track in top_tracks: + if top_track['id'] in track_ids: + out.append(top_track['id']) + return out + + +def print_link_stats(link_pattern): + resolved_reversed = {} + resolved = [] + unresolved_local = [] + unresolved_remote = [] + + for local_id, links in link_pattern.items(): + if len(links['items']): + resolved.append(local_id) + resolved_reversed[links['items'][0]] = local_id + else: + unresolved_local.append(local_id) + + for track in user_tracks: + track_id = track['track']['id'] + if track_id not in resolved_reversed: + unresolved_remote.append(track_id) + + print(f"Local Tracks: {len(local_library)}") + print(f"Remote Tracks: {len(user_tracks)}") + + unresolved_remote = sort_remote_tracks_by_popularity(unresolved_remote) + unresolved_remote = unresolved_remote[0:min(len(unresolved_remote), 100)] + + print(f"\nUnresolved tracks from the remote: {len(unresolved_remote)}") + index = 0 + for remote_id in unresolved_remote: + remote_track = user_tracks_by_id[remote_id] + print(f" {index}: '{remote_track['name']}' by '{get_artists_str(remote_track['artists'])}'") + index += 1 + + print(f"\nResolved tracks: {len(resolved)}") + index = 0 + for link in resolved: + remote_id = link_pattern[link]['items'][0] + local_track = local_library[link] + remote_track = user_tracks_by_id[remote_id] + + print(f" {index}: '{local_track['name']}' by '{local_track['artist']}'", end=' ----> ') + print(f"'{remote_track['name']}' by '{get_artists_str(remote_track['artists'])}'") + index += 1 + + return + print(f"\nUnresolved tracks from the local: {len(unresolved_local)}") + index = 0 + for link in unresolved_local: + local_track = local_library[link] + print(f" {index}: '{local_track['name']}' by '{local_track['artist']}'") + index += 1 + + class Interpreter(cmd.Cmd): intro = "Welcome to the interactive command loop. Type help or ? to list commands.\n" prompt = "(spotify) " + def do_link_patterns(self, arg): + """prints available linking patterns""" + print(f"0 - fuzzy tags") + print(f"1 - spotify") + print(f"2 - fuzzy") + + def do_link_pattern(self, arg): + """prints available linking patterns""" + pattern_id = int(arg.split()[0]) + print_link_stats(link_patterns[pattern_id]) + def do_playlists(self, arg): """prints user playlists""" pl_id = 0 From 9fe80257a85662126364f157bee0560eb350a7a2 Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 11:57:06 +0300 Subject: [PATCH 14/18] tmp --- LinkerPatternGenerator.py | 20 +++++++---- main.py | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index bb16839..c037899 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -94,10 +94,10 @@ def fuzzy_tag_pattern_generator(): mappings = {} def fuzzy_tags_ratio(first: SongTags, second: SongTags): - title_ratio = fuzz.ratio(first.title, second.title) / 100 - artist_ratio = fuzz.ratio(first.artist, second.artist) / 100 - album_ratio = fuzz.ratio(first.album, second.album) / 100 - return sum([title_ratio, artist_ratio, album_ratio]) / 3 + title_ratio = fuzz.token_set_ratio(first.title, second.title) / 100 + artist_ratio = fuzz.token_set_ratio(first.artist, second.artist) / 100 + album_ratio = fuzz.token_set_ratio(first.album, second.album) / 100 + return title_ratio, artist_ratio, album_ratio with tqdm(total=len(local_library), desc='Searching local songs on spotify') as pbar: for local_track_id, local_track in local_library.items(): @@ -111,9 +111,15 @@ def fuzzy_tag_pattern_generator(): ratios.append((fuzzy_tags_ratio(tag1, tag2), track['id'])) - ratios.sort(reverse=True, key=lambda x: x[0]) - if ratios[0][0] > 0.8: - mapping['items'] = [ratios[0][1]] + filtered_ratios = [] + threshold = 0.8 + for ratio in ratios: + if all([val > threshold for val in ratio[0]]): + filtered_ratios.append((sum(i for i in ratio[0]) / len(ratio[0]), ratio[1])) + + filtered_ratios.sort(reverse=True, key=lambda x: x[0]) + if len(filtered_ratios): + mapping['items'] = [filtered_ratios[0][1]] mappings[local_track_id] = mapping pbar.update(1) diff --git a/main.py b/main.py index 339152f..6e04234 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,9 @@ playlists = get_data('playlistTracks') top_artists = get_data('top_artists') top_tracks = get_data('top_tracks') user_tracks = get_data('tracks') +link_patterns = [get_data('link_pattern_tags'), get_data('link_pattern_tags'), get_data('link_pattern_tags')] +local_library = get_data("local_library") +user_tracks_by_id = {track['track']['id']: track['track'] for track in user_tracks} def get_playlist_names(pl): @@ -56,10 +59,80 @@ def print_tracks(): track_idx += 1 +def sort_remote_tracks_by_popularity(track_ids): + out = [] + for top_track in top_tracks: + if top_track['id'] in track_ids: + out.append(top_track['id']) + return out + + +def print_link_stats(link_pattern): + resolved_reversed = {} + resolved = [] + unresolved_local = [] + unresolved_remote = [] + + for local_id, links in link_pattern.items(): + if len(links['items']): + resolved.append(local_id) + resolved_reversed[links['items'][0]] = local_id + else: + unresolved_local.append(local_id) + + for track in user_tracks: + track_id = track['track']['id'] + if track_id not in resolved_reversed: + unresolved_remote.append(track_id) + + print(f"Local Tracks: {len(local_library)}") + print(f"Remote Tracks: {len(user_tracks)}") + + unresolved_remote = sort_remote_tracks_by_popularity(unresolved_remote) + unresolved_remote = unresolved_remote[0:min(len(unresolved_remote), 100)] + + print(f"\nUnresolved tracks from the remote: {len(unresolved_remote)}") + index = 0 + for remote_id in unresolved_remote: + remote_track = user_tracks_by_id[remote_id] + print(f" {index}: '{remote_track['name']}' by '{get_artists_str(remote_track['artists'])}'") + index += 1 + + print(f"\nResolved tracks: {len(resolved)}") + index = 0 + for link in resolved: + remote_id = link_pattern[link]['items'][0] + local_track = local_library[link] + remote_track = user_tracks_by_id[remote_id] + + print(f" {index}: '{local_track['name']}' by '{local_track['artist']}'", end=' ----> ') + print(f"'{remote_track['name']}' by '{get_artists_str(remote_track['artists'])}'") + index += 1 + + return + print(f"\nUnresolved tracks from the local: {len(unresolved_local)}") + index = 0 + for link in unresolved_local: + local_track = local_library[link] + print(f" {index}: '{local_track['name']}' by '{local_track['artist']}'") + index += 1 + + class Interpreter(cmd.Cmd): intro = "Welcome to the interactive command loop. Type help or ? to list commands.\n" prompt = "(spotify) " + def do_link_patterns(self, arg): + """prints available linking patterns""" + print(f"0 - fuzzy tags") + print(f"1 - spotify") + print(f"2 - fuzzy") + + def do_link_pattern(self, arg): + """prints available linking patterns""" + pattern_id = int(arg.split()[0]) + print_link_stats(link_patterns[pattern_id]) + def do_playlists(self, arg): """prints user playlists""" pl_id = 0 From b2e582c56654b87ac4eb103169390c260f9be97a Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 13:36:53 +0300 Subject: [PATCH 15/18] tmp --- LinkerPatternGenerator.py | 2 +- LocalLibrary.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index c037899..11fdf4e 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -134,4 +134,4 @@ def run_pattern_generators(): if __name__ == "__main__": - run_pattern_generators() \ No newline at end of file + run_pattern_generators() diff --git a/LocalLibrary.py b/LocalLibrary.py index 2d8791d..2cc220b 100644 --- a/LocalLibrary.py +++ b/LocalLibrary.py @@ -47,9 +47,12 @@ def update_local_library(root_dir): match track['type']: case ".mp3": tag = eyed3.load(abs_path).tag - track['artist'] = tag.artist - track['name'] = tag.title - track['album'] = tag.album + if not tag: + print(f"Invalid mp3 header {abs_path}") + else: + track['artist'] = tag.artist + track['name'] = tag.title + track['album'] = tag.album case ".flac": try: From d4d31d6752880e40c56847561009e8601c7a01ca Mon Sep 17 00:00:00 2001 From: IlyaShurupov Date: Thu, 11 Jul 2024 13:36:53 +0300 Subject: [PATCH 16/18] tmp --- LinkerPatternGenerator.py | 2 +- LocalLibrary.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LinkerPatternGenerator.py b/LinkerPatternGenerator.py index c037899..11fdf4e 100644 --- a/LinkerPatternGenerator.py +++ b/LinkerPatternGenerator.py @@ -134,4 +134,4 @@ def run_pattern_generators(): if __name__ == "__main__": - run_pattern_generators() \ No newline at end of file + run_pattern_generators() diff --git a/LocalLibrary.py b/LocalLibrary.py index 2d8791d..2cc220b 100644 --- a/LocalLibrary.py +++ b/LocalLibrary.py @@ -47,9 +47,12 @@ def update_local_library(root_dir): match track['type']: case ".mp3": tag = eyed3.load(abs_path).tag - track['artist'] = tag.artist - track['name'] = tag.title - track['album'] = tag.album + if not tag: + print(f"Invalid mp3 header {abs_path}") + else: + track['artist'] = tag.artist + track['name'] = tag.title + track['album'] = tag.album case ".flac": try: From d8929d30647106771f575d2e3fab60715de7e76e Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:26:51 +0300 Subject: [PATCH 17/18] tmp --- Linker.py | 2 - LocalLibrary.py => LocalLibraryIndexer.py | 0 ParseItunesToJson.py | 101 ++++++++++++++++++++++ env | 36 ++++++++ main.py | 3 +- 5 files changed, 139 insertions(+), 3 deletions(-) delete mode 100644 Linker.py rename LocalLibrary.py => LocalLibraryIndexer.py (100%) create mode 100755 ParseItunesToJson.py create mode 100755 env diff --git a/Linker.py b/Linker.py deleted file mode 100644 index 139597f..0000000 --- a/Linker.py +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/LocalLibrary.py b/LocalLibraryIndexer.py similarity index 100% rename from LocalLibrary.py rename to LocalLibraryIndexer.py diff --git a/ParseItunesToJson.py b/ParseItunesToJson.py new file mode 100755 index 0000000..1505a36 --- /dev/null +++ b/ParseItunesToJson.py @@ -0,0 +1,101 @@ +import xmltodict +import json + + +def flatten_dict(d): + + out = [] + for song in d: + newSong = {} + + strCount = 0 + intCount = 0 + dateCount = 0 + + def getStr(id): + nonlocal strCount + if id not in song["key"]: + return "undef" + if len(song["string"]) <= strCount: + return "error" + strCount += 1 + return song["string"][strCount - 1] + + def getInt(id): + nonlocal intCount + if id not in song["key"]: + return -1 + if len(song["integer"]) <= intCount: + return -1 + intCount += 1 + return song["integer"][intCount - 1] + + def getDate(id): + nonlocal dateCount + if id not in song["key"]: + return "undef" + if len(song["date"]) <= dateCount: + return "error" + dateCount += 1 + return song["date"][dateCount - 1] + + newSong["Track ID"] = getInt("Track ID") + newSong["Name"] = getStr("Name") + newSong["Artist"] = getStr("Artist") + newSong["Album Artist"] = getStr("Album Artist") + newSong["Composer"] = getStr("Composer") + newSong["Album"] = getStr("Album") + newSong["Genre"] = getStr("Genre") + newSong["Kind"] = getStr("Kind") + newSong["Size"] = getInt("Size") + newSong["Total Time"] = getInt("Total Time") + newSong["Disc Number"] = getInt("Disc Number") + newSong["Disc Count"] = getInt("Disc Count") + newSong["Track Number"] = getInt("Track Number") + newSong["Track Count"] = getInt("Track Count") + newSong["Year"] = getInt("Year") + newSong["Date Modified"] = getDate("Date Modified") + newSong["Date Added"] = getDate("Date Added") + newSong["Bit Rate"] = getInt("Bit Rate") + newSong["Sample Rate"] = getInt("Sample Rate") + newSong["Play Count"] = getInt("Play Count") + newSong["Play Date"] = getInt("Play Date") + newSong["Play Date UTC"] = getDate("Play Date UTC") + newSong["Skip Count"] = getInt("Skip Count") + newSong["Skip Date"] = getDate("Skip Date") + newSong["Release Date"] = getDate("Release Date") + newSong["Album Rating"] = getInt("Album Rating") + newSong["Album Rating Computed"] = "Album Rating Computed" in song["key"] + newSong["Loved"] = "Loved" in song["key"] + newSong["Album Loved"] = "Album Loved" in song["key"] + newSong["Explicit"] = "Explicit" in song["key"] + newSong["Compilation"] = "Compilation" in song["key"] + newSong["Artwork Count"] = getInt("Artwork Count") + newSong["Sort Album"] = getStr("Sort Album") + newSong["Sort Artist"] = getStr("Sort Artist") + newSong["Sort Name"] = getStr("Sort Name") + newSong["Persistent ID"] = getStr("Persistent ID") + newSong["Track Type"] = getStr("Track Type") + + out.append(newSong) + + return out + + +def convert(filename, out_path): + with open(filename, 'r', encoding='utf-8') as xml_file: + data_dict = xmltodict.parse(xml_file.read()) + + # Extract the "Tracks" dictionary to be flattened + tracks_dict = data_dict['plist']['dict']['dict'] + + flat_tracks_dict = flatten_dict(tracks_dict["dict"]) + + json_data = json.dumps(flat_tracks_dict, indent=2) + + with open(out_path, 'w', encoding='utf-8') as json_file: + json_file.write(json_data) + + +if __name__ == "__main__": + convert('./prefetched/ItunesLibrary.xml', './prefetched/ItunesLibrary.json') diff --git a/env b/env new file mode 100755 index 0000000..8955572 --- /dev/null +++ b/env @@ -0,0 +1,36 @@ + +PS1=" > " + +#export PATH="/usr/local/bin/:$PATH" +export PATH="$HOME/bin/scripts:$HOME/bin/:$PATH" +#export PATH="$HOME/home/auser/.local/bin:$PATH" + +export SIP="185.238.170.251" + +alias v=nvim +alias ll="ls -l -a" +alias gl="git log --oneline" +alias ssh_server="ssh auser@185.238.170.251" + +#eval "$(zoxide init bash --cmd zd)" + +#source $PMAIN/.scripts/bashmarks.sh + +fe() { + local result=$(command tere "$@") + [ -n "$result" ] && cd -- "$result" +} + +vim_configure() { + pwd=$(pwd) + cd ~/.config/nvim/ + nvim + cd $pwd +} + +pyenv() { + source ~/bin/python/env311/bin/activate +} + +source ~/src/scripts/remotes.sh +source /usr/share/fzf/key-bindings.bash diff --git a/main.py b/main.py index 6e04234..204c0ab 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ from DataBase import * -from SpotifyWebAPI import fetch_data +from SpotifyWebAPI import fetch_data, update_access_token import cmd playlists = get_data('playlistTracks') @@ -167,6 +167,7 @@ class Interpreter(cmd.Cmd): def do_fetch(self, arg): """Fetch all spotify data""" try: + update_access_token() fetch_data() global playlists, top_artists, top_tracks, user_tracks From e68e29fa4adae3cebf501c2935299d90e84385b8 Mon Sep 17 00:00:00 2001 From: Ilya Shurupov <163508118+elushaX@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:26:51 +0300 Subject: [PATCH 18/18] tmp --- Linker.py | 2 - LocalLibrary.py => LocalLibraryIndexer.py | 0 ParseItunesToJson.py | 101 ++++++++++++++++++++++ env | 36 ++++++++ main.py | 3 +- 5 files changed, 139 insertions(+), 3 deletions(-) delete mode 100644 Linker.py rename LocalLibrary.py => LocalLibraryIndexer.py (100%) create mode 100755 ParseItunesToJson.py create mode 100755 env diff --git a/Linker.py b/Linker.py deleted file mode 100644 index 139597f..0000000 --- a/Linker.py +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/LocalLibrary.py b/LocalLibraryIndexer.py similarity index 100% rename from LocalLibrary.py rename to LocalLibraryIndexer.py diff --git a/ParseItunesToJson.py b/ParseItunesToJson.py new file mode 100755 index 0000000..1505a36 --- /dev/null +++ b/ParseItunesToJson.py @@ -0,0 +1,101 @@ +import xmltodict +import json + + +def flatten_dict(d): + + out = [] + for song in d: + newSong = {} + + strCount = 0 + intCount = 0 + dateCount = 0 + + def getStr(id): + nonlocal strCount + if id not in song["key"]: + return "undef" + if len(song["string"]) <= strCount: + return "error" + strCount += 1 + return song["string"][strCount - 1] + + def getInt(id): + nonlocal intCount + if id not in song["key"]: + return -1 + if len(song["integer"]) <= intCount: + return -1 + intCount += 1 + return song["integer"][intCount - 1] + + def getDate(id): + nonlocal dateCount + if id not in song["key"]: + return "undef" + if len(song["date"]) <= dateCount: + return "error" + dateCount += 1 + return song["date"][dateCount - 1] + + newSong["Track ID"] = getInt("Track ID") + newSong["Name"] = getStr("Name") + newSong["Artist"] = getStr("Artist") + newSong["Album Artist"] = getStr("Album Artist") + newSong["Composer"] = getStr("Composer") + newSong["Album"] = getStr("Album") + newSong["Genre"] = getStr("Genre") + newSong["Kind"] = getStr("Kind") + newSong["Size"] = getInt("Size") + newSong["Total Time"] = getInt("Total Time") + newSong["Disc Number"] = getInt("Disc Number") + newSong["Disc Count"] = getInt("Disc Count") + newSong["Track Number"] = getInt("Track Number") + newSong["Track Count"] = getInt("Track Count") + newSong["Year"] = getInt("Year") + newSong["Date Modified"] = getDate("Date Modified") + newSong["Date Added"] = getDate("Date Added") + newSong["Bit Rate"] = getInt("Bit Rate") + newSong["Sample Rate"] = getInt("Sample Rate") + newSong["Play Count"] = getInt("Play Count") + newSong["Play Date"] = getInt("Play Date") + newSong["Play Date UTC"] = getDate("Play Date UTC") + newSong["Skip Count"] = getInt("Skip Count") + newSong["Skip Date"] = getDate("Skip Date") + newSong["Release Date"] = getDate("Release Date") + newSong["Album Rating"] = getInt("Album Rating") + newSong["Album Rating Computed"] = "Album Rating Computed" in song["key"] + newSong["Loved"] = "Loved" in song["key"] + newSong["Album Loved"] = "Album Loved" in song["key"] + newSong["Explicit"] = "Explicit" in song["key"] + newSong["Compilation"] = "Compilation" in song["key"] + newSong["Artwork Count"] = getInt("Artwork Count") + newSong["Sort Album"] = getStr("Sort Album") + newSong["Sort Artist"] = getStr("Sort Artist") + newSong["Sort Name"] = getStr("Sort Name") + newSong["Persistent ID"] = getStr("Persistent ID") + newSong["Track Type"] = getStr("Track Type") + + out.append(newSong) + + return out + + +def convert(filename, out_path): + with open(filename, 'r', encoding='utf-8') as xml_file: + data_dict = xmltodict.parse(xml_file.read()) + + # Extract the "Tracks" dictionary to be flattened + tracks_dict = data_dict['plist']['dict']['dict'] + + flat_tracks_dict = flatten_dict(tracks_dict["dict"]) + + json_data = json.dumps(flat_tracks_dict, indent=2) + + with open(out_path, 'w', encoding='utf-8') as json_file: + json_file.write(json_data) + + +if __name__ == "__main__": + convert('./prefetched/ItunesLibrary.xml', './prefetched/ItunesLibrary.json') diff --git a/env b/env new file mode 100755 index 0000000..8955572 --- /dev/null +++ b/env @@ -0,0 +1,36 @@ + +PS1=" > " + +#export PATH="/usr/local/bin/:$PATH" +export PATH="$HOME/bin/scripts:$HOME/bin/:$PATH" +#export PATH="$HOME/home/auser/.local/bin:$PATH" + +export SIP="185.238.170.251" + +alias v=nvim +alias ll="ls -l -a" +alias gl="git log --oneline" +alias ssh_server="ssh auser@185.238.170.251" + +#eval "$(zoxide init bash --cmd zd)" + +#source $PMAIN/.scripts/bashmarks.sh + +fe() { + local result=$(command tere "$@") + [ -n "$result" ] && cd -- "$result" +} + +vim_configure() { + pwd=$(pwd) + cd ~/.config/nvim/ + nvim + cd $pwd +} + +pyenv() { + source ~/bin/python/env311/bin/activate +} + +source ~/src/scripts/remotes.sh +source /usr/share/fzf/key-bindings.bash diff --git a/main.py b/main.py index 6e04234..204c0ab 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ from DataBase import * -from SpotifyWebAPI import fetch_data +from SpotifyWebAPI import fetch_data, update_access_token import cmd playlists = get_data('playlistTracks') @@ -167,6 +167,7 @@ class Interpreter(cmd.Cmd): def do_fetch(self, arg): """Fetch all spotify data""" try: + update_access_token() fetch_data() global playlists, top_artists, top_tracks, user_tracks