Spaces:
Sleeping
Sleeping
Nikita Makarov
commited on
Commit
·
bb97e91
1
Parent(s):
3229c47
Add Modal proxy for YouTube search (bypasses HF Spaces DNS restrictions)
Browse files- MODAL_SETUP.md +32 -0
- modal_proxy.py +107 -0
MODAL_SETUP.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal YouTube Proxy Setup Instructions
|
| 2 |
+
|
| 3 |
+
## 1. Install Modal CLI
|
| 4 |
+
pip install modal
|
| 5 |
+
|
| 6 |
+
## 2. Login to Modal
|
| 7 |
+
modal login
|
| 8 |
+
|
| 9 |
+
## 3. Create YouTube API Key (Optional)
|
| 10 |
+
# Go to https://console.developers.google.com/
|
| 11 |
+
# Create project, enable YouTube Data API v3
|
| 12 |
+
# Create API key and add to Modal secrets:
|
| 13 |
+
modal secret create youtube-api-key YOUTUBE_API_KEY=your_key_here
|
| 14 |
+
|
| 15 |
+
## 4. Deploy the proxy
|
| 16 |
+
modal deploy modal_proxy.py
|
| 17 |
+
|
| 18 |
+
## 5. Get the URL
|
| 19 |
+
# After deployment, Modal will show the URL like:
|
| 20 |
+
# https://your-modal-app.modal.run
|
| 21 |
+
|
| 22 |
+
## 6. Update music_server.py
|
| 23 |
+
# Replace 'your-modal-app.modal.run' with your actual Modal URL
|
| 24 |
+
|
| 25 |
+
## 7. Push to HF Spaces
|
| 26 |
+
git add .
|
| 27 |
+
git commit -m 'Add Modal proxy support'
|
| 28 |
+
git push hf main --force
|
| 29 |
+
|
| 30 |
+
## Testing
|
| 31 |
+
curl 'https://your-modal-app.modal.run/youtube-search?query=drake&limit=3'
|
| 32 |
+
|
modal_proxy.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
YouTube Search Proxy for Modal
|
| 4 |
+
Bypasses DNS restrictions in HuggingFace Spaces
|
| 5 |
+
"""
|
| 6 |
+
import modal
|
| 7 |
+
import requests
|
| 8 |
+
import json
|
| 9 |
+
from typing import Dict, List, Any
|
| 10 |
+
|
| 11 |
+
# Create Modal app
|
| 12 |
+
app = modal.App("youtube-proxy")
|
| 13 |
+
|
| 14 |
+
# Define image with dependencies
|
| 15 |
+
image = modal.Image.debian_slim().pip_install(
|
| 16 |
+
"requests",
|
| 17 |
+
"fastapi",
|
| 18 |
+
"uvicorn"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
@app.function(image=image, secrets=[modal.Secret.from_name("youtube-api-key")])
|
| 22 |
+
@modal.web_endpoint(method="GET", label="youtube-search")
|
| 23 |
+
def search_youtube(query: str, limit: int = 5) -> Dict[str, Any]:
|
| 24 |
+
"""Search YouTube via official API"""
|
| 25 |
+
try:
|
| 26 |
+
# YouTube Data API v3 search endpoint
|
| 27 |
+
url = "https://www.googleapis.com/youtube/v3/search"
|
| 28 |
+
params = {
|
| 29 |
+
'part': 'snippet',
|
| 30 |
+
'q': f"{query} music",
|
| 31 |
+
'type': 'video',
|
| 32 |
+
'maxResults': limit,
|
| 33 |
+
'order': 'relevance',
|
| 34 |
+
'safeSearch': 'moderate',
|
| 35 |
+
'key': "" # Will use Modal secret
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
response = requests.get(url, params=params, timeout=30)
|
| 39 |
+
response.raise_for_status()
|
| 40 |
+
data = response.json()
|
| 41 |
+
|
| 42 |
+
tracks = []
|
| 43 |
+
if 'items' in data:
|
| 44 |
+
for item in data['items'][:limit]:
|
| 45 |
+
if item.get('id', {}).get('videoId'):
|
| 46 |
+
video_id = item['id']['videoId']
|
| 47 |
+
snippet = item.get('snippet', {})
|
| 48 |
+
|
| 49 |
+
track = {
|
| 50 |
+
"title": snippet.get('title', 'Unknown'),
|
| 51 |
+
"artist": snippet.get('channelTitle', 'Unknown Artist'),
|
| 52 |
+
"url": f"https://www.youtube.com/watch?v={video_id}",
|
| 53 |
+
"youtube_id": video_id,
|
| 54 |
+
"duration": 0,
|
| 55 |
+
"genre": query.split()[0] if query else "unknown",
|
| 56 |
+
"source": "youtube_api",
|
| 57 |
+
"thumbnail": snippet.get('thumbnails', {}).get('default', {}).get('url', '')
|
| 58 |
+
}
|
| 59 |
+
tracks.append(track)
|
| 60 |
+
|
| 61 |
+
return {"success": True, "tracks": tracks}
|
| 62 |
+
|
| 63 |
+
except Exception as e:
|
| 64 |
+
return {"success": False, "error": str(e), "tracks": []}
|
| 65 |
+
|
| 66 |
+
@app.function(image=image)
|
| 67 |
+
@modal.web_endpoint(method="GET", label="soundcloud-search")
|
| 68 |
+
def search_soundcloud(query: str, limit: int = 5) -> Dict[str, Any]:
|
| 69 |
+
"""Search SoundCloud via API"""
|
| 70 |
+
try:
|
| 71 |
+
# SoundCloud API search
|
| 72 |
+
search_query = f"{query} music"
|
| 73 |
+
url = f"https://api-v2.soundcloud.com/search/tracks"
|
| 74 |
+
params = {
|
| 75 |
+
'q': search_query,
|
| 76 |
+
'limit': limit,
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
response = requests.get(url, params=params, timeout=30)
|
| 80 |
+
if response.status_code == 200:
|
| 81 |
+
data = response.json()
|
| 82 |
+
tracks = []
|
| 83 |
+
|
| 84 |
+
if 'collection' in data:
|
| 85 |
+
for item in data['collection'][:limit]:
|
| 86 |
+
if item.get('streamable'):
|
| 87 |
+
track = {
|
| 88 |
+
"title": item.get('title', 'Unknown'),
|
| 89 |
+
"artist": item.get('user', {}).get('username', 'Unknown Artist'),
|
| 90 |
+
"url": item.get('permalink_url', ''),
|
| 91 |
+
"duration": item.get('duration', 0) // 1000,
|
| 92 |
+
"genre": query.split()[0] if query else "unknown",
|
| 93 |
+
"source": "soundcloud_api"
|
| 94 |
+
}
|
| 95 |
+
tracks.append(track)
|
| 96 |
+
|
| 97 |
+
return {"success": True, "tracks": tracks}
|
| 98 |
+
else:
|
| 99 |
+
return {"success": False, "error": f"HTTP {response.status_code}", "tracks": []}
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return {"success": False, "error": str(e), "tracks": []}
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
# For local testing
|
| 106 |
+
print("Modal YouTube Proxy")
|
| 107 |
+
print("Deploy with: modal deploy modal_proxy.py")
|