File size: 15,838 Bytes
7e58923
 
 
4bc7387
510a8f2
4bc7387
 
63e3048
6146a19
ae3ede2
58138cb
e1c913c
 
63c1006
 
4bc7387
7e58923
 
6a45bf5
 
 
 
5ce8d0d
7e58923
 
 
 
 
 
 
 
 
 
 
f94628b
ded5ac7
 
0f09a72
f94628b
6b74354
f94628b
 
c07416b
0f09a72
408a4d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f09a72
 
408a4d2
0f09a72
408a4d2
 
 
 
 
 
 
0f09a72
 
 
408a4d2
 
 
0f09a72
408a4d2
0f09a72
408a4d2
 
 
 
 
 
0f09a72
408a4d2
 
0f09a72
 
 
408a4d2
 
 
 
 
0f09a72
408a4d2
0f09a72
 
e843d84
408a4d2
 
 
 
 
 
 
 
 
 
ad3ab65
408a4d2
 
 
 
0f09a72
408a4d2
0f09a72
 
 
408a4d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f09a72
 
408a4d2
 
 
 
 
 
 
 
 
 
 
0f09a72
408a4d2
 
 
 
 
 
 
7191a0b
408a4d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179ee8b
408a4d2
 
 
 
0f09a72
dbc7bc9
 
c07416b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d69267d
8a6f020
2e809a6
8a6f020
c07416b
 
 
 
 
e1c913c
c07416b
6146a19
 
c07416b
 
 
 
 
 
4bc7387
 
ddfd898
6146a19
 
 
e1c913c
6146a19
 
2dbf28a
 
 
c07416b
 
 
 
 
 
 
 
 
 
 
 
 
 
4bc7387
 
5ce8d0d
c07416b
 
4bc7387
f19d74e
 
a51e30e
f19d74e
 
c07416b
fecd6d1
3aefb0f
63c1006
3aefb0f
63c1006
 
3aefb0f
63c1006
 
fecd6d1
3aefb0f
63c1006
3aefb0f
 
e1c913c
 
e6bda95
5ce8d0d
44f1011
e1c913c
 
 
3aefb0f
e1c913c
 
 
 
 
 
44f1011
e1c913c
 
fecd6d1
f19d74e
e1c913c
 
44f1011
e1c913c
 
44f1011
e1c913c
 
 
 
 
44f1011
e1c913c
 
 
 
 
44f1011
e1c913c
c07416b
5ce8d0d
4bc7387
 
7e58923
 
 
 
 
 
 
 
 
 
 
 
74078ba
 
 
 
 
 
7e58923
c07416b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
from flask import Flask, render_template, jsonify, request
from ytmusicapi import YTMusic
import os
import logging
import requests
from datetime import datetime, timedelta
import time
import asyncio
import cloudscraper
from pydantic import BaseModel
from urllib.parse import urlparse, parse_qs
from collections import defaultdict
import threading
import aiohttp


app = Flask(__name__)
ytmusic = YTMusic()


# Configure logging 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/search', methods=['POST'])
def search():
    query = request.json.get('query', '')
    search_results = ytmusic.search(query, filter="songs")
    return jsonify(search_results)

@app.route('/searcht', methods=['POST'])
def searcht(): 
    query = request.json.get('query', '')
    logger.info(f"serch query: {query}")
    search_results = ytmusic.search(query, filter="songs")
    first_song = next((song for song in search_results if 'videoId' in song and song['videoId']), {}) if search_results else {}
    return jsonify(first_song)


def extract_amazon_track_id(url: str):
    """
    Extracts track ID from various Amazon Music URL formats.
    """
    if "music.amazon.com" not in url: # MODIFIED: Slight logic inversion for early exit compared to original, same effective outcome.
        return None

    parsed_url = urlparse(url)
    query_params = parse_qs(parsed_url.query)

    if "trackAsin" in query_params:
        return query_params["trackAsin"][0]

    path_parts = parsed_url.path.split('/') # MODIFIED: Changed from simple `url.split` to more robust path parsing for Case 2.
    if "tracks" in path_parts:
        try:
            track_id_index = path_parts.index("tracks") + 1
            if track_id_index < len(path_parts):
                return path_parts[track_id_index] # MODIFIED: Accessing specific part after "tracks".
        except (ValueError, IndexError):
            pass

    logger.warning(f"Could not extract Amazon track ID from URL: {url}") # ADDED: Logging for when no ID is found.
    return None


