first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/credentials.json
|
||||
/test.py
|
||||
/venv
|
||||
14
app.py
Normal file
14
app.py
Normal 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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
downloads/playlists/The Weeknd/Starboy/Reminder (NORMAL)..ogg
Normal file
BIN
downloads/playlists/The Weeknd/Starboy/Reminder (NORMAL)..ogg
Normal file
Binary file not shown.
Binary file not shown.
45
requirements.txt
Normal file
45
requirements.txt
Normal 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
0
routes/__init__.py
Normal file
BIN
routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
routes/__pycache__/search.cpython-312.pyc
Normal file
BIN
routes/__pycache__/search.cpython-312.pyc
Normal file
Binary file not shown.
40
routes/search.py
Normal file
40
routes/search.py
Normal 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
0
routes/utils/__init__.py
Normal file
BIN
routes/utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
routes/utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
routes/utils/__pycache__/search.cpython-312.pyc
Normal file
BIN
routes/utils/__pycache__/search.cpython-312.pyc
Normal file
Binary file not shown.
178
routes/utils/search.py
Normal file
178
routes/utils/search.py
Normal 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 []
|
||||
Reference in New Issue
Block a user