diff --git a/SpotiFLAC.py b/SpotiFLAC.py
index 5c4bbb2..e60b1b8 100644
--- a/SpotiFLAC.py
+++ b/SpotiFLAC.py
@@ -356,7 +356,7 @@ class TidalStatusChecker(QThread):
def run(self):
try:
- response = requests.get("https://hifi.401658.xyz", timeout=5)
+ response = requests.get("https://tidal.401658.xyz", timeout=5)
is_online = response.status_code == 200 or response.status_code == 429
self.status_updated.emit(is_online)
except Exception as e:
@@ -640,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget):
def __init__(self):
super().__init__()
- self.current_version = "4.5"
+ self.current_version = "4.6"
self.tracks = []
self.all_tracks = []
self.reset_state()
@@ -1596,6 +1596,10 @@ class SpotiFLACGUI(QWidget):
self.handle_album_metadata(metadata)
elif url_info["type"] == "playlist":
self.handle_playlist_metadata(metadata)
+ elif url_info["type"] == "artist_discography":
+ self.handle_discography_metadata(metadata)
+ elif url_info["type"] == "artist":
+ self.handle_artist_metadata(metadata)
self.update_button_states()
self.tab_widget.setCurrentIndex(0)
@@ -1695,9 +1699,57 @@ class SpotiFLACGUI(QWidget):
'artists': playlist_data["playlist_info"]["owner"]["display_name"],
'cover': playlist_data["playlist_info"]["owner"]["images"],
'followers': playlist_data["playlist_info"]["followers"]["total"],
- 'total_tracks': playlist_data["playlist_info"]["tracks"]["total"] }
+ 'total_tracks': playlist_data["playlist_info"]["tracks"]["total"]
+ }
self.update_display_after_fetch(metadata)
+ def handle_discography_metadata(self, discography_data):
+ artist_info = discography_data["artist_info"]
+ self.album_or_playlist_name = f"{artist_info['name']} - Discography ({artist_info['discography_type'].title()})"
+ self.tracks = []
+
+ for track in discography_data["track_list"]:
+ track_id = track["external_urls"].split("/")[-1] if track.get("external_urls") else ""
+
+ self.tracks.append(Track(
+ external_urls=track.get("external_urls", ""),
+ title=track["name"],
+ artists=track["artists"],
+ album=track["album_name"],
+ track_number=track.get("track_number", len(self.tracks) + 1),
+ duration_ms=track.get("duration_ms", 0),
+ id=track_id,
+ isrc=track.get("isrc", ""),
+ release_date=track.get("release_date", "")
+ ))
+
+ self.all_tracks = self.tracks.copy()
+ self.is_playlist = True
+ self.is_album = self.is_single_track = False
+
+ metadata = {
+ 'title': f"{artist_info['name']} - Discography",
+ 'artists': f"{artist_info['discography_type'].title()} • {artist_info['total_albums']} albums",
+ 'cover': artist_info["images"],
+ 'followers': artist_info.get("followers", 0),
+ 'total_tracks': len(self.tracks),
+ 'discography_type': artist_info['discography_type']
+ }
+ self.update_display_after_fetch(metadata)
+
+ def handle_artist_metadata(self, artist_data):
+ self.reset_state()
+
+ metadata = {
+ 'title': artist_data["artist"]["name"],
+ 'artists': f"Followers: {artist_data['artist']['followers']:,}",
+ 'cover': artist_data["artist"]["images"],
+ 'followers': artist_data["artist"]["followers"],
+ 'genres': artist_data["artist"].get("genres", [])
+ }
+
+ self.update_info_widget_artist_only(metadata)
+
def update_display_after_fetch(self, metadata):
self.track_list.setVisible(not self.is_single_track)
@@ -1753,12 +1805,40 @@ class SpotiFLACGUI(QWidget):
self.type_label.setText(f"Album • {total_tracks} tracks")
elif self.is_playlist:
total_tracks = metadata.get('total_tracks', 0)
- self.type_label.setText(f"Playlist • {total_tracks} tracks")
+ if metadata.get('discography_type'):
+ discography_type = metadata['discography_type'].title()
+ self.type_label.setText(f"Discography ({discography_type}) • {total_tracks} tracks")
+ else:
+ self.type_label.setText(f"Playlist • {total_tracks} tracks")
self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
self.info_widget.show()
+ def update_info_widget_artist_only(self, metadata):
+ self.title_label.setText(metadata['title'])
+ self.artists_label.setText(f"Followers {metadata['followers']:,}")
+
+ if metadata.get('genres'):
+ genres_text = ", ".join(metadata['genres'][:3])
+ if len(metadata['genres']) > 3:
+ genres_text += f" (+{len(metadata['genres']) - 3} more)"
+ self.followers_label.setText(f"Genres {genres_text}")
+ self.followers_label.show()
+ else:
+ self.followers_label.hide()
+
+ self.release_date_label.hide()
+ self.type_label.setText("Artist Profile • No tracks available for download")
+
+ self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
+
+ self.track_list.hide()
+ self.search_widget.hide()
+ self.hide_track_buttons()
+
+ self.info_widget.show()
+
def reset_info_widget(self):
self.title_label.clear()
self.artists_label.clear()
diff --git a/getMetadata.py b/getMetadata.py
index 3c6efa9..3e44d5c 100644
--- a/getMetadata.py
+++ b/getMetadata.py
@@ -57,6 +57,8 @@ token_url = 'https://open.spotify.com/api/token'
playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
album_base_url = 'https://api.spotify.com/v1/albums/{}'
track_base_url = 'https://api.spotify.com/v1/tracks/{}'
+artist_base_url = 'https://api.spotify.com/v1/artists/{}'
+artist_albums_url = 'https://api.spotify.com/v1/artists/{}/albums'
headers = {
'User-Agent': get_random_user_agent(),
'Accept': 'application/json',
@@ -101,10 +103,18 @@ def parse_uri(uri):
parts = parts[1:]
l = len(parts)
- if l == 3 and parts[1] in ["album", "track", "playlist"]:
+ if l == 3 and parts[1] in ["album", "track", "playlist", "artist"]:
return {"type": parts[1], "id": parts[2]}
if l == 5 and parts[3] == "playlist":
return {"type": parts[3], "id": parts[4]}
+ if l >= 4 and parts[1] == "artist" and len(parts) >= 4:
+ if parts[3] == "discography":
+ discography_type = "all"
+ if len(parts) >= 5 and parts[4] in ["all", "album", "single", "compilation"]:
+ discography_type = parts[4]
+ return {"type": "artist_discography", "id": parts[2], "discography_type": discography_type}
+ else:
+ return {"type": "artist", "id": parts[2]}
raise SpotifyInvalidUrlException("ERROR: unable to determine Spotify URL type or type is unsupported.")
@@ -339,6 +349,69 @@ def get_raw_spotify_data(spotify_url, batch: bool = False, delay: float = 1.0):
raw_data = track_data
except Exception as e:
return {"error": f"Failed to get track data: {str(e)}"}
+
+ elif url_info["type"] == "artist_discography":
+ try:
+ artist_data = get_json_from_api(
+ artist_base_url.format(url_info["id"]),
+ access_token
+ )
+ if not artist_data:
+ return {"error": "Failed to get artist data"}
+
+ discography_type = url_info.get("discography_type", "all")
+ if discography_type == "all":
+ include_groups = "album,single,compilation"
+ else:
+ include_groups = discography_type
+
+ albums = []
+ albums_url = f'{artist_albums_url.format(url_info["id"])}?include_groups={include_groups}&limit=50'
+
+ if batch:
+ albums, num_batches = fetch_tracks_in_batches(albums_url, access_token, 50, delay)
+ raw_data = {
+ "artist_info": artist_data,
+ "albums": albums,
+ "discography_type": discography_type,
+ "_batch_count": num_batches,
+ "_batch_enabled": True
+ }
+ else:
+ while albums_url:
+ album_data = get_json_from_api(albums_url, access_token)
+ if not album_data:
+ break
+
+ albums.extend(album_data['items'])
+ albums_url = album_data.get('next')
+ if albums_url and "&locale=" in albums_url:
+ albums_url = albums_url.split("&locale=")[0]
+
+ raw_data = {
+ "artist_info": artist_data,
+ "albums": albums,
+ "discography_type": discography_type,
+ "_batch_enabled": False
+ }
+
+ raw_data['_token'] = access_token
+
+ except Exception as e:
+ return {"error": f"Failed to get artist discography data: {str(e)}"}
+
+ elif url_info["type"] == "artist":
+ try:
+ artist_data = get_json_from_api(
+ artist_base_url.format(url_info["id"]),
+ access_token
+ )
+ if not artist_data:
+ return {"error": "Failed to get artist data"}
+
+ raw_data = artist_data
+ except Exception as e:
+ return {"error": f"Failed to get artist data: {str(e)}"}
return raw_data
@@ -469,6 +542,134 @@ def format_playlist_data(playlist_data):
"track_list": track_list
}
+def format_artist_discography_data(discography_data):
+ artist_info = discography_data.get('artist_info', {})
+ albums = discography_data.get('albums', [])
+ access_token = discography_data.get('_token', '')
+
+ artist_image = ''
+ if artist_info.get('images'):
+ artist_image = artist_info.get('images', [{}])[0].get('url', '')
+
+ formatted_artist_info = {
+ "name": artist_info.get('name', ''),
+ "followers": artist_info.get('followers', {}).get('total', 0),
+ "genres": artist_info.get('genres', []),
+ "images": artist_image,
+ "external_urls": artist_info.get('external_urls', {}).get('spotify', ''),
+ "discography_type": discography_data.get('discography_type', 'all'),
+ "total_albums": len(albums)
+ }
+
+ if discography_data.get('_batch_enabled', False):
+ formatted_artist_info["batch"] = f"{discography_data.get('_batch_count', 1)}"
+
+ album_list = []
+ all_tracks = []
+
+ for album in albums:
+ album_image = ''
+ if album.get('images'):
+ album_image = album.get('images', [{}])[0].get('url', '')
+
+ album_artists = []
+ for artist in album.get('artists', []):
+ album_artists.append(artist['name'])
+
+ album_info = {
+ "id": album.get('id', ''),
+ "name": album.get('name', ''),
+ "album_type": album.get('album_type', ''),
+ "release_date": album.get('release_date', ''),
+ "total_tracks": album.get('total_tracks', 0),
+ "artists": ", ".join(album_artists),
+ "images": album_image,
+ "external_urls": album.get('external_urls', {}).get('spotify', '')
+ }
+
+ album_list.append(album_info)
+
+ if access_token and album.get('id'):
+ try:
+ album_tracks_data = get_json_from_api(
+ f'{album_base_url.format(album.get("id"))}/tracks?limit=50',
+ access_token
+ )
+
+ if album_tracks_data:
+ tracks = []
+ tracks_url = f'{album_base_url.format(album.get("id"))}/tracks?limit=50'
+
+ while tracks_url:
+ track_data = get_json_from_api(tracks_url, access_token)
+ if not track_data:
+ break
+
+ tracks.extend(track_data['items'])
+ tracks_url = track_data.get('next')
+ if tracks_url and "&locale=" in tracks_url:
+ tracks_url = tracks_url.split("&locale=")[0]
+
+ for track in tracks:
+ track_artists = []
+ for artist in track.get('artists', []):
+ track_artists.append(artist['name'])
+
+ track_id = track.get('id', '')
+ track_isrc = ''
+
+ if track_id:
+ try:
+ full_track_data = get_json_from_api(
+ track_base_url.format(track_id),
+ access_token
+ )
+ if full_track_data:
+ track_isrc = full_track_data.get('external_ids', {}).get('isrc', '')
+ except:
+ pass
+
+ formatted_track = {
+ "artists": ", ".join(track_artists),
+ "name": track.get('name', ''),
+ "album_name": album.get('name', ''),
+ "album_type": album.get('album_type', ''),
+ "duration_ms": track.get('duration_ms', 0),
+ "images": album_image,
+ "release_date": album.get('release_date', ''),
+ "track_number": track.get('track_number', 0),
+ "external_urls": track.get('external_urls', {}).get('spotify', ''),
+ "isrc": track_isrc
+ }
+
+ all_tracks.append(formatted_track)
+
+ except Exception as e:
+ print(f"Error getting tracks for album {album.get('name', '')}: {str(e)}")
+ continue
+
+ return {
+ "artist_info": formatted_artist_info,
+ "album_list": album_list,
+ "track_list": all_tracks
+ }
+
+def format_artist_data(artist_data):
+ artist_image = ''
+ if artist_data.get('images'):
+ artist_image = artist_data.get('images', [{}])[0].get('url', '')
+
+ return {
+ "artist": {
+ "name": artist_data.get('name', ''),
+ "followers": artist_data.get('followers', {}).get('total', 0),
+ "genres": artist_data.get('genres', []),
+ "images": artist_image,
+ "external_urls": artist_data.get('external_urls', {}).get('spotify', ''),
+ "popularity": artist_data.get('popularity', 0)
+ }
+ }
+
def process_spotify_data(raw_data, data_type):
if not raw_data or "error" in raw_data:
return {"error": "Invalid data provided"}
@@ -480,6 +681,10 @@ def process_spotify_data(raw_data, data_type):
return format_album_data(raw_data)
elif data_type == "playlist":
return format_playlist_data(raw_data)
+ elif data_type == "artist_discography":
+ return format_artist_discography_data(raw_data)
+ elif data_type == "artist":
+ return format_artist_data(raw_data)
else:
return {"error": "Invalid data type"}
except Exception as e:
@@ -498,11 +703,23 @@ if __name__ == '__main__':
album = "https://open.spotify.com/album/6J84szYCnMfzEcvIcfWMFL"
song = "https://open.spotify.com/track/7so0lgd0zP2Sbgs2d7a1SZ"
+ artist_discography_all = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/all"
+ artist_discography_albums = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/album"
+ artist_discography_singles = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/single"
+ artist_discography_compilations = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/compilation"
+
+ print("=== Testing Artist Discography (All) ===")
+ filtered_discography = get_filtered_data(artist_discography_all, batch=True, delay=0.1)
+ print(json.dumps(filtered_discography, indent=2))
+
+ print("\n=== Testing Playlist ===")
filtered_playlist = get_filtered_data(playlist, batch=True, delay=0.1)
print(json.dumps(filtered_playlist, indent=2))
+ print("\n=== Testing Album ===")
filtered_album = get_filtered_data(album)
print(json.dumps(filtered_album, indent=2))
+ print("\n=== Testing Track ===")
filtered_track = get_filtered_data(song)
print(json.dumps(filtered_track, indent=2))
\ No newline at end of file
diff --git a/tidalDL.py b/tidalDL.py
index 0094001..d0772e5 100644
--- a/tidalDL.py
+++ b/tidalDL.py
@@ -144,7 +144,7 @@ class TidalDownloader:
def get_download_url(self, track_id, quality="LOSSLESS"):
print("Fetching URL...")
- download_api_url = f"https://hifi.401658.xyz/track/?id={track_id}&quality={quality}"
+ download_api_url = f"https://tidal.401658.xyz/track/?id={track_id}&quality={quality}"
try:
response = requests.get(download_api_url, timeout=self.timeout)