def get_song_link_info(url: str):
    """
    Fetches track information from the Song.link API.
    Uses requests.get() which is a blocking call.
    """
    api_base_url = "https://api.song.link/v1-alpha.1/links" # ADDED: Defined base URL for clarity.
    params = {"userCountry": "US"} # MODIFIED: Using a params dictionary for requests.get().

    if "music.amazon.com" in url:
        track_id = extract_amazon_track_id(url)
        if track_id:
            params["platform"] = "amazonMusic" # MODIFIED: Populating params dict.
            params["id"] = track_id
            params["type"] = "song"
        else:
            params["url"] = url # MODIFIED: Populating params dict.
    else:
        params["url"] = url # MODIFIED: Populating params dict.

    try: # ADDED: try-except block for robust error handling during API call.
        logger.info(f"Querying Song.link API with params: {params}") # ADDED: Logging the API query.
        response = requests.get(api_base_url, params=params, timeout=10) # MODIFIED: Call uses base_url and params. ADDED: timeout.
        response.raise_for_status() # ADDED: Checks for HTTP errors (4xx or 5xx responses).
        return response.json()
    except requests.exceptions.RequestException as e: # ADDED: Catching network/request related exceptions.
        logger.error(f"Error fetching from Song.link API: {e}") # ADDED: Logging the specific error.
        return None

def extract_url(links_by_platform: dict, platform: str):
    """
    Extracts a specific platform URL from Song.link API response.
    """
    # MODIFIED: Added .get("url") for safer access to prevent KeyError if "url" key is missing.
    if platform in links_by_platform and links_by_platform[platform].get("url"):
        return links_by_platform[platform]["url"]
    logger.warning(f"No URL found for platform '{platform}' in links: {links_by_platform.keys()}") # ADDED: Logging if platform URL not found.
    return None

