File size: 9,941 Bytes
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
32fe069
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32fe069
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32fe069
65eeb26
 
 
32fe069
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32fe069
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32fe069
 
 
 
 
 
 
 
 
 
65eeb26
 
 
 
 
 
 
 
 
 
 
32fe069
 
 
65eeb26
 
 
 
b36e841
 
 
 
65eeb26
 
 
32fe069
65eeb26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0375ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import shutil
from fastapi import FastAPI, HTTPException, Request, Body
from deezspot.deezloader import DeeLogin
from deezspot.spotloader import SpoLogin
import requests
import os
import logging
from typing import Optional
from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
from pydantic import BaseModel, Field, HttpUrl
from urllib.parse import quote
import uuid
import gc
from typing import Literal
import urllib.parse
from fastapi.responses import JSONResponse

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

app = FastAPI(title="Deezer API")
# Load environment variables
load_dotenv()

# Mount a static files directory to serve downloaded files
os.makedirs("downloads", exist_ok=True)
app.mount("/downloads", StaticFiles(directory="downloads"), name="downloads")

# Deezer API base URL
DEEZER_API_URL = "https://api.deezer.com"

# Deezer ARL token (required for deezspot downloads)
ARL_TOKEN = os.getenv('ARL')


class DownloadRequest(BaseModel):
    url: str
    quality: str
    arl: str


def convert_deezer_short_link_async(short_link: str) -> str:
    try:
        response = requests.get(short_link, allow_redirects=True)
        return response.url
    except requests.RequestException as e:
        print(f"An error occurred: {e}")
        return ""


@app.middleware("http")
async def log_errors_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception as e:
        logger.error("An unhandled exception occurred!", exc_info=True)
        return JSONResponse(status_code=500, content={'detail': 'Internal Server Error'})


@app.get("/")
def read_root():
    return {"message": "running"}


