tecuts commited on
Commit
006c7ab
·
verified ·
1 Parent(s): d842ae1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +136 -39
app.py CHANGED
@@ -2,6 +2,8 @@ import os
2
  import logging
3
  import sys
4
  from datetime import datetime
 
 
5
  import tidalapi
6
  from tidalapi import Track, Quality
7
  from fastapi import FastAPI, HTTPException, Query
@@ -9,6 +11,7 @@ from fastapi.responses import JSONResponse
9
 
10
  app = FastAPI()
11
 
 
12
  logger = logging.getLogger("tidalapi_app")
13
  logger.setLevel(logging.INFO)
14
  handler = logging.StreamHandler(sys.stdout)
@@ -16,6 +19,7 @@ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
16
  handler.setFormatter(formatter)
17
  logger.addHandler(handler)
18
 
 
19
  def load_tokens_from_secret(session):
20
  secret = os.getenv("TIDAL_OAUTH_TOKENS")
21
  if not secret:
@@ -34,7 +38,6 @@ def load_tokens_from_secret(session):
34
  logger.info("OAuth tokens loaded successfully from secret")
35
  return True
36
 
37
- # Initialize session globally
38
  session = tidalapi.Session()
39
 
40
  if not load_tokens_from_secret(session):
@@ -43,7 +46,7 @@ if not load_tokens_from_secret(session):
43
  if not session.check_login():
44
  raise RuntimeError("Failed to login with saved tokens")
45
 
46
- # Map client quality strings to tidalapi Quality enums
47
  QUALITY_MAP = {
48
  "LOW": Quality.low_96k,
49
  "HIGH": Quality.low_320k,
@@ -51,6 +54,55 @@ QUALITY_MAP = {
51
  "HI_RES": Quality.hi_res_lossless,
52
  }
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  @app.get("/")
55
  async def root():
56
  logger.info("GET /")
@@ -59,55 +111,100 @@ async def root():
59
  "author": "made by Cody from chrunos.com"
60
  }
61
 
