commit be4d01f32682a3df814efd60f4e3f12f0a78feb8 Author: cool.gitter.choco Date: Sat Jan 25 08:19:33 2025 -0600 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaef9d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/credentials.json +/test.py +/venv \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..eff025e --- /dev/null +++ b/app.py @@ -0,0 +1,14 @@ +from flask import Flask +from flask_cors import CORS +from routes.search import search_bp + +def create_app(): + app = Flask(__name__) + CORS(app) + app.register_blueprint(search_bp, url_prefix='/api') + return app + +if __name__ == '__main__': + from waitress import serve + app = create_app() + serve(app, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/downloads/albums/Ardhito Pramono/Waking Up Together With You/Waking Up Together With You (HIGH)..ogg b/downloads/albums/Ardhito Pramono/Waking Up Together With You/Waking Up Together With You (HIGH)..ogg new file mode 100644 index 0000000..4ba9ab1 Binary files /dev/null and b/downloads/albums/Ardhito Pramono/Waking Up Together With You/Waking Up Together With You (HIGH)..ogg differ diff --git a/downloads/albums/Waking Up Together With You - Ardhito Pramono/1|1 - Waking Up Together With You - Ardhito Pramono (HIGH).ogg b/downloads/albums/Waking Up Together With You - Ardhito Pramono/1|1 - Waking Up Together With You - Ardhito Pramono (HIGH).ogg new file mode 100644 index 0000000..3be3c65 Binary files /dev/null and b/downloads/albums/Waking Up Together With You - Ardhito Pramono/1|1 - Waking Up Together With You - Ardhito Pramono (HIGH).ogg differ diff --git a/downloads/albums/Waking Up Together With You - Ardhito PramonoWaking Up Together With You - Ardhito Pramono 3617221609140 (HIGH).zip b/downloads/albums/Waking Up Together With You - Ardhito PramonoWaking Up Together With You - Ardhito Pramono 3617221609140 (HIGH).zip new file mode 100644 index 0000000..9800daf Binary files /dev/null and b/downloads/albums/Waking Up Together With You - Ardhito PramonoWaking Up Together With You - Ardhito Pramono 3617221609140 (HIGH).zip differ diff --git a/downloads/playlists/Charlie Puth/Nine Track Mind (Special Edition)/One Call Away - Acoustic (NORMAL)..ogg b/downloads/playlists/Charlie Puth/Nine Track Mind (Special Edition)/One Call Away - Acoustic (NORMAL)..ogg new file mode 100644 index 0000000..e69de29 diff --git a/downloads/playlists/The Weeknd & Ariana Grande/Save Your Tears (Remix)/Save Your Tears (with Ariana Grande) (Remix) (NORMAL)..ogg b/downloads/playlists/The Weeknd & Ariana Grande/Save Your Tears (Remix)/Save Your Tears (with Ariana Grande) (Remix) (NORMAL)..ogg new file mode 100644 index 0000000..da42045 Binary files /dev/null and b/downloads/playlists/The Weeknd & Ariana Grande/Save Your Tears (Remix)/Save Your Tears (with Ariana Grande) (Remix) (NORMAL)..ogg differ diff --git a/downloads/playlists/The Weeknd & Ariana Grande/Starboy (Deluxe)/Die For You (with Ariana Grande) - Remix (NORMAL)..ogg b/downloads/playlists/The Weeknd & Ariana Grande/Starboy (Deluxe)/Die For You (with Ariana Grande) - Remix (NORMAL)..ogg new file mode 100644 index 0000000..95450d3 Binary files /dev/null and b/downloads/playlists/The Weeknd & Ariana Grande/Starboy (Deluxe)/Die For You (with Ariana Grande) - Remix (NORMAL)..ogg differ diff --git a/downloads/playlists/The Weeknd & Daft Punk/Starboy/Starboy (NORMAL)..ogg b/downloads/playlists/The Weeknd & Daft Punk/Starboy/Starboy (NORMAL)..ogg new file mode 100644 index 0000000..3116e3b Binary files /dev/null and b/downloads/playlists/The Weeknd & Daft Punk/Starboy/Starboy (NORMAL)..ogg differ diff --git a/downloads/playlists/The Weeknd & Playboi Carti & Madonna/Popular (Music from the HBO Original Series)/Popular (with Playboi Carti & Madonna) - From The Idol Vol. 1 (Music from the HBO Original Series) (NORMAL)..ogg b/downloads/playlists/The Weeknd & Playboi Carti & Madonna/Popular (Music from the HBO Original Series)/Popular (with Playboi Carti & Madonna) - From The Idol Vol. 1 (Music from the HBO Original Series) (NORMAL)..ogg new file mode 100644 index 0000000..e18e047 Binary files /dev/null and b/downloads/playlists/The Weeknd & Playboi Carti & Madonna/Popular (Music from the HBO Original Series)/Popular (with Playboi Carti & Madonna) - From The Idol Vol. 1 (Music from the HBO Original Series) (NORMAL)..ogg differ diff --git a/downloads/playlists/The Weeknd/Starboy/Reminder (NORMAL)..ogg b/downloads/playlists/The Weeknd/Starboy/Reminder (NORMAL)..ogg new file mode 100644 index 0000000..7a2f1b9 Binary files /dev/null and b/downloads/playlists/The Weeknd/Starboy/Reminder (NORMAL)..ogg differ diff --git a/downloads/tracks/Fifth Harmony and Ty Dolla sign/Work from Home (feat. Ty Dolla sign) - Fifth Harmony and Ty Dolla sign (NORMAL).ogg b/downloads/tracks/Fifth Harmony and Ty Dolla sign/Work from Home (feat. Ty Dolla sign) - Fifth Harmony and Ty Dolla sign (NORMAL).ogg new file mode 100644 index 0000000..fa6300f Binary files /dev/null and b/downloads/tracks/Fifth Harmony and Ty Dolla sign/Work from Home (feat. Ty Dolla sign) - Fifth Harmony and Ty Dolla sign (NORMAL).ogg differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6a7e608 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,45 @@ +annotated-types==0.7.0 +anyio==4.8.0 +blinker==1.9.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +click==8.1.8 +deezspot @ git+https://github.com/Xoconoch/deezspot-fork@a10c9e1fa35d84869e8e396b0085404e34d822d5 +defusedxml==0.7.1 +fastapi==0.115.7 +Flask==3.1.0 +Flask-Cors==5.0.0 +h11==0.14.0 +httptools==0.6.4 +idna==3.10 +ifaddr==0.2.0 +itsdangerous==2.2.0 +Jinja2==3.1.5 +librespot==0.0.9 +MarkupSafe==3.0.2 +mutagen==1.47.0 +protobuf==3.20.1 +pycryptodome==3.21.0 +pycryptodomex==3.17 +pydantic==2.10.6 +pydantic_core==2.27.2 +PyOgg==0.6.14a1 +python-dotenv==1.0.1 +PyYAML==6.0.2 +redis==5.2.1 +requests==2.30.0 +sniffio==1.3.1 +spotipy==2.25.0 +spotipy_anon==1.3 +starlette==0.45.3 +tqdm==4.67.1 +typing_extensions==4.12.2 +urllib3==2.3.0 +uvicorn==0.34.0 +uvloop==0.21.0 +waitress==3.0.2 +watchfiles==1.0.4 +websocket-client==1.5.1 +websockets==14.2 +Werkzeug==3.1.3 +zeroconf==0.62.0 diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routes/__pycache__/__init__.cpython-312.pyc b/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..96949c9 Binary files /dev/null and b/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/routes/__pycache__/search.cpython-312.pyc b/routes/__pycache__/search.cpython-312.pyc new file mode 100644 index 0000000..654d88e Binary files /dev/null and b/routes/__pycache__/search.cpython-312.pyc differ diff --git a/routes/search.py b/routes/search.py new file mode 100644 index 0000000..c05aa5f --- /dev/null +++ b/routes/search.py @@ -0,0 +1,40 @@ +from flask import Blueprint, jsonify, request +from routes.utils.search import search_and_combine + +search_bp = Blueprint('search', __name__) + +@search_bp.route('/search', methods=['GET']) +def handle_search(): + try: + # Get query parameters + query = request.args.get('q', '') + search_type = request.args.get('type', 'track') + service = request.args.get('service', 'both') + limit = int(request.args.get('limit', 10)) + + # Validate parameters + if not query: + return jsonify({'error': 'Missing search query'}), 400 + + valid_types = ['track', 'album', 'artist', 'playlist', 'episode'] + if search_type not in valid_types: + return jsonify({'error': 'Invalid search type'}), 400 + + # Perform the search + results = search_and_combine( + query=query, + search_type=search_type, + service=service, + limit=limit + ) + + return jsonify({ + 'results': results, + 'count': len(results), + 'error': None + }) + + except ValueError as e: + return jsonify({'error': str(e)}), 400 + except Exception as e: + return jsonify({'error': 'Internal server error'}), 500 \ No newline at end of file diff --git a/routes/utils/__init__.py b/routes/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routes/utils/__pycache__/__init__.cpython-312.pyc b/routes/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ac1ba28 Binary files /dev/null and b/routes/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/routes/utils/__pycache__/search.cpython-312.pyc b/routes/utils/__pycache__/search.cpython-312.pyc new file mode 100644 index 0000000..d6ccf36 Binary files /dev/null and b/routes/utils/__pycache__/search.cpython-312.pyc differ diff --git a/routes/utils/search.py b/routes/utils/search.py new file mode 100644 index 0000000..4e051c5 --- /dev/null +++ b/routes/utils/search.py @@ -0,0 +1,178 @@ +from deezspot.easy_spoty import Spo +from deezspot.deezloader import API +import json +import difflib +from typing import List, Dict + +def string_similarity(a: str, b: str) -> float: + return difflib.SequenceMatcher(None, a.lower(), b.lower()).ratio() + +def normalize_item(item: Dict, service: str, item_type: str) -> Dict: + normalized = { + "service": service, + "type": item_type + } + + if item_type == "track": + normalized.update({ + "id": item.get('id'), + "title": item.get('title') if service == "deezer" else item.get('name'), + "artists": [{"name": item['artist']['name']}] if service == "deezer" + else [{"name": a['name']} for a in item.get('artists', [])], + "album": { + "title": item['album']['title'] if service == "deezer" else item['album']['name'], + "id": item['album']['id'] if service == "deezer" else item['album'].get('id'), + }, + "duration": item.get('duration') if service == "deezer" else item.get('duration_ms'), + "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), + "isrc": item.get('isrc') if service == "deezer" else item.get('external_ids', {}).get('isrc') + }) + + elif item_type == "album": + normalized.update({ + "id": item.get('id'), + "title": item.get('title') if service == "deezer" else item.get('name'), + "artists": [{"name": item['artist']['name']}] if service == "deezer" + else [{"name": a['name']} for a in item.get('artists', [])], + "total_tracks": item.get('nb_tracks') if service == "deezer" else item.get('total_tracks'), + "release_date": item.get('release_date'), + "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), + "images": [ + {"url": item.get('cover_xl')}, + {"url": item.get('cover_big')}, + {"url": item.get('cover_medium')} + ] if service == "deezer" else item.get('images', []) + }) + + elif item_type == "artist": + normalized.update({ + "id": item.get('id'), + "name": item.get('name'), + "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), + "images": [ + {"url": item.get('picture_xl')}, + {"url": item.get('picture_big')}, + {"url": item.get('picture_medium')} + ] if service == "deezer" else item.get('images', []) + }) + + else: # For playlists, episodes, etc. + normalized.update({ + "id": item.get('id'), + "title": item.get('title') if service == "deezer" else item.get('name'), + "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), + "description": item.get('description'), + "owner": item.get('user', {}).get('name') if service == "deezer" else item.get('owner', {}).get('display_name') + }) + + return {k: v for k, v in normalized.items() if v is not None} + +def is_same_item(deezer_item: Dict, spotify_item: Dict, item_type: str) -> bool: + deezer_normalized = normalize_item(deezer_item, "deezer", item_type) + spotify_normalized = normalize_item(spotify_item, "spotify", item_type) + + if item_type == "track": + title_match = string_similarity(deezer_normalized['title'], spotify_normalized['title']) >= 0.8 + artist_match = string_similarity( + deezer_normalized['artists'][0]['name'], + spotify_normalized['artists'][0]['name'] + ) >= 0.8 + album_match = string_similarity( + deezer_normalized['album']['title'], + spotify_normalized['album']['title'] + ) >= 0.9 + return title_match and artist_match and album_match + + if item_type == "album": + title_match = string_similarity(deezer_normalized['title'], spotify_normalized['title']) >= 0.8 + artist_match = string_similarity( + deezer_normalized['artists'][0]['name'], + spotify_normalized['artists'][0]['name'] + ) >= 0.8 + tracks_match = deezer_normalized['total_tracks'] == spotify_normalized['total_tracks'] + return title_match and artist_match and tracks_match + + if item_type == "artist": + name_match = string_similarity(deezer_normalized['name'], spotify_normalized['name']) >= 0.85 + return name_match + + return False + +def process_results(deezer_results: Dict, spotify_results: Dict, search_type: str) -> List[Dict]: + combined = [] + processed_spotify_ids = set() + + for deezer_item in deezer_results.get('data', []): + match_found = False + normalized_deezer = normalize_item(deezer_item, "deezer", search_type) + + for spotify_item in spotify_results.get('items', []): + if is_same_item(deezer_item, spotify_item, search_type): + processed_spotify_ids.add(spotify_item['id']) + match_found = True + break + + combined.append(normalized_deezer) + + for spotify_item in spotify_results.get('items', []): + if spotify_item['id'] not in processed_spotify_ids: + combined.append(normalize_item(spotify_item, "spotify", search_type)) + + return combined + +def search_and_combine( + query: str, + search_type: str, + service: str = "both", + limit: int = 3 +) -> List[Dict]: + if search_type == "playlist" and service == "both": + raise ValueError("Playlist search requires explicit service selection (deezer or spotify)") + + if search_type == "episode" and service != "spotify": + raise ValueError("Episode search is only available for Spotify") + + deezer_data = [] + spotify_items = [] + + # Deezer search with limit + if service in ["both", "deezer"] and search_type != "episode": + deezer_api = API() + deezer_methods = { + 'track': deezer_api.search_track, + 'album': deezer_api.search_album, + 'artist': deezer_api.search_artist, + 'playlist': deezer_api.search_playlist + } + deezer_method = deezer_methods.get(search_type, deezer_api.search) + deezer_response = deezer_method(query, limit=limit) + deezer_data = deezer_response.get('data', [])[:limit] + + if service == "deezer": + return [normalize_item(item, "deezer", search_type) for item in deezer_data] + + # Spotify search with limit + if service in ["both", "spotify"]: + Spo.__init__() + spotify_response = Spo.search(query=query, search_type=search_type, limit=limit) + + if search_type == "episode": + spotify_items = spotify_response.get('episodes', {}).get('items', [])[:limit] + else: + spotify_items = spotify_response.get('tracks', {}).get('items', + spotify_response.get('albums', {}).get('items', + spotify_response.get('artists', {}).get('items', + spotify_response.get('playlists', {}).get('items', []))))[:limit] + + if service == "spotify": + return [normalize_item(item, "spotify", search_type) for item in spotify_items] + + # Combined results + if service == "both" and search_type != "playlist": + return process_results( + {"data": deezer_data}, + {"items": spotify_items}, + search_type + )[:limit] + + return [] \ No newline at end of file