# Helper function to get track info
def get_track_info(track_id: str):
    try:
        response = requests.get(f"{DEEZER_API_URL}/track/{track_id}")
        if response.status_code != 200:
            raise HTTPException(status_code=404, detail="Track not found")
        return response.json()
    except requests.exceptions.RequestException as e:
        logger.error(f"Network error fetching track metadata: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    except Exception as e:
        logger.error(f"Error fetching track metadata: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# Fetch track metadata from Deezer API
@app.get("/track/{track_id}")
def get_track(track_id: str):
    return get_track_info(track_id)


# Download a track and return a download URL (NO RATE LIMITS)
@app.post("/download/track")
def download_track(request: Request, download_request: DownloadRequest):
    try:
        # Use provided ARL or fallback to env
        if download_request.arl is None or download_request.arl.strip() == "":
            ARL = ARL_TOKEN
        else:
            ARL = download_request.arl
        logger.info(f'arl: {ARL}')
        url = download_request.url
        if 'dzr.page' in url or 'deezer.page' in url or 'link.deezer' in url:
            url = convert_deezer_short_link_async(url)
        quality = download_request.quality
        dl = DeeLogin(arl=ARL)
        logger.info(f'track_url: {url}')

        if quality not in ["MP3_320", "MP3_128", "FLAC"]:
            raise HTTPException(status_code=400, detail="Invalid quality specified")

        # Extract track_id
        track_id = url.split("/")[-1]

        # Fetch track info
        track_info = get_track_info(track_id)
        track_link = track_info.get("link")
        if not track_link:
            raise HTTPException(status_code=404, detail="Track link not found")

        # Sanitize filename
        track_title = track_info.get("title", "track")
        artist_name = track_info.get("artist", {}).get("name", "unknown")
        file_extension = "flac" if quality == "FLAC" else "mp3"

        # Clear the downloads directory
        for root, dirs, files in os.walk("downloads"):
            for file in files:
                os.remove(os.path.join(root, file))
            for dir in dirs:
                shutil.rmtree(os.path.join(root, dir))

        # Download the track
        dl.download_trackdee(
            link_track=track_link,
            output_dir="downloads",
            quality_download=quality,
            recursive_quality=False,
            recursive_download=False
        )

        # Find the file
        filepath = None
        for root, dirs, files in os.walk("downloads"):
            for file in files:
                if file.endswith(f'.{file_extension}'):
                    filepath = os.path.join(root, file)
                    break
            if filepath:
                break

        if not filepath:
            raise HTTPException(status_code=500, detail=f"{file_extension} file not found after download")

        file_size = os.path.getsize(filepath)
        logger.info(f"Downloaded file size: {file_size} bytes")

        # Return the download URL
        relative_path = quote(str(os.path.relpath(filepath, "downloads")))
        base_url = str(request.base_url).rstrip('/')

        if base_url.startswith("http://"):
            base_url = base_url.replace("http://", "https://", 1)
               
        download_url = f"{base_url}/downloads/{relative_path}"
        logger.info(f"Download successful: {download_url}")
        gc.collect()
        return {"download_url": download_url}
    except Exception as e:
        logger.error(f"Error downloading track: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# Pydantic model for album request
class AlbumRequest(BaseModel):
    id: str


# Fetch album data
@app.post("/z_album")
def fetch_album(request: AlbumRequest):
    album_id = request.id
    try:
        response = requests.get(f"{DEEZER_API_URL}/album/{album_id}")
        response.raise_for_status()
        album_data = response.json()
        tracks = album_data.get("tracks", {}).get("data", [])
        result = []
        for track in tracks:
            title = track.get("title")
            link = track.get("link")
            if title and link:
                result.append({
                    "title": title,
                    "link": link
                })
        return result
    except requests.exceptions.RequestException as e:
        logger.error(f"Network error fetching album: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    except Exception as e:
        logger.error(f"Error fetching album: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# Pydantic model for playlist request
class PlaylistRequest(BaseModel):
    id: str


# Fetch playlist data
@app.post("/z_playlist")
def fetch_playlist(request: PlaylistRequest):
    playlist_id = request.id
    try:
        response = requests.get(f"{DEEZER_API_URL}/playlist/{playlist_id}")
        response.raise_for_status()
        playlist_data = response.json()
        tracks = playlist_data.get("tracks", {}).get("data", [])
        result = []
        for track in tracks:
            title = track.get("title")
            link = track.get("link")
            if title and link:
                result.append({
                    "title": title,
                    "link": link
                })
        return result
    except requests.exceptions.RequestException as e:
        logger.error(f"Network error fetching album: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    except Exception as e:
        logger.error(f"Error fetching album: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# Search tracks using Deezer API
@app.get("/z_search")
def search_tracks(query: str, limit: Optional[int] = 10):
    try:
        response = requests.get(f"{DEEZER_API_URL}/search", params={"q": query, "limit": limit})
        return response.json()
    except requests.exceptions.RequestException as e:
        logger.error(f"Network error searching tracks: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    except Exception as e:
        logger.error(f"Error searching tracks: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# --- Request Body Model ---
class SpotDlRequest(BaseModel):
    url: HttpUrl = Field(..., description="The URL to be processed.")
    quality: Literal["128", "320", "FLAC"] = Field(
        ...,
        description="The desired quality. Currently, only '128' is supported for link generation."
    )

# --- Response Body Model ---
class SpotDlResponse(BaseModel):
    download_url: str

# --- Error Response Model ---
class ErrorResponse(BaseModel):
    detail: str

# --- API Endpoint ---
@app.post(
    "/spot_dl",
    response_model=SpotDlResponse,
    responses={
        400: {"model": ErrorResponse, "description": "Bad Request - Invalid input"},
        422: {"model": ErrorResponse, "description": "Validation Error - Input data is not valid"}
    },
    summary="Generate SpotDL Link",
    description="Accepts a URL and quality, returns a processed URL if quality is '128', "
                "otherwise returns an error for higher qualities."
)
async def create_spot_dl_link(request: SpotDlRequest = Body(...)):
    print(f"Received request: url='{request.url}', quality='{request.quality}'")

    if request.quality == "128":
        encoded_url = urllib.parse.quote(str(request.url), safe='')
        output_url = f"https://velynapi.vercel.app/api/downloader/spotifydl?url={encoded_url}"
        return SpotDlResponse(download_url=output_url)
    elif request.quality == "320" or request.quality == "FLAC":
        raise HTTPException(
            status_code=400,
            detail=f"Quality '{request.quality}' is for Premium Users Only. '128' is allowed."
        )
    else:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid quality value: '{request.quality}'. Allowed values are '128', '320', 'FLAC'."
        )