62
- @app.get("/track/")
63
- def get_track_download_url(
64
- id: int = Query(..., description="TIDAL Track ID"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  quality: str = Query("HI_RES", description="Audio quality", pattern="^(LOW|HIGH|LOSSLESS|HI_RES)$"),
 
 
66
  ):
67
- logger.info(f"Request received for track_id: {id} with quality: {quality}")
 
 
 
68
 
69
  if quality not in QUALITY_MAP:
70
- logger.error(f"Invalid quality requested: {quality}")
71
  raise HTTPException(status_code=400, detail=f"Invalid quality. Available: {list(QUALITY_MAP.keys())}")
72
 
73
  session.audio_quality = QUALITY_MAP[quality]
74
 
75
  try:
76
- track = Track(session, id)
77
- logger.info(f"Track found: {track.name} by {track.artist.name}")
 
 
78
  except Exception as e:
79
- logger.error(f"Track lookup failed: {e}")
80
- raise HTTPException(status_code=404, detail="Track not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- stream = track.get_stream()
83
- manifest = stream.get_stream_manifest()
84
-
85
- result = {
86
- "track_id": id,
87
- "track_name": track.name,
88
- "artist_name": track.artist.name,
89
- "audio_quality": str(stream.audio_quality),
90
- "stream_type": None,
91
- "download_urls": [],
92
- }
93
 
94
- if stream.is_bts:
95
- urls = manifest.get_urls()
96
- if not urls:
97
- logger.error("No direct URLs found in manifest")
98
- raise HTTPException(status_code=500, detail="No downloadable URLs found")
99
- result["stream_type"] = "bts"
100
- result["download_urls"] = urls
101
- logger.info(f"Returning {len(urls)} direct download URL(s)")
102
 
103
- elif stream.is_mpd:
104
- mpd_manifest = stream.get_manifest_data()
105
- result["stream_type"] = "mpd"
106
- result["mpd_manifest"] = mpd_manifest
107
- logger.info("Returning MPEG-DASH (MPD) manifest data")
108
 
109
- else:
110
- logger.error("Unsupported stream type")
111
- raise HTTPException(status_code=500, detail="Unsupported stream type")
 
112
 
113
- return JSONResponse(content=result)
 
2
  import logging
3
  import sys
4
  from datetime import datetime
5
+ from typing import List, Optional
6
+
7
  import tidalapi
8
  from tidalapi import Track, Quality
9
  from fastapi import FastAPI, HTTPException, Query
 
11
 
12
  app = FastAPI()
13
 
14
+ # --- Logging Setup ---
15
  logger = logging.getLogger("tidalapi_app")
16
  logger.setLevel(logging.INFO)
17
  handler = logging.StreamHandler(sys.stdout)
 
19
  handler.setFormatter(formatter)
20
  logger.addHandler(handler)
21
 
22
+ # --- Authentication ---
23
  def load_tokens_from_secret(session):
24
  secret = os.getenv("TIDAL_OAUTH_TOKENS")
25
  if not secret:
 
38
  logger.info("OAuth tokens loaded successfully from secret")
39
  return True
40
 
 
41
  session = tidalapi.Session()
42
 
43
  if not load_tokens_from_secret(session):
 
46
  if not session.check_login():
47
  raise RuntimeError("Failed to login with saved tokens")
48
 
49
+ # --- Constants ---
50
  QUALITY_MAP = {
51
  "LOW": Quality.low_96k,
52
  "HIGH": Quality.low_320k,
 
54
  "HI_RES": Quality.hi_res_lossless,
55
  }
56
 
57
+ # --- Helper Functions ---
58
+ def _extract_stream_info(track_id: int):
59
+ """
60
+ Internal helper to fetch stream info for a specific track ID.
61
+ Returns a dictionary or raises an exception/returns None on failure.
62
+ """
63
+ try:
64
+ track = Track(session, track_id)
65
+ # Verify track exists/can be fetched
66
+ _ = track.name
67
+ except Exception as e:
68
+ logger.error(f"Track lookup failed for ID {track_id}: {e}")
69
+ return None
70
+
71
+ try:
72
+ stream = track.get_stream()
73
+ manifest = stream.get_stream_manifest()
74
+ except Exception as e:
75
+ logger.error(f"Stream fetching failed for {track.name} ({track_id}): {e}")
76
+ return None
77
+
78
+ result = {
79
+ "track_id": track_id,
80
+ "track_name": track.name,
81
+ "artist_name": track.artist.name,
82
+ "album_name": track.album.name if track.album else "Unknown",
83
+ "audio_quality": str(stream.audio_quality),
84
+ "stream_type": None,
85
+ "download_urls": [],
86
+ "mpd_manifest": None
87
+ }
88
+
89
+ if stream.is_bts:
90
+ urls = manifest.get_urls()
91
+ if urls:
92
+ result["stream_type"] = "bts"
93
+ result["download_urls"] = urls
94
+ elif stream.is_mpd:
95
+ mpd_manifest = stream.get_manifest_data()
96
+ result["stream_type"] = "mpd"
97
+ result["mpd_manifest"] = mpd_manifest
98
+ else:
99
+ logger.warning(f"Unsupported stream type for track {track_id}")
100
+ return None
101
+
102
+ return result
103
+
104
+ # --- Endpoints ---
105
+
106
  @app.get("/")
107
  async def root():
108
  logger.info("GET /")
 
111
  "author": "made by Cody from chrunos.com"
112
  }
113
 
114
+ @app.get("/search")
115
+ def search_tracks(
116
+ query: str = Query(..., description="Search query (e.g., song title)"),
117
+ limit: int = Query(10, description="Max results to return", ge=1, le=50),
118
+ offset: int = Query(0, description="Pagination offset", ge=0)
119
+ ):
120
+ """
121
+ Search for tracks on Tidal.
122
+ """
123
+ logger.info(f"Searching for: {query} (limit={limit}, offset={offset})")
124
+
125
+ try:
126
+ # models=[Track] ensures we only get tracks back
127
+ search_results = session.search(query, models=[Track], limit=limit, offset=offset)
128
+ tracks = search_results.get('tracks', [])
129
+
130
+ results = []
131
+ for t in tracks:
132
+ results.append({
133
+ "id": t.id,
134
+ "title": t.name,
135
+ "artist": t.artist.name,
136
+ "album": t.album.name if t.album else None,
137
+ "duration": t.duration,
138
+ "explicit": t.explicit
139
+ })
140
+
141
+ return JSONResponse(content={"query": query, "count": len(results), "results": results})
142
+
143
+ except Exception as e:
144
+ logger.error(f"Search failed: {e}")
145
+ raise HTTPException(status_code=500, detail=str(e))
146
+
147
+ @app.get("/playlist")
148
+ def get_playlist(
149
+ id: str = Query(..., description="Tidal Playlist UUID"),
150
  quality: str = Query("HI_RES", description="Audio quality", pattern="^(LOW|HIGH|LOSSLESS|HI_RES)$"),
151
+ limit: int = Query(50, description="Max tracks to process (prevent timeouts)", ge=1, le=100),
152
+ offset: int = Query(0, description="Track offset", ge=0)
153
  ):
154
+ """
155
+ Fetch a playlist and return download URLs for its tracks.
156
+ """
157
+ logger.info(f"Request received for playlist: {id} with quality: {quality}")
158
 
159
  if quality not in QUALITY_MAP:
 
160
  raise HTTPException(status_code=400, detail=f"Invalid quality. Available: {list(QUALITY_MAP.keys())}")
161
 
162
  session.audio_quality = QUALITY_MAP[quality]
163
 
164
  try:
165
+ playlist = session.playlist(id)
166
+ # Fetch tracks from the playlist
167
+ tracks = playlist.tracks(limit=limit, offset=offset)
168
+ logger.info(f"Found {len(tracks)} tracks in playlist {playlist.name}")
169
  except Exception as e:
170
+ logger.error(f"Playlist lookup failed: {e}")
171
+ raise HTTPException(status_code=404, detail="Playlist not found or API error")
172
+
173
+ processed_tracks = []
174
+ failed_tracks = []
175
+
176
+ for t in tracks:
177
+ # Reuse the helper to get stream info
178
+ stream_info = _extract_stream_info(t.id)
179
+ if stream_info:
180
+ processed_tracks.append(stream_info)
181
+ else:
182
+ failed_tracks.append({"id": t.id, "name": t.name})
183
+
184
+ return JSONResponse(content={
185
+ "playlist_id": id,
186
+ "playlist_name": playlist.name,
187
+ "processed_count": len(processed_tracks),
188
+ "failed_count": len(failed_tracks),
189
+ "tracks": processed_tracks,
190
+ "failed": failed_tracks
191
+ })
192
 
193
+ @app.get("/track/")
194
+ def get_track_download_url(
195
+ id: int = Query(..., description="TIDAL Track ID"),
196
+ quality: str = Query("HI_RES", description="Audio quality", pattern="^(LOW|HIGH|LOSSLESS|HI_RES)$"),
197
+ ):
198
+ logger.info(f"Request received for track_id: {id} with quality: {quality}")
 
 
 
 
 
199
 
200
+ if quality not in QUALITY_MAP:
201
+ raise HTTPException(status_code=400, detail=f"Invalid quality. Available: {list(QUALITY_MAP.keys())}")
 
 
 
 
 
 
202
 
203
+ session.audio_quality = QUALITY_MAP[quality]
 
 
 
 
204
 
205
+ result = _extract_stream_info(id)
206
+
207
+ if not result:
208
+ raise HTTPException(status_code=404, detail="Track not found or no stream available")
209
 
210
+ return JSONResponse(content=result)