v4.6
This commit is contained in:
+83
-3
@@ -356,7 +356,7 @@ class TidalStatusChecker(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
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
|
is_online = response.status_code == 200 or response.status_code == 429
|
||||||
self.status_updated.emit(is_online)
|
self.status_updated.emit(is_online)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -640,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
|
|||||||
class SpotiFLACGUI(QWidget):
|
class SpotiFLACGUI(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_version = "4.5"
|
self.current_version = "4.6"
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.all_tracks = []
|
self.all_tracks = []
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
@@ -1596,6 +1596,10 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.handle_album_metadata(metadata)
|
self.handle_album_metadata(metadata)
|
||||||
elif url_info["type"] == "playlist":
|
elif url_info["type"] == "playlist":
|
||||||
self.handle_playlist_metadata(metadata)
|
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.update_button_states()
|
||||||
self.tab_widget.setCurrentIndex(0)
|
self.tab_widget.setCurrentIndex(0)
|
||||||
@@ -1695,9 +1699,57 @@ class SpotiFLACGUI(QWidget):
|
|||||||
'artists': playlist_data["playlist_info"]["owner"]["display_name"],
|
'artists': playlist_data["playlist_info"]["owner"]["display_name"],
|
||||||
'cover': playlist_data["playlist_info"]["owner"]["images"],
|
'cover': playlist_data["playlist_info"]["owner"]["images"],
|
||||||
'followers': playlist_data["playlist_info"]["followers"]["total"],
|
'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)
|
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):
|
def update_display_after_fetch(self, metadata):
|
||||||
self.track_list.setVisible(not self.is_single_track)
|
self.track_list.setVisible(not self.is_single_track)
|
||||||
|
|
||||||
@@ -1753,12 +1805,40 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.type_label.setText(f"<b>Album</b> • {total_tracks} tracks")
|
self.type_label.setText(f"<b>Album</b> • {total_tracks} tracks")
|
||||||
elif self.is_playlist:
|
elif self.is_playlist:
|
||||||
total_tracks = metadata.get('total_tracks', 0)
|
total_tracks = metadata.get('total_tracks', 0)
|
||||||
|
if metadata.get('discography_type'):
|
||||||
|
discography_type = metadata['discography_type'].title()
|
||||||
|
self.type_label.setText(f"<b>Discography ({discography_type})</b> • {total_tracks} tracks")
|
||||||
|
else:
|
||||||
self.type_label.setText(f"<b>Playlist</b> • {total_tracks} tracks")
|
self.type_label.setText(f"<b>Playlist</b> • {total_tracks} tracks")
|
||||||
|
|
||||||
self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
|
self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
|
||||||
|
|
||||||
self.info_widget.show()
|
self.info_widget.show()
|
||||||
|
|
||||||
|
def update_info_widget_artist_only(self, metadata):
|
||||||
|
self.title_label.setText(metadata['title'])
|
||||||
|
self.artists_label.setText(f"<b>Followers</b> {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"<b>Genres</b> {genres_text}")
|
||||||
|
self.followers_label.show()
|
||||||
|
else:
|
||||||
|
self.followers_label.hide()
|
||||||
|
|
||||||
|
self.release_date_label.hide()
|
||||||
|
self.type_label.setText("<b>Artist Profile</b> • 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):
|
def reset_info_widget(self):
|
||||||
self.title_label.clear()
|
self.title_label.clear()
|
||||||
self.artists_label.clear()
|
self.artists_label.clear()
|
||||||
|
|||||||
+218
-1
@@ -57,6 +57,8 @@ token_url = 'https://open.spotify.com/api/token'
|
|||||||
playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
|
playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
|
||||||
album_base_url = 'https://api.spotify.com/v1/albums/{}'
|
album_base_url = 'https://api.spotify.com/v1/albums/{}'
|
||||||
track_base_url = 'https://api.spotify.com/v1/tracks/{}'
|
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 = {
|
headers = {
|
||||||
'User-Agent': get_random_user_agent(),
|
'User-Agent': get_random_user_agent(),
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -101,10 +103,18 @@ def parse_uri(uri):
|
|||||||
parts = parts[1:]
|
parts = parts[1:]
|
||||||
|
|
||||||
l = len(parts)
|
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]}
|
return {"type": parts[1], "id": parts[2]}
|
||||||
if l == 5 and parts[3] == "playlist":
|
if l == 5 and parts[3] == "playlist":
|
||||||
return {"type": parts[3], "id": parts[4]}
|
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.")
|
raise SpotifyInvalidUrlException("ERROR: unable to determine Spotify URL type or type is unsupported.")
|
||||||
|
|
||||||
@@ -340,6 +350,69 @@ def get_raw_spotify_data(spotify_url, batch: bool = False, delay: float = 1.0):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Failed to get track data: {str(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
|
return raw_data
|
||||||
|
|
||||||
def format_track_data(track_data):
|
def format_track_data(track_data):
|
||||||
@@ -469,6 +542,134 @@ def format_playlist_data(playlist_data):
|
|||||||
"track_list": track_list
|
"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):
|
def process_spotify_data(raw_data, data_type):
|
||||||
if not raw_data or "error" in raw_data:
|
if not raw_data or "error" in raw_data:
|
||||||
return {"error": "Invalid data provided"}
|
return {"error": "Invalid data provided"}
|
||||||
@@ -480,6 +681,10 @@ def process_spotify_data(raw_data, data_type):
|
|||||||
return format_album_data(raw_data)
|
return format_album_data(raw_data)
|
||||||
elif data_type == "playlist":
|
elif data_type == "playlist":
|
||||||
return format_playlist_data(raw_data)
|
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:
|
else:
|
||||||
return {"error": "Invalid data type"}
|
return {"error": "Invalid data type"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -498,11 +703,23 @@ if __name__ == '__main__':
|
|||||||
album = "https://open.spotify.com/album/6J84szYCnMfzEcvIcfWMFL"
|
album = "https://open.spotify.com/album/6J84szYCnMfzEcvIcfWMFL"
|
||||||
song = "https://open.spotify.com/track/7so0lgd0zP2Sbgs2d7a1SZ"
|
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)
|
filtered_playlist = get_filtered_data(playlist, batch=True, delay=0.1)
|
||||||
print(json.dumps(filtered_playlist, indent=2))
|
print(json.dumps(filtered_playlist, indent=2))
|
||||||
|
|
||||||
|
print("\n=== Testing Album ===")
|
||||||
filtered_album = get_filtered_data(album)
|
filtered_album = get_filtered_data(album)
|
||||||
print(json.dumps(filtered_album, indent=2))
|
print(json.dumps(filtered_album, indent=2))
|
||||||
|
|
||||||
|
print("\n=== Testing Track ===")
|
||||||
filtered_track = get_filtered_data(song)
|
filtered_track = get_filtered_data(song)
|
||||||
print(json.dumps(filtered_track, indent=2))
|
print(json.dumps(filtered_track, indent=2))
|
||||||
+1
-1
@@ -144,7 +144,7 @@ class TidalDownloader:
|
|||||||
|
|
||||||
def get_download_url(self, track_id, quality="LOSSLESS"):
|
def get_download_url(self, track_id, quality="LOSSLESS"):
|
||||||
print("Fetching URL...")
|
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:
|
try:
|
||||||
response = requests.get(download_api_url, timeout=self.timeout)
|
response = requests.get(download_api_url, timeout=self.timeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user