@app.route('/match', methods=['POST'])
def match(): # MODIFIED: Changed from `async def` to `def` for synchronous Flask.
    """
    Matches a given music track URL to a YouTube Music URL.
    Expects a JSON body with "url".
    """
    data = request.get_json()
    if not data: # ADDED: Check for empty JSON payload.
        logger.error("Match endpoint: No JSON payload received.") # ADDED: Logging.
        return jsonify({"detail": "No JSON payload received."}), 400 # MODIFIED: Flask-style JSON error response.

    track_url = data.get('url')
    # MODIFIED: Added more specific validation for track_url presence and type.
    if not track_url or not isinstance(track_url, str):
        logger.error(f"Match endpoint: Invalid or missing URL: {track_url}") # ADDED: Logging.
        return jsonify({"detail": "Valid 'url' string is required in request body."}), 400 # MODIFIED: Flask-style JSON error response.

    logger.info(f"Match endpoint: Processing URL: {track_url}") # ADDED: Logging.

    track_info = get_song_link_info(track_url)
    if not track_info:
        logger.error(f"Match endpoint: Could not fetch track info for URL: {track_url}") # ADDED: Logging.
        # MODIFIED: Flask-style JSON error response instead of HTTPException.
        return jsonify({"detail": "Could not fetch track info from Song.link API."}), 404

    entity_unique_id = track_info.get("entityUniqueId") # MODIFIED: Used .get() for safer access.
    title = None
    artist = None

    # MODIFIED: More robust extraction of title and artist with checks and logging.
    if entity_unique_id and entity_unique_id in track_info.get("entitiesByUniqueId", {}):
        main_entity = track_info["entitiesByUniqueId"][entity_unique_id]
        title = main_entity.get("title")
        artist = main_entity.get("artistName")
        logger.info(f"Match endpoint: Found main entity - Title: '{title}', Artist: '{artist}'") # ADDED: Logging.
    else:
        logger.warning(f"Match endpoint: Could not find main entity details for {track_url} using entityUniqueId: {entity_unique_id}") # ADDED: Logging.
        # ADDED: Fallback logic to find title/artist from other entities if main one fails.
        for entity_id, entity_data in track_info.get("entitiesByUniqueId", {}).items():
            if entity_data.get("title") and entity_data.get("artistName"):
                title = entity_data.get("title")
                artist = entity_data.get("artistName")
                logger.info(f"Match endpoint: Using fallback entity - Title: '{title}', Artist: '{artist}' from entity ID {entity_id}") # ADDED: Logging.
                break
        if not title or not artist: # ADDED: Check if title/artist still not found after fallback.
             logger.error(f"Match endpoint: Could not determine title and artist for URL: {track_url}") # ADDED: Logging.
             return jsonify({"detail": "Could not determine title and artist from Song.link info."}), 404 # MODIFIED: Flask-style JSON error.


    youtube_url = extract_url(track_info.get("linksByPlatform", {}), "youtube") # MODIFIED: Used .get() for safer access.

    if youtube_url:
        video_id = None
        # MODIFIED: Improved video_id extraction from youtube_url, handles direct watch links and youtu.be, and strips extra params.
        if "v=" in youtube_url:
            video_id = youtube_url.split("v=")[1].split("&")[0]
        elif "youtu.be/" in youtube_url: # MODIFIED: Handling for youtu.be links if present in song.link
            video_id = youtube_url.split("youtu.be/")[1].split("?")[0]

        filename = f"{title} - {artist}" if title and artist else "Unknown Track - Unknown Artist"
        logger.info(f"Match endpoint: Found direct YouTube URL: {youtube_url}, Video ID: {video_id}") # ADDED: Logging.
        # MODIFIED: Flask-style JSON response instead of returning dict directly.
        return jsonify({"url": youtube_url, "filename": filename, "track_id": video_id}), 200
    else:
        logger.info(f"Match endpoint: No direct YouTube URL. Searching YTMusic with: '{title} - {artist}'") # ADDED: Logging.
        # ADDED: Explicit check if title or artist is missing before searching.
        if not title or not artist:
            logger.error("Match endpoint: Cannot search YTMusic without title and artist.") # ADDED: Logging.
            return jsonify({"detail": "Cannot search on YouTube Music without title and artist information."}), 400 # MODIFIED: Flask-style JSON error.

        search_query = f'{title} {artist}' # MODIFIED: Changed from '+' to space for a more natural search query.
        search_results = ytmusic.search(search_query, filter="songs")

        if search_results:
            # MODIFIED: Improved logic to pick the first song with a videoId using next() and .get().
            first_song = next((song for song in search_results if song.get('videoId')), None)
            if first_song and first_song.get('videoId'):
                video_id = first_song["videoId"]
                # MODIFIED: Changed ym_url to a standard YouTube watch URL format.
                ym_url = f'https://music.youtube.com/watch?v={video_id}'
                # MODIFIED: More robust filename generation using .get() and providing fallbacks.
                filename = f"{first_song.get('title', title)} - {first_song.get('artists', [{'name': artist}])[0]['name']}"
                logger.info(f"Match endpoint: Found YTMusic search result - URL: {ym_url}, Video ID: {video_id}") # ADDED: Logging.
                # MODIFIED: Flask-style JSON response.
                return jsonify({"filename": filename, "url": ym_url, "track_id": video_id}), 200
            else:
                logger.error(f"Match endpoint: YTMusic search for '{search_query}' yielded no results with a videoId.") # ADDED: Logging.
                # MODIFIED: Flask-style JSON error response.
                return jsonify({"detail": "No matching video ID found on YouTube Music after search."}), 404
        else:
            logger.error(f"Match endpoint: YTMusic search for '{search_query}' yielded no results.") # ADDED: Logging.
            # MODIFIED: Flask-style JSON error response.
            return jsonify({"detail": "No results found on YouTube Music for the track."}), 404
    # REMOVED: The final `raise HTTPException` was determined to be unreachable and removed.

    

class ApiRotator:
    def __init__(self, apis):
        self.apis = apis
        self.last_successful_index = None

    def get_prioritized_apis(self):
        if self.last_successful_index is not None:
            # Move the last successful API to the front
            rotated_apis = (
                [self.apis[self.last_successful_index]] + 
                self.apis[:self.last_successful_index] + 
                self.apis[self.last_successful_index+1:]
            )
            return rotated_apis
        return self.apis

    def update_last_successful(self, index):
        self.last_successful_index = index

