superbrain-server 1.0.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/superbrain.js +196 -0
- package/package.json +23 -0
- package/payload/.dockerignore +45 -0
- package/payload/.env.example +58 -0
- package/payload/Dockerfile +73 -0
- package/payload/analyzers/__init__.py +0 -0
- package/payload/analyzers/audio_transcribe.py +225 -0
- package/payload/analyzers/caption.py +244 -0
- package/payload/analyzers/music_identifier.py +346 -0
- package/payload/analyzers/text_analyzer.py +117 -0
- package/payload/analyzers/visual_analyze.py +218 -0
- package/payload/analyzers/webpage_analyzer.py +789 -0
- package/payload/analyzers/youtube_analyzer.py +320 -0
- package/payload/api.py +1676 -0
- package/payload/config/.api_keys.example +22 -0
- package/payload/config/model_rankings.json +492 -0
- package/payload/config/openrouter_free_models.json +1364 -0
- package/payload/config/whisper_model.txt +1 -0
- package/payload/config_settings.py +185 -0
- package/payload/core/__init__.py +0 -0
- package/payload/core/category_manager.py +219 -0
- package/payload/core/database.py +811 -0
- package/payload/core/link_checker.py +300 -0
- package/payload/core/model_router.py +1253 -0
- package/payload/docker-compose.yml +120 -0
- package/payload/instagram/__init__.py +0 -0
- package/payload/instagram/instagram_downloader.py +253 -0
- package/payload/instagram/instagram_login.py +190 -0
- package/payload/main.py +912 -0
- package/payload/requirements.txt +39 -0
- package/payload/reset.py +311 -0
- package/payload/start-docker-prod.sh +125 -0
- package/payload/start-docker.sh +56 -0
- package/payload/start.py +1302 -0
- package/payload/static/favicon.ico +0 -0
- package/payload/stop-docker.sh +16 -0
- package/payload/utils/__init__.py +0 -0
- package/payload/utils/db_stats.py +108 -0
- package/payload/utils/manage_token.py +91 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Simple Instagram Caption Extractor
|
|
5
|
+
Fast, reliable, no rate limiting using direct HTML parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import html
|
|
13
|
+
import io
|
|
14
|
+
|
|
15
|
+
# Force UTF-8 encoding for stdout on Windows
|
|
16
|
+
if sys.platform == 'win32':
|
|
17
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
18
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_valid_instagram_url(url):
|
|
22
|
+
"""
|
|
23
|
+
Check if the URL is a valid Instagram post/reel URL.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
url: Instagram URL to validate
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Boolean indicating if URL is valid
|
|
30
|
+
"""
|
|
31
|
+
patterns = [
|
|
32
|
+
r'instagram\.com/p/[A-Za-z0-9_-]+', # Regular posts
|
|
33
|
+
r'instagram\.com/reel/[A-Za-z0-9_-]+', # Reels
|
|
34
|
+
r'instagram\.com/tv/[A-Za-z0-9_-]+', # IGTV
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
return any(re.search(pattern, url) for pattern in patterns)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def clean_caption(caption):
|
|
41
|
+
"""
|
|
42
|
+
Clean the caption by removing metadata, hashtags, and decoding HTML entities.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
caption: Raw caption text
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Cleaned caption text
|
|
49
|
+
"""
|
|
50
|
+
if not caption:
|
|
51
|
+
return caption
|
|
52
|
+
|
|
53
|
+
# Decode HTML entities (e.g., " -> ", ❤ -> ❤)
|
|
54
|
+
caption = html.unescape(caption)
|
|
55
|
+
|
|
56
|
+
# Remove Instagram metadata patterns - multiple variations
|
|
57
|
+
# Pattern 1: "123 likes, 45 comments - username on Date: "
|
|
58
|
+
# Pattern 2: "12K likes, 50 comments - username on Date: "
|
|
59
|
+
# Pattern 3: "1,277 likes, 34 comments - username on Date: "
|
|
60
|
+
caption = re.sub(r'^\s*[\d,\.]+[KMB]?\s*(likes?|comments?)[^:]*?:\s*["\']?', '', caption, flags=re.IGNORECASE)
|
|
61
|
+
|
|
62
|
+
# Remove trailing quotes
|
|
63
|
+
caption = re.sub(r'["\']\.?\s*$', '', caption)
|
|
64
|
+
|
|
65
|
+
# Remove trailing metadata like "- See photos and videos"
|
|
66
|
+
caption = re.sub(r'\s*-\s*See\s+(photos?|videos?).*$', '', caption, flags=re.IGNORECASE)
|
|
67
|
+
|
|
68
|
+
# Remove "X likes, Y comments" patterns at the end
|
|
69
|
+
caption = re.sub(r'\s*[\d,\.]+[KMB]?\s*(likes?|comments?).*$', '', caption, flags=re.IGNORECASE)
|
|
70
|
+
|
|
71
|
+
# Remove hashtags (including the # symbol and the tag text)
|
|
72
|
+
caption = re.sub(r'#\w+', '', caption)
|
|
73
|
+
|
|
74
|
+
# Clean up extra quotes at the beginning and end
|
|
75
|
+
caption = caption.strip('"\'')
|
|
76
|
+
|
|
77
|
+
# Clean up extra whitespace and newlines
|
|
78
|
+
caption = re.sub(r'\n\s*\n+', '\n', caption) # Remove multiple blank lines
|
|
79
|
+
caption = re.sub(r'[ \t]+', ' ', caption) # Normalize spaces
|
|
80
|
+
caption = caption.strip()
|
|
81
|
+
|
|
82
|
+
# Remove lines that only contain dots or whitespace
|
|
83
|
+
lines = caption.split('\n')
|
|
84
|
+
lines = [line.strip() for line in lines if line.strip() and line.strip() != '.']
|
|
85
|
+
caption = '\n'.join(lines)
|
|
86
|
+
|
|
87
|
+
return caption
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_caption(url):
|
|
92
|
+
"""
|
|
93
|
+
Get the caption from an Instagram post or reel by parsing HTML.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
url: Instagram post or reel URL
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Caption text or error message
|
|
100
|
+
"""
|
|
101
|
+
# Validate URL
|
|
102
|
+
if not is_valid_instagram_url(url):
|
|
103
|
+
return "❌ Invalid Instagram URL. Please provide a valid post or reel link."
|
|
104
|
+
|
|
105
|
+
# Clean URL - remove query parameters and trailing slashes
|
|
106
|
+
url = url.split('?')[0].rstrip('/') + '/'
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Request headers to mimic a browser
|
|
110
|
+
headers = {
|
|
111
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
112
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
113
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
114
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
115
|
+
'DNT': '1',
|
|
116
|
+
'Connection': 'keep-alive',
|
|
117
|
+
'Upgrade-Insecure-Requests': '1',
|
|
118
|
+
'Sec-Fetch-Dest': 'document',
|
|
119
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
120
|
+
'Sec-Fetch-Site': 'none',
|
|
121
|
+
'Cache-Control': 'max-age=0',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Make request
|
|
125
|
+
response = requests.get(url, headers=headers, timeout=15)
|
|
126
|
+
|
|
127
|
+
if response.status_code != 200:
|
|
128
|
+
return f"❌ Error: Unable to fetch post (Status code: {response.status_code})"
|
|
129
|
+
|
|
130
|
+
# Get text - let requests handle any decompression
|
|
131
|
+
html = response.text
|
|
132
|
+
|
|
133
|
+
# Method 1: Try to extract from JSON-LD structured data
|
|
134
|
+
json_ld_pattern = r'<script type="application/ld\+json">(.*?)</script>'
|
|
135
|
+
json_ld_matches = re.findall(json_ld_pattern, html, re.DOTALL)
|
|
136
|
+
|
|
137
|
+
for json_str in json_ld_matches:
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(json_str)
|
|
140
|
+
if isinstance(data, dict):
|
|
141
|
+
# Check for caption in various fields
|
|
142
|
+
caption = data.get('caption') or data.get('description') or data.get('articleBody')
|
|
143
|
+
if caption:
|
|
144
|
+
return clean_caption(caption)
|
|
145
|
+
except:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Method 2: Extract from meta tags
|
|
149
|
+
meta_patterns = [
|
|
150
|
+
r'<meta property="og:description" content="([^"]*)"',
|
|
151
|
+
r'<meta name="description" content="([^"]*)"',
|
|
152
|
+
r'<meta property="og:title" content="([^"]*)"',
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
for pattern in meta_patterns:
|
|
156
|
+
match = re.search(pattern, html)
|
|
157
|
+
if match:
|
|
158
|
+
caption = match.group(1)
|
|
159
|
+
caption = clean_caption(caption)
|
|
160
|
+
if caption and len(caption) > 10: # Make sure it's not just metadata
|
|
161
|
+
return caption
|
|
162
|
+
|
|
163
|
+
# Method 3: Try to find in embedded JSON data
|
|
164
|
+
shared_data_pattern = r'window\._sharedData\s*=\s*({.*?});'
|
|
165
|
+
match = re.search(shared_data_pattern, html)
|
|
166
|
+
if match:
|
|
167
|
+
try:
|
|
168
|
+
shared_data = json.loads(match.group(1))
|
|
169
|
+
# Navigate through the nested structure
|
|
170
|
+
entry_data = shared_data.get('entry_data', {})
|
|
171
|
+
|
|
172
|
+
# Try PostPage
|
|
173
|
+
if 'PostPage' in entry_data:
|
|
174
|
+
media = entry_data['PostPage'][0]['graphql']['shortcode_media']
|
|
175
|
+
caption_edges = media.get('edge_media_to_caption', {}).get('edges', [])
|
|
176
|
+
if caption_edges:
|
|
177
|
+
return clean_caption(caption_edges[0]['node']['text'])
|
|
178
|
+
|
|
179
|
+
except:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# Method 4: Try additional_data pattern
|
|
183
|
+
additional_pattern = r'"caption":\s*"([^"]*)"'
|
|
184
|
+
matches = re.findall(additional_pattern, html)
|
|
185
|
+
if matches:
|
|
186
|
+
# Get the longest caption (likely the actual post caption)
|
|
187
|
+
caption = max(matches, key=len)
|
|
188
|
+
if caption:
|
|
189
|
+
# Decode unicode escapes
|
|
190
|
+
caption = caption.encode().decode('unicode_escape')
|
|
191
|
+
return clean_caption(caption)
|
|
192
|
+
|
|
193
|
+
return "ℹ️ Could not extract caption. The post may have no caption or Instagram's HTML structure has changed."
|
|
194
|
+
|
|
195
|
+
except requests.exceptions.Timeout:
|
|
196
|
+
return "❌ Request timed out. Please check your internet connection and try again."
|
|
197
|
+
|
|
198
|
+
except requests.exceptions.ConnectionError:
|
|
199
|
+
return "❌ Connection error. Please check your internet connection."
|
|
200
|
+
|
|
201
|
+
except requests.exceptions.RequestException as e:
|
|
202
|
+
return f"❌ Request error: {str(e)}"
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return f"❌ Unexpected error: {str(e)}"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
"""Main function to run the caption extractor."""
|
|
210
|
+
# Check if URL was provided as command line argument
|
|
211
|
+
if len(sys.argv) > 1:
|
|
212
|
+
url = sys.argv[1]
|
|
213
|
+
# When called from API, just print the caption
|
|
214
|
+
caption = get_caption(url)
|
|
215
|
+
print(caption)
|
|
216
|
+
else:
|
|
217
|
+
# Interactive mode
|
|
218
|
+
print("=" * 60)
|
|
219
|
+
print("📸 Instagram Caption Extractor")
|
|
220
|
+
print("=" * 60)
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
# Prompt for URL
|
|
224
|
+
url = input("Enter Instagram post or reel URL: ").strip()
|
|
225
|
+
|
|
226
|
+
if not url:
|
|
227
|
+
print("❌ No URL provided. Exiting.")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
print()
|
|
231
|
+
print("🔍 Fetching caption...")
|
|
232
|
+
print()
|
|
233
|
+
|
|
234
|
+
# Get and display caption
|
|
235
|
+
caption = get_caption(url)
|
|
236
|
+
|
|
237
|
+
print("📝 Caption:")
|
|
238
|
+
print("-" * 60)
|
|
239
|
+
print(caption)
|
|
240
|
+
print("-" * 60)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Music Identifier – Optimized Shazam multi-segment recognition
|
|
4
|
+
==============================================================
|
|
5
|
+
Strategy:
|
|
6
|
+
• Quick probe — tries a 12 s fingerprint from several evenly-spaced
|
|
7
|
+
positions first (fast, low-bandwidth)
|
|
8
|
+
• Deep scan — if nothing found, retries the same positions with
|
|
9
|
+
full 20 s segments for trickier tracks
|
|
10
|
+
• Positions — up to 5 evenly-spread offsets scaled to audio length
|
|
11
|
+
so songs that start after speech/ambient sound are caught
|
|
12
|
+
|
|
13
|
+
Output format is identical to the original so that main.py's parser
|
|
14
|
+
requires no changes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
import asyncio
|
|
20
|
+
import subprocess
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Ensure backend root is on sys.path when called as a subprocess
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from shazamio import Shazam
|
|
29
|
+
_HAS_SHAZAM = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
_HAS_SHAZAM = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
# Audio helpers
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
def _get_duration(audio_path: str) -> float:
|
|
39
|
+
"""Return audio duration in seconds via ffprobe. Falls back to 60 s."""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["ffprobe", "-v", "error",
|
|
43
|
+
"-show_entries", "format=duration",
|
|
44
|
+
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
45
|
+
audio_path],
|
|
46
|
+
capture_output=True, text=True, timeout=10,
|
|
47
|
+
)
|
|
48
|
+
return float(result.stdout.strip())
|
|
49
|
+
except Exception:
|
|
50
|
+
return 60.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _extract_segment(audio_path: str, start_sec: float,
|
|
54
|
+
duration: float = 20.0) -> str | None:
|
|
55
|
+
"""Cut a slice from *audio_path* starting at *start_sec*.
|
|
56
|
+
Returns path to a temp MP3 file (caller must delete it), or None.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
fd, seg_path = tempfile.mkstemp(suffix=".mp3")
|
|
60
|
+
os.close(fd)
|
|
61
|
+
subprocess.run(
|
|
62
|
+
["ffmpeg", "-y",
|
|
63
|
+
"-ss", str(int(start_sec)),
|
|
64
|
+
"-t", str(int(duration)),
|
|
65
|
+
"-i", audio_path,
|
|
66
|
+
"-acodec", "libmp3lame", "-q:a", "3",
|
|
67
|
+
seg_path],
|
|
68
|
+
capture_output=True, timeout=30,
|
|
69
|
+
)
|
|
70
|
+
if os.path.getsize(seg_path) > 1024:
|
|
71
|
+
return seg_path
|
|
72
|
+
os.remove(seg_path)
|
|
73
|
+
return None
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _segment_positions(duration: float) -> list[float]:
|
|
79
|
+
"""Return evenly-spread start-second offsets to probe.
|
|
80
|
+
|
|
81
|
+
Rules:
|
|
82
|
+
<= 20 s -> [0] (short clip, try as-is)
|
|
83
|
+
<= 50 s -> [0, ~50%] (2 positions)
|
|
84
|
+
<= 90 s -> [0, ~33%, ~66%] (3 positions)
|
|
85
|
+
<= 180 s -> [0, ~25%, ~50%, ~75%] (4 positions)
|
|
86
|
+
> 180 s -> [0, ~20%, ~40%, ~60%, ~80%] (5 positions)
|
|
87
|
+
"""
|
|
88
|
+
if duration <= 20:
|
|
89
|
+
return [0.0]
|
|
90
|
+
if duration <= 50:
|
|
91
|
+
return [0.0, duration * 0.50]
|
|
92
|
+
if duration <= 90:
|
|
93
|
+
return [0.0, duration * 0.33, duration * 0.66]
|
|
94
|
+
if duration <= 180:
|
|
95
|
+
return [0.0, duration * 0.25, duration * 0.50, duration * 0.75]
|
|
96
|
+
return [0.0, duration * 0.20, duration * 0.40,
|
|
97
|
+
duration * 0.60, duration * 0.80]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
# Shazam core
|
|
102
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
async def _shazam_recognize_file(shazam, path: str) -> dict | None:
|
|
105
|
+
try:
|
|
106
|
+
result = await shazam.recognize(path)
|
|
107
|
+
if result and "track" in result:
|
|
108
|
+
return result
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _shazam_multi_segment(audio_path: str) -> dict | None:
|
|
115
|
+
"""Two-pass Shazam scan.
|
|
116
|
+
|
|
117
|
+
Pass 1 — Quick probe (12 s segments): fast fingerprint; catches most hits
|
|
118
|
+
Pass 2 — Deep scan (20 s segments): longer window catches harder tracks
|
|
119
|
+
Both passes share the same evenly-distributed position list.
|
|
120
|
+
"""
|
|
121
|
+
if not _HAS_SHAZAM:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
shazam = Shazam()
|
|
125
|
+
duration = _get_duration(audio_path)
|
|
126
|
+
positions = _segment_positions(duration)
|
|
127
|
+
total = len(positions)
|
|
128
|
+
|
|
129
|
+
# ── Pass 1: quick 12 s probe ─────────────────────────────────────────────
|
|
130
|
+
print(f" ⚡ [Pass 1 – quick probe, 12 s x {total} position{'s' if total > 1 else ''}]")
|
|
131
|
+
for i, start in enumerate(positions, start=1):
|
|
132
|
+
label = f"@{int(start)}s" if start > 0 else "start"
|
|
133
|
+
print(f" [Shazam] {i}/{total} {label}...", end=" ", flush=True)
|
|
134
|
+
|
|
135
|
+
if start == 0:
|
|
136
|
+
# Try the original file first (no re-encoding overhead)
|
|
137
|
+
result = await _shazam_recognize_file(shazam, audio_path)
|
|
138
|
+
else:
|
|
139
|
+
seg = _extract_segment(audio_path, start, duration=12)
|
|
140
|
+
if not seg:
|
|
141
|
+
print("(extract failed)")
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
result = await _shazam_recognize_file(shazam, seg)
|
|
145
|
+
finally:
|
|
146
|
+
try:
|
|
147
|
+
os.remove(seg)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
if result:
|
|
152
|
+
print("match!")
|
|
153
|
+
return result
|
|
154
|
+
print("no match")
|
|
155
|
+
|
|
156
|
+
# ── Pass 2: deep 20 s scan ───────────────────────────────────────────────
|
|
157
|
+
if total == 1:
|
|
158
|
+
# Audio is <= 20 s — pass 2 adds nothing new
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
print()
|
|
162
|
+
print(f" [Shazam] Pass 2 – deep scan, 20 s x {total} positions")
|
|
163
|
+
for i, start in enumerate(positions, start=1):
|
|
164
|
+
label = f"@{int(start)}s" if start > 0 else "start"
|
|
165
|
+
print(f" [Shazam] {i}/{total} {label} (20 s)...", end=" ", flush=True)
|
|
166
|
+
|
|
167
|
+
seg = _extract_segment(audio_path, start, duration=20)
|
|
168
|
+
if not seg:
|
|
169
|
+
print("(extract failed)")
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
result = await _shazam_recognize_file(shazam, seg)
|
|
173
|
+
finally:
|
|
174
|
+
try:
|
|
175
|
+
os.remove(seg)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
if result:
|
|
180
|
+
print("match!")
|
|
181
|
+
return result
|
|
182
|
+
print("no match")
|
|
183
|
+
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
# Result formatter
|
|
189
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def _format_shazam(result: dict) -> dict:
|
|
192
|
+
track = result["track"]
|
|
193
|
+
|
|
194
|
+
# ── Artist ────────────────────────────────────────────────────────────────
|
|
195
|
+
artist = track.get("subtitle", "").strip()
|
|
196
|
+
if not artist and track.get("artists"):
|
|
197
|
+
aliases = [a.get("alias", "").replace("-", " ").title()
|
|
198
|
+
for a in track["artists"] if a.get("alias")]
|
|
199
|
+
artist = ", ".join(aliases)
|
|
200
|
+
if not artist:
|
|
201
|
+
for section in track.get("sections", []):
|
|
202
|
+
if section.get("type") == "SONG":
|
|
203
|
+
for meta in section.get("metadata", []):
|
|
204
|
+
if meta.get("title", "").lower() in ("artist", "artists"):
|
|
205
|
+
artist = meta.get("text", "").strip()
|
|
206
|
+
if not artist:
|
|
207
|
+
artist = section.get("tabname", "").strip()
|
|
208
|
+
if not artist and "hub" in track:
|
|
209
|
+
hub_text = track["hub"].get("actions", [{}])[0].get("name", "")
|
|
210
|
+
if " - " in hub_text:
|
|
211
|
+
artist = hub_text.split(" - ")[0].strip()
|
|
212
|
+
|
|
213
|
+
# ── Metadata ──────────────────────────────────────────────────────────────
|
|
214
|
+
album = released = label = genre = ""
|
|
215
|
+
for section in track.get("sections", []):
|
|
216
|
+
if section.get("type") == "SONG":
|
|
217
|
+
for meta in section.get("metadata", []):
|
|
218
|
+
t, v = meta.get("title", "").lower(), meta.get("text", "")
|
|
219
|
+
if t == "album": album = v
|
|
220
|
+
elif t == "released": released = v
|
|
221
|
+
elif t == "label": label = v
|
|
222
|
+
if "genres" in track:
|
|
223
|
+
genre = track["genres"].get("primary", "")
|
|
224
|
+
|
|
225
|
+
# ── Links ─────────────────────────────────────────────────────────────────
|
|
226
|
+
spotify = ""
|
|
227
|
+
if "hub" in track:
|
|
228
|
+
for p in track["hub"].get("providers", []):
|
|
229
|
+
if p.get("type") == "SPOTIFY":
|
|
230
|
+
spotify = p["actions"][0].get("uri", "")
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"title": track.get("title", ""),
|
|
234
|
+
"artist": artist or "Unknown",
|
|
235
|
+
"album": album,
|
|
236
|
+
"released": released,
|
|
237
|
+
"label": label,
|
|
238
|
+
"genre": genre,
|
|
239
|
+
"shazam_count": track.get("shazamcount", 0),
|
|
240
|
+
"spotify": spotify,
|
|
241
|
+
"apple": track.get("url", ""),
|
|
242
|
+
"source": "Shazam",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
# Output printer
|
|
248
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
def _print_result(info: dict) -> None:
|
|
251
|
+
print()
|
|
252
|
+
print("=" * 70)
|
|
253
|
+
print(f"\u2705 MUSIC IDENTIFIED [{info['source']}]")
|
|
254
|
+
print("=" * 70)
|
|
255
|
+
print()
|
|
256
|
+
print(f"\U0001f3b5 Song: {info['title']}")
|
|
257
|
+
print(f"\U0001f464 Artist: {info['artist']}")
|
|
258
|
+
if info["album"]:
|
|
259
|
+
print(f"\U0001f4bf Album: {info['album']}")
|
|
260
|
+
if info["released"]:
|
|
261
|
+
print(f"\U0001f4c5 Released: {info['released']}")
|
|
262
|
+
if info["label"]:
|
|
263
|
+
print(f"\U0001f3f7\ufe0f Label: {info['label']}")
|
|
264
|
+
if info["genre"]:
|
|
265
|
+
print(f"\U0001f3b8 Genre: {info['genre']}")
|
|
266
|
+
if info["shazam_count"]:
|
|
267
|
+
c = info["shazam_count"]
|
|
268
|
+
fmt = (f"{c / 1_000_000:.1f}M" if c >= 1_000_000
|
|
269
|
+
else (f"{c / 1_000:.1f}K" if c >= 1_000 else str(c)))
|
|
270
|
+
print(f"\U0001f525 Shazams: {fmt}")
|
|
271
|
+
print()
|
|
272
|
+
print("\U0001f517 LINKS:")
|
|
273
|
+
if info["spotify"]:
|
|
274
|
+
print(f" Spotify: {info['spotify']}")
|
|
275
|
+
if info["apple"]:
|
|
276
|
+
print(f" Apple Music: {info['apple']}")
|
|
277
|
+
print()
|
|
278
|
+
print("=" * 70)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
# Public API
|
|
283
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
async def identify_music(audio_path: str) -> None:
|
|
286
|
+
"""Identify music from *audio_path* using optimized Shazam multi-segment."""
|
|
287
|
+
|
|
288
|
+
print("=" * 70)
|
|
289
|
+
print("MUSIC IDENTIFIER (Shazam – optimized multi-segment)")
|
|
290
|
+
print("=" * 70)
|
|
291
|
+
print()
|
|
292
|
+
|
|
293
|
+
path = Path(audio_path)
|
|
294
|
+
if not path.exists():
|
|
295
|
+
print(f"File not found: {audio_path}")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
valid_exts = {".mp3", ".wav", ".m4a", ".ogg", ".flac",
|
|
299
|
+
".aac", ".mp4", ".avi", ".mov"}
|
|
300
|
+
if path.suffix.lower() not in valid_exts:
|
|
301
|
+
print(f"Unsupported file type: {path.suffix}")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
if not _HAS_SHAZAM:
|
|
305
|
+
print("shazamio is not installed. Run: pip install shazamio")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
print(f"Analyzing: {path.name}")
|
|
309
|
+
print()
|
|
310
|
+
|
|
311
|
+
result = await _shazam_multi_segment(str(path))
|
|
312
|
+
if result:
|
|
313
|
+
_print_result(_format_shazam(result))
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
print()
|
|
317
|
+
print("No match found. The audio might be:")
|
|
318
|
+
print(" - Original / unreleased / user-created music")
|
|
319
|
+
print(" - Too short or poor audio quality")
|
|
320
|
+
print(" - Background / ambient sound without a clear melody")
|
|
321
|
+
print(" - In a niche regional catalogue not yet in Shazam's DB")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
325
|
+
# CLI entry point
|
|
326
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
def main() -> None:
|
|
329
|
+
if len(sys.argv) > 1:
|
|
330
|
+
audio_path = sys.argv[1].strip("\"'").strip()
|
|
331
|
+
else:
|
|
332
|
+
print("=" * 70)
|
|
333
|
+
print("MUSIC IDENTIFIER (Shazam – optimized multi-segment)")
|
|
334
|
+
print("=" * 70)
|
|
335
|
+
print()
|
|
336
|
+
audio_path = input("Enter audio/video file path: ").strip()
|
|
337
|
+
|
|
338
|
+
if not audio_path:
|
|
339
|
+
print("No path provided!")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
asyncio.run(identify_music(audio_path))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
main()
|