first commit

This commit is contained in:
cool.gitter.choco
2025-01-25 08:19:33 -06:00
commit be4d01f326
21 changed files with 280 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/credentials.json
/test.py
/venv

14
app.py Normal file
View File

@@ -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)

45
requirements.txt Normal file
View File

@@ -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

0
routes/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

40
routes/search.py Normal file
View File

@@ -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

0
routes/utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

178
routes/utils/search.py Normal file
View File

@@ -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 []