# In your function:
api_rotator = ApiRotator([
    "https://cobalt-api.ayo.tf",
    "https://dwnld.nichind.dev",
    "https://cobalt-api.kwiatekmiki.com",
    "http://34.107.254.11"      
    
])



async def get_track_download_url(track_id: str, quality: str) -> str:
    apis = api_rotator.get_prioritized_apis()
    session = cloudscraper.create_scraper()  # Requires cloudscraper package
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    }

    for i, api_url in enumerate(apis):
        try:
            logger.info(f"Attempting to get download URL from: {api_url}")
            y_url = f"https://youtu.be/{track_id}"
            response = session.post(
                api_url,
                timeout=20,
                json={"url": y_url, "audioFormat": "mp3", "downloadMode": "audio", "audioBitrate": quality},
                headers=headers
            )
            logger.info(f"Response status: {response.status_code}")
            logger.info(f"Response content: {response.content}")

            if response.headers.get('content-type', '').startswith('application/json'):
                json_response = response.json()
                error_code = json_response.get("error", {}).get("code", "")
                
                if error_code == "error.api.content.video.unavailable":
                    logger.warning(f"Video unavailable error from {api_url}")
                    break  # Only break for specific error
                
                if "url" in json_response:
                    api_rotator.update_last_successful(i)
                    return json_response["url"]
                
        except Exception as e:
            logger.error(f"Failed with {api_url}: {str(e)}")
            continue
    
    logger.error(f"No download URL found")
    return {"error": "Download URL not found"}


async def get_audio_download_url(track_id: str, quality: str) -> str:
    youtube_url = f'https://www.youtube.com/watch?v={track_id}'
    donwnload_url = f'https://chrunos-grab.hf.space/yt/dl?url={youtube_url}&type=audio'
    return donwnload_url
    
    
async def get_download_url(track_id):
    base_url = "https://velynapi.vercel.app/api/downloader/ytmp3"
    youtube_url = f'https://www.youtube.com/watch?v={track_id}'
    
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get(base_url, params={'url': youtube_url}) as response:
                if response.status == 200:
                    result = await response.json()
                    return result.get('output')
                logger.info(f"Request failed, status: {response.status}")
        except aiohttp.ClientError as e:
            logger.info(f"Client error: {e}")
        return None


@app.route('/track_dl', methods=['POST'])
async def track_dl():
   
    data = request.get_json()
    track_id = data.get('track_id')
    quality = data.get('quality', '128')
    logger.info(f'id: {track_id}, quality: {quality}')
    
    try:
        quality_num = int(quality)
        if quality_num > 128 or quality.upper() == 'FLAC':
            return jsonify({
                "error": "Quality above 128 or FLAC is for Premium users Only.",
                "premium": "https://chrunos.com/premium-shortcuts/"
            }), 400
            
        dl_url = await get_download_url(track_id)
        logger.info(dl_url)
        
        if dl_url and "http" in dl_url:
                       
            result = {
                "url": dl_url,
                "premium": "https://chrunos.com/premium-shortcuts/"
            }
            return jsonify(result)
        else:
            return jsonify({
                "error": "Failed to Fetch the Track.",
                "premium": "https://chrunos.com/premium-shortcuts/"
            }), 400
            
    except ValueError:
        return jsonify({
            "error": "Invalid quality value provided. It should be a valid integer or FLAC.",
            "premium": "https://chrunos.com/premium-shortcuts/"
        }), 400
    



@app.route('/get_artist', methods=['GET'])
def get_artist():
    artist_id = request.args.get('id')
    artist_info = ytmusic.get_artist(artist_id)
    return jsonify(artist_info)

@app.route('/get_album', methods=['GET'])
def get_album():
    album_id = request.args.get('id')
    album_info = ytmusic.get_album(album_id)
    return jsonify(album_info)

@app.route('/get_song', methods=['GET'])
def get_song():
    song_id = request.args.get('id')
    song_info = ytmusic.get_song(song_id)
    return jsonify(song_info)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=7860)