Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e7a48d263 | |||
| 0a83a0dd6e | |||
| da429d9410 | |||
| 63211c726b | |||
| 055cb6991a | |||
| 222d681551 | |||
| 479c6ede2b | |||
| ceb727adb9 | |||
| bbea8ca493 | |||
| f567dd19bf |
@@ -1,12 +1,12 @@
|
|||||||
[](https://github.com/afkarxyz/SpotiFLAC/releases)
|
[](https://github.com/afkarxyz/SpotiFLAC/releases)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Deezer with the help of Lucida.
|
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Deezer with the help of Lucida.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v2.2/SpotiFLAC.exe)
|
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v2.5/SpotiFLAC.exe)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -15,13 +15,17 @@ Sometimes, the **download speed** from Lucida can be fast or slow; it varies unp
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
> When **Fallback** is enabled, it will use the backup server `Lucida.su`
|
> When **Fallback** is enabled, it will use the backup server `Lucida.su`
|
||||||
|
|
||||||
|
|||||||
+125
-35
@@ -29,13 +29,33 @@ class Track:
|
|||||||
duration_ms: int
|
duration_ms: int
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
class MetadataFetchWorker(QThread):
|
||||||
|
finished = pyqtSignal(dict)
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
super().__init__()
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
metadata = get_filtered_data(self.url)
|
||||||
|
if "error" in metadata:
|
||||||
|
self.error.emit(metadata["error"])
|
||||||
|
else:
|
||||||
|
self.finished.emit(metadata)
|
||||||
|
except SpotifyInvalidUrlException as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(f'Failed to fetch metadata: {str(e)}')
|
||||||
|
|
||||||
class DownloadWorker(QThread):
|
class DownloadWorker(QThread):
|
||||||
finished = pyqtSignal(bool, str, list)
|
finished = pyqtSignal(bool, str, list)
|
||||||
progress = pyqtSignal(str, int)
|
progress = pyqtSignal(str, int)
|
||||||
|
|
||||||
def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False,
|
def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False,
|
||||||
album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True,
|
album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True,
|
||||||
use_album_subfolders=False, use_fallback=False, service="amazon"):
|
use_album_subfolders=False, use_fallback=False, service="amazon", timeout=30):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.tracks = tracks
|
self.tracks = tracks
|
||||||
self.outpath = outpath
|
self.outpath = outpath
|
||||||
@@ -48,6 +68,7 @@ class DownloadWorker(QThread):
|
|||||||
self.use_album_subfolders = use_album_subfolders
|
self.use_album_subfolders = use_album_subfolders
|
||||||
self.use_fallback = use_fallback
|
self.use_fallback = use_fallback
|
||||||
self.service = service
|
self.service = service
|
||||||
|
self.timeout = timeout
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.is_stopped = False
|
self.is_stopped = False
|
||||||
self.failed_tracks = []
|
self.failed_tracks = []
|
||||||
@@ -61,18 +82,19 @@ class DownloadWorker(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
downloader = TrackDownloader(self.use_fallback)
|
downloader = TrackDownloader(self.use_fallback, self.timeout)
|
||||||
|
|
||||||
def progress_update(current, total):
|
def progress_update(current, total):
|
||||||
if total > 0:
|
if total > 0:
|
||||||
percent = (current / total) * 100
|
percent = (current / total) * 100
|
||||||
self.progress.emit(f"Download progress: {percent:.2f}% ({current}/{total})",
|
current_mb = current / (1024 * 1024)
|
||||||
|
total_mb = total / (1024 * 1024)
|
||||||
|
self.progress.emit(f"Download progress: {percent:.2f}% ({current_mb:.2f}MB/{total_mb:.2f}MB)",
|
||||||
int(percent))
|
int(percent))
|
||||||
else:
|
else:
|
||||||
self.progress.emit(f"Processing metadata...", 0)
|
self.progress.emit(f"Processing metadata...", 0)
|
||||||
|
|
||||||
downloader.set_progress_callback(progress_update)
|
downloader.set_progress_callback(progress_update)
|
||||||
downloader.set_filename_format(self.filename_format)
|
|
||||||
|
|
||||||
total_tracks = len(self.tracks)
|
total_tracks = len(self.tracks)
|
||||||
|
|
||||||
@@ -102,7 +124,16 @@ class DownloadWorker(QThread):
|
|||||||
metadata = asyncio.run(downloader.get_track_info(track_id, self.service))
|
metadata = asyncio.run(downloader.get_track_info(track_id, self.service))
|
||||||
|
|
||||||
self.progress.emit(f"Track info received, starting download process", 0)
|
self.progress.emit(f"Track info received, starting download process", 0)
|
||||||
downloaded_file = downloader.download(metadata, track_outpath)
|
|
||||||
|
is_paused_callback = lambda: self.is_paused
|
||||||
|
is_stopped_callback = lambda: self.is_stopped
|
||||||
|
|
||||||
|
downloaded_file = downloader.download(
|
||||||
|
metadata,
|
||||||
|
track_outpath,
|
||||||
|
is_paused_callback=is_paused_callback,
|
||||||
|
is_stopped_callback=is_stopped_callback
|
||||||
|
)
|
||||||
|
|
||||||
if (self.is_album or (self.is_playlist and self.use_album_subfolders)) and self.use_track_numbers:
|
if (self.is_album or (self.is_playlist and self.use_album_subfolders)) and self.use_track_numbers:
|
||||||
new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}"
|
new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}"
|
||||||
@@ -300,7 +331,7 @@ class ServiceComboBox(QComboBox):
|
|||||||
class SpotiFLACGUI(QWidget):
|
class SpotiFLACGUI(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_version = "2.2"
|
self.current_version = "2.6"
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
|
|
||||||
@@ -313,6 +344,7 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
|
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
|
||||||
self.use_fallback = self.settings.value('use_fallback', False, type=bool)
|
self.use_fallback = self.settings.value('use_fallback', False, type=bool)
|
||||||
self.service = self.settings.value('service', 'amazon')
|
self.service = self.settings.value('service', 'amazon')
|
||||||
|
self.timeout_value = self.settings.value('timeout_value', 30, type=int)
|
||||||
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
|
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
|
||||||
|
|
||||||
self.elapsed_time = QTime(0, 0, 0)
|
self.elapsed_time = QTime(0, 0, 0)
|
||||||
@@ -556,7 +588,7 @@ class SpotiFLACGUI(QWidget):
|
|||||||
output_layout.setSpacing(5)
|
output_layout.setSpacing(5)
|
||||||
|
|
||||||
output_label = QLabel('Output Directory')
|
output_label = QLabel('Output Directory')
|
||||||
output_label.setStyleSheet("font-weight: bold; color: palette(text);")
|
output_label.setStyleSheet("font-weight: bold;")
|
||||||
output_layout.addWidget(output_label)
|
output_layout.addWidget(output_label)
|
||||||
|
|
||||||
output_dir_layout = QHBoxLayout()
|
output_dir_layout = QHBoxLayout()
|
||||||
@@ -579,21 +611,18 @@ class SpotiFLACGUI(QWidget):
|
|||||||
file_layout.setSpacing(5)
|
file_layout.setSpacing(5)
|
||||||
|
|
||||||
file_label = QLabel('File Settings')
|
file_label = QLabel('File Settings')
|
||||||
file_label.setStyleSheet("font-weight: bold; color: palette(text);")
|
file_label.setStyleSheet("font-weight: bold;")
|
||||||
file_layout.addWidget(file_label)
|
file_layout.addWidget(file_label)
|
||||||
|
|
||||||
format_layout = QHBoxLayout()
|
format_layout = QHBoxLayout()
|
||||||
format_label = QLabel('Filename Format:')
|
format_label = QLabel('Filename Format:')
|
||||||
format_label.setStyleSheet("color: palette(text);")
|
|
||||||
|
|
||||||
self.format_group = QButtonGroup(self)
|
self.format_group = QButtonGroup(self)
|
||||||
self.title_artist_radio = QRadioButton('Title - Artist')
|
self.title_artist_radio = QRadioButton('Title - Artist')
|
||||||
self.title_artist_radio.setStyleSheet("color: palette(text);")
|
|
||||||
self.title_artist_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.title_artist_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.title_artist_radio.toggled.connect(self.save_filename_format)
|
self.title_artist_radio.toggled.connect(self.save_filename_format)
|
||||||
|
|
||||||
self.artist_title_radio = QRadioButton('Artist - Title')
|
self.artist_title_radio = QRadioButton('Artist - Title')
|
||||||
self.artist_title_radio.setStyleSheet("color: palette(text);")
|
|
||||||
self.artist_title_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.artist_title_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.artist_title_radio.toggled.connect(self.save_filename_format)
|
self.artist_title_radio.toggled.connect(self.save_filename_format)
|
||||||
|
|
||||||
@@ -614,14 +643,12 @@ class SpotiFLACGUI(QWidget):
|
|||||||
checkbox_layout = QHBoxLayout()
|
checkbox_layout = QHBoxLayout()
|
||||||
|
|
||||||
self.track_number_checkbox = QCheckBox('Add Track Numbers to Album Files')
|
self.track_number_checkbox = QCheckBox('Add Track Numbers to Album Files')
|
||||||
self.track_number_checkbox.setStyleSheet("color: palette(text);")
|
|
||||||
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.track_number_checkbox.setChecked(self.use_track_numbers)
|
self.track_number_checkbox.setChecked(self.use_track_numbers)
|
||||||
self.track_number_checkbox.toggled.connect(self.save_track_numbering)
|
self.track_number_checkbox.toggled.connect(self.save_track_numbering)
|
||||||
checkbox_layout.addWidget(self.track_number_checkbox)
|
checkbox_layout.addWidget(self.track_number_checkbox)
|
||||||
|
|
||||||
self.album_subfolder_checkbox = QCheckBox('Create Album Subfolders for Playlist Downloads')
|
self.album_subfolder_checkbox = QCheckBox('Create Album Subfolders for Playlist Downloads')
|
||||||
self.album_subfolder_checkbox.setStyleSheet("color: palette(text);")
|
|
||||||
self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.album_subfolder_checkbox.setChecked(self.use_album_subfolders)
|
self.album_subfolder_checkbox.setChecked(self.use_album_subfolders)
|
||||||
self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting)
|
self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting)
|
||||||
@@ -636,14 +663,13 @@ class SpotiFLACGUI(QWidget):
|
|||||||
auth_layout = QVBoxLayout(auth_group)
|
auth_layout = QVBoxLayout(auth_group)
|
||||||
auth_layout.setSpacing(5)
|
auth_layout.setSpacing(5)
|
||||||
|
|
||||||
auth_label = QLabel('Lucida')
|
auth_label = QLabel('Lucida Settings')
|
||||||
auth_label.setStyleSheet("font-weight: bold; color: palette(text);")
|
auth_label.setStyleSheet("font-weight: bold;")
|
||||||
auth_layout.addWidget(auth_label)
|
auth_layout.addWidget(auth_label)
|
||||||
|
|
||||||
service_fallback_layout = QHBoxLayout()
|
service_fallback_layout = QHBoxLayout()
|
||||||
|
|
||||||
service_label = QLabel('Service:')
|
service_label = QLabel('Service:')
|
||||||
service_label.setStyleSheet("color: palette(text);")
|
|
||||||
|
|
||||||
self.service_dropdown = ServiceComboBox()
|
self.service_dropdown = ServiceComboBox()
|
||||||
self.service_dropdown.currentIndexChanged.connect(self.save_service_setting)
|
self.service_dropdown.currentIndexChanged.connect(self.save_service_setting)
|
||||||
@@ -654,12 +680,21 @@ class SpotiFLACGUI(QWidget):
|
|||||||
service_fallback_layout.addSpacing(20)
|
service_fallback_layout.addSpacing(20)
|
||||||
|
|
||||||
self.fallback_checkbox = QCheckBox('Fallback')
|
self.fallback_checkbox = QCheckBox('Fallback')
|
||||||
self.fallback_checkbox.setStyleSheet("color: palette(text);")
|
|
||||||
self.fallback_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.fallback_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.fallback_checkbox.setChecked(self.use_fallback)
|
self.fallback_checkbox.setChecked(self.use_fallback)
|
||||||
self.fallback_checkbox.toggled.connect(self.save_fallback_setting)
|
self.fallback_checkbox.toggled.connect(self.save_fallback_setting)
|
||||||
service_fallback_layout.addWidget(self.fallback_checkbox)
|
service_fallback_layout.addWidget(self.fallback_checkbox)
|
||||||
|
|
||||||
|
service_fallback_layout.addSpacing(20)
|
||||||
|
|
||||||
|
timeout_label = QLabel('Timeout:')
|
||||||
|
self.timeout_input = QLineEdit()
|
||||||
|
self.timeout_input.setText(str(self.timeout_value))
|
||||||
|
self.timeout_input.setFixedWidth(60)
|
||||||
|
self.timeout_input.textChanged.connect(self.save_timeout_setting)
|
||||||
|
service_fallback_layout.addWidget(timeout_label)
|
||||||
|
service_fallback_layout.addWidget(self.timeout_input)
|
||||||
|
|
||||||
service_fallback_layout.addStretch()
|
service_fallback_layout.addStretch()
|
||||||
auth_layout.addLayout(service_fallback_layout)
|
auth_layout.addLayout(service_fallback_layout)
|
||||||
|
|
||||||
@@ -719,8 +754,8 @@ class SpotiFLACGUI(QWidget):
|
|||||||
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||||
about_layout.addItem(spacer)
|
about_layout.addItem(spacer)
|
||||||
|
|
||||||
footer_label = QLabel("v2.2 | March 2025")
|
footer_label = QLabel("v2.6 | May 2025")
|
||||||
footer_label.setStyleSheet("font-size: 12px; color: palette(text); margin-top: 10px;")
|
footer_label.setStyleSheet("font-size: 12px; margin-top: 10px;")
|
||||||
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
about_tab.setLayout(about_layout)
|
about_tab.setLayout(about_layout)
|
||||||
@@ -751,6 +786,21 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.log_output.append("Fallback setting saved successfully!")
|
self.log_output.append("Fallback setting saved successfully!")
|
||||||
|
|
||||||
|
def save_timeout_setting(self):
|
||||||
|
try:
|
||||||
|
timeout = int(self.timeout_input.text())
|
||||||
|
if timeout > 0:
|
||||||
|
self.timeout_value = timeout
|
||||||
|
self.settings.setValue('timeout_value', self.timeout_value)
|
||||||
|
self.settings.sync()
|
||||||
|
self.log_output.append(f"Timeout setting saved: {self.timeout_value} seconds")
|
||||||
|
else:
|
||||||
|
self.timeout_input.setText(str(self.timeout_value))
|
||||||
|
self.log_output.append("Timeout must be a positive number")
|
||||||
|
except ValueError:
|
||||||
|
self.timeout_input.setText(str(self.timeout_value))
|
||||||
|
self.log_output.append("Timeout must be a valid number")
|
||||||
|
|
||||||
def save_service_setting(self):
|
def save_service_setting(self):
|
||||||
service = self.service_dropdown.currentData()
|
service = self.service_dropdown.currentData()
|
||||||
self.service = service
|
self.service = service
|
||||||
@@ -778,11 +828,20 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.reset_state()
|
self.reset_state()
|
||||||
self.reset_ui()
|
self.reset_ui()
|
||||||
|
|
||||||
metadata = get_filtered_data(url)
|
self.log_output.append('Just a moment. Fetching metadata...')
|
||||||
if "error" in metadata:
|
self.tab_widget.setCurrentWidget(self.process_tab)
|
||||||
raise Exception(metadata["error"])
|
|
||||||
|
self.metadata_worker = MetadataFetchWorker(url)
|
||||||
url_info = parse_uri(url)
|
self.metadata_worker.finished.connect(self.on_metadata_fetched)
|
||||||
|
self.metadata_worker.error.connect(self.on_metadata_error)
|
||||||
|
self.metadata_worker.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_output.append(f'Error: Failed to start metadata fetch: {str(e)}')
|
||||||
|
|
||||||
|
def on_metadata_fetched(self, metadata):
|
||||||
|
try:
|
||||||
|
url_info = parse_uri(self.spotify_url.text().strip())
|
||||||
|
|
||||||
if url_info["type"] == "track":
|
if url_info["type"] == "track":
|
||||||
self.handle_track_metadata(metadata["track"])
|
self.handle_track_metadata(metadata["track"])
|
||||||
@@ -793,11 +852,11 @@ class SpotiFLACGUI(QWidget):
|
|||||||
|
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
self.tab_widget.setCurrentIndex(0)
|
self.tab_widget.setCurrentIndex(0)
|
||||||
|
|
||||||
except SpotifyInvalidUrlException as e:
|
|
||||||
self.log_output.append(f'Error: {str(e)}')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_output.append(f'Error: Failed to fetch metadata: {str(e)}')
|
self.log_output.append(f'Error: {str(e)}')
|
||||||
|
|
||||||
|
def on_metadata_error(self, error_message):
|
||||||
|
self.log_output.append(f'Error: {error_message}')
|
||||||
|
|
||||||
def handle_track_metadata(self, track_data):
|
def handle_track_metadata(self, track_data):
|
||||||
track_id = track_data["external_urls"].split("/")[-1]
|
track_id = track_data["external_urls"].split("/")[-1]
|
||||||
@@ -911,10 +970,21 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.followers_label.hide()
|
self.followers_label.hide()
|
||||||
|
|
||||||
if metadata.get('releaseDate'):
|
if metadata.get('releaseDate'):
|
||||||
release_date = datetime.strptime(metadata['releaseDate'], "%Y-%m-%d")
|
try:
|
||||||
formatted_date = release_date.strftime("%d-%m-%Y")
|
release_date = metadata['releaseDate']
|
||||||
self.release_date_label.setText(f"<b>Released</b> {formatted_date}")
|
if len(release_date) == 4:
|
||||||
self.release_date_label.show()
|
date_obj = datetime.strptime(release_date, "%Y")
|
||||||
|
elif len(release_date) == 7:
|
||||||
|
date_obj = datetime.strptime(release_date, "%Y-%m")
|
||||||
|
else:
|
||||||
|
date_obj = datetime.strptime(release_date, "%Y-%m-%d")
|
||||||
|
|
||||||
|
formatted_date = date_obj.strftime("%d-%m-%Y")
|
||||||
|
self.release_date_label.setText(f"<b>Released</b> {formatted_date}")
|
||||||
|
self.release_date_label.show()
|
||||||
|
except ValueError:
|
||||||
|
self.release_date_label.setText(f"<b>Released</b> {metadata['releaseDate']}")
|
||||||
|
self.release_date_label.show()
|
||||||
else:
|
else:
|
||||||
self.release_date_label.hide()
|
self.release_date_label.hide()
|
||||||
|
|
||||||
@@ -1025,7 +1095,8 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.use_track_numbers,
|
self.use_track_numbers,
|
||||||
self.use_album_subfolders,
|
self.use_album_subfolders,
|
||||||
self.use_fallback,
|
self.use_fallback,
|
||||||
service
|
service,
|
||||||
|
self.timeout_value
|
||||||
)
|
)
|
||||||
self.worker.finished.connect(self.on_download_finished)
|
self.worker.finished.connect(self.on_download_finished)
|
||||||
self.worker.progress.connect(self.update_progress)
|
self.worker.progress.connect(self.update_progress)
|
||||||
@@ -1044,8 +1115,27 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.tab_widget.setCurrentWidget(self.process_tab)
|
self.tab_widget.setCurrentWidget(self.process_tab)
|
||||||
|
|
||||||
def update_progress(self, message, percentage):
|
def update_progress(self, message, percentage):
|
||||||
self.log_output.append(message)
|
if "Download progress:" in message or "Processing metadata..." in message:
|
||||||
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
current_text = self.log_output.toPlainText()
|
||||||
|
|
||||||
|
if current_text:
|
||||||
|
lines = current_text.split('\n')
|
||||||
|
|
||||||
|
if "Download progress:" in lines[-1] or "Processing metadata..." in lines[-1]:
|
||||||
|
lines[-1] = message
|
||||||
|
|
||||||
|
new_text = '\n'.join(lines)
|
||||||
|
|
||||||
|
self.log_output.setPlainText(new_text)
|
||||||
|
|
||||||
|
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
|
||||||
|
else:
|
||||||
|
self.log_output.append(message)
|
||||||
|
else:
|
||||||
|
self.log_output.append(message)
|
||||||
|
else:
|
||||||
|
self.log_output.append(message)
|
||||||
|
|
||||||
if percentage > 0:
|
if percentage > 0:
|
||||||
self.progress_bar.setValue(percentage)
|
self.progress_bar.setValue(percentage)
|
||||||
|
|
||||||
|
|||||||
+193
-55
@@ -5,7 +5,7 @@ import json
|
|||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Tuple, Callable
|
from typing import Tuple, Callable, Dict, Any, List
|
||||||
|
|
||||||
_TOTP_SECRET = bytearray([53,53,48,55,49,52,53,56,53,51,52,56,55,52,57,57,53,57,50,50,52,56,54,51,48,51,50,57,51,52,55])
|
_TOTP_SECRET = bytearray([53,53,48,55,49,52,53,56,53,51,52,56,55,52,57,57,53,57,50,50,52,56,54,51,48,51,50,57,51,52,55])
|
||||||
|
|
||||||
@@ -100,9 +100,7 @@ def get_json_from_api(api_url, access_token):
|
|||||||
|
|
||||||
return req.json()
|
return req.json()
|
||||||
|
|
||||||
def get_raw_spotify_data(spotify_url):
|
def get_access_token():
|
||||||
url_info = parse_uri(spotify_url)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
totp, timestamp = generate_totp()
|
totp, timestamp = generate_totp()
|
||||||
|
|
||||||
@@ -117,34 +115,116 @@ def get_raw_spotify_data(spotify_url):
|
|||||||
req = requests.get(token_url, headers=headers, params=params, timeout=10)
|
req = requests.get(token_url, headers=headers, params=params, timeout=10)
|
||||||
if req.status_code != 200:
|
if req.status_code != 200:
|
||||||
return {"error": f"Failed to get access token. Status code: {req.status_code}"}
|
return {"error": f"Failed to get access token. Status code: {req.status_code}"}
|
||||||
token = req.json()
|
return req.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Failed to get access token: {str(e)}"}
|
return {"error": f"Failed to get access token: {str(e)}"}
|
||||||
|
|
||||||
|
def fetch_tracks_in_batches(url: str, access_token: str, batch_size: int = 100, delay: float = 1.0) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
all_tracks = []
|
||||||
|
current_batch = 0
|
||||||
|
|
||||||
|
while url:
|
||||||
|
print(f"Batch : {current_batch}")
|
||||||
|
|
||||||
|
url_parts = url.split("offset=")
|
||||||
|
if len(url_parts) > 1:
|
||||||
|
offset_part = url_parts[1].split("&")[0]
|
||||||
|
print(f"Offset : {offset_part}")
|
||||||
|
print("-------------")
|
||||||
|
|
||||||
|
track_data = get_json_from_api(url, access_token)
|
||||||
|
if not track_data:
|
||||||
|
break
|
||||||
|
|
||||||
|
items = track_data.get('items', [])
|
||||||
|
all_tracks.extend(items)
|
||||||
|
|
||||||
|
url = track_data.get('next')
|
||||||
|
if url and "&locale=" in url:
|
||||||
|
url = url.split("&locale=")[0]
|
||||||
|
|
||||||
|
if url and delay > 0:
|
||||||
|
sleep(delay)
|
||||||
|
|
||||||
|
current_batch += 1
|
||||||
|
|
||||||
|
return all_tracks, current_batch
|
||||||
|
|
||||||
|
def get_raw_spotify_data(spotify_url, batch: bool = False, delay: float = 1.0):
|
||||||
|
url_info = parse_uri(spotify_url)
|
||||||
|
token = get_access_token()
|
||||||
|
|
||||||
|
if "error" in token:
|
||||||
|
return token
|
||||||
|
|
||||||
|
access_token = token["accessToken"]
|
||||||
raw_data = {}
|
raw_data = {}
|
||||||
|
|
||||||
if url_info['type'] == "playlist":
|
if url_info['type'] == "playlist":
|
||||||
try:
|
try:
|
||||||
playlist_data = get_json_from_api(
|
playlist_data = get_json_from_api(
|
||||||
playlist_base_url.format(url_info["id"]),
|
playlist_base_url.format(url_info["id"]),
|
||||||
token["accessToken"]
|
access_token
|
||||||
)
|
)
|
||||||
if not playlist_data:
|
if not playlist_data:
|
||||||
return {"error": "Failed to get playlist data"}
|
return {"error": "Failed to get playlist data"}
|
||||||
|
|
||||||
raw_data = playlist_data
|
raw_data = playlist_data
|
||||||
|
total_tracks = playlist_data.get('tracks', {}).get('total', 0)
|
||||||
|
|
||||||
tracks = []
|
if batch:
|
||||||
tracks_url = f'https://api.spotify.com/v1/playlists/{url_info["id"]}/tracks?limit=100'
|
tracks_url = f'https://api.spotify.com/v1/playlists/{url_info["id"]}/tracks?limit=100'
|
||||||
while tracks_url:
|
tracks, num_batches = fetch_tracks_in_batches(tracks_url, access_token, 100, delay)
|
||||||
track_data = get_json_from_api(tracks_url, token["accessToken"])
|
raw_data['tracks']['items'] = tracks
|
||||||
if not track_data:
|
raw_data['_batch_count'] = num_batches
|
||||||
break
|
raw_data['_batch_enabled'] = True
|
||||||
|
|
||||||
tracks.extend(track_data['items'])
|
if len(tracks) < total_tracks:
|
||||||
tracks_url = track_data.get('next')
|
last_offset = len(tracks)
|
||||||
|
remaining_tracks = []
|
||||||
|
|
||||||
|
while last_offset < total_tracks:
|
||||||
|
print(f"Batch : {num_batches}")
|
||||||
|
print(f"Offset : {last_offset}")
|
||||||
|
print("-------------")
|
||||||
|
|
||||||
|
remainder_url = f'https://api.spotify.com/v1/playlists/{url_info["id"]}/tracks?offset={last_offset}&limit=100'
|
||||||
|
track_data = get_json_from_api(remainder_url, access_token)
|
||||||
|
|
||||||
|
if not track_data or not track_data.get('items'):
|
||||||
|
break
|
||||||
|
|
||||||
|
items = track_data.get('items', [])
|
||||||
|
remaining_tracks.extend(items)
|
||||||
|
|
||||||
|
if len(items) < 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
last_offset += len(items)
|
||||||
|
num_batches += 1
|
||||||
|
|
||||||
|
if delay > 0:
|
||||||
|
sleep(delay)
|
||||||
|
|
||||||
|
tracks.extend(remaining_tracks)
|
||||||
|
raw_data['tracks']['items'] = tracks
|
||||||
|
raw_data['_batch_count'] = num_batches
|
||||||
|
else:
|
||||||
|
tracks = []
|
||||||
|
tracks_url = f'https://api.spotify.com/v1/playlists/{url_info["id"]}/tracks?limit=100'
|
||||||
|
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]
|
||||||
|
|
||||||
|
raw_data['tracks']['items'] = tracks
|
||||||
|
raw_data['_batch_enabled'] = False
|
||||||
|
|
||||||
raw_data['tracks']['items'] = tracks
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Failed to get playlist data: {str(e)}"}
|
return {"error": f"Failed to get playlist data: {str(e)}"}
|
||||||
|
|
||||||
@@ -152,25 +232,68 @@ def get_raw_spotify_data(spotify_url):
|
|||||||
try:
|
try:
|
||||||
album_data = get_json_from_api(
|
album_data = get_json_from_api(
|
||||||
album_base_url.format(url_info["id"]),
|
album_base_url.format(url_info["id"]),
|
||||||
token["accessToken"]
|
access_token
|
||||||
)
|
)
|
||||||
if not album_data:
|
if not album_data:
|
||||||
return {"error": "Failed to get album data"}
|
return {"error": "Failed to get album data"}
|
||||||
|
|
||||||
album_data['_token'] = token["accessToken"]
|
album_data['_token'] = access_token
|
||||||
raw_data = album_data
|
raw_data = album_data
|
||||||
|
total_tracks = album_data.get('total_tracks', 0)
|
||||||
|
|
||||||
tracks = []
|
if batch:
|
||||||
tracks_url = f'{album_base_url.format(url_info["id"])}/tracks?limit=50'
|
tracks_url = f'{album_base_url.format(url_info["id"])}/tracks?limit=50'
|
||||||
while tracks_url:
|
tracks, num_batches = fetch_tracks_in_batches(tracks_url, access_token, 50, delay)
|
||||||
track_data = get_json_from_api(tracks_url, token["accessToken"])
|
raw_data['tracks']['items'] = tracks
|
||||||
if not track_data:
|
raw_data['_batch_count'] = num_batches
|
||||||
break
|
raw_data['_batch_enabled'] = True
|
||||||
|
|
||||||
tracks.extend(track_data['items'])
|
if len(tracks) < total_tracks:
|
||||||
tracks_url = track_data.get('next')
|
last_offset = len(tracks)
|
||||||
|
remaining_tracks = []
|
||||||
|
|
||||||
|
while last_offset < total_tracks:
|
||||||
|
print(f"Batch : {num_batches}")
|
||||||
|
print(f"Offset : {last_offset}")
|
||||||
|
print("-------------")
|
||||||
|
|
||||||
|
remainder_url = f'{album_base_url.format(url_info["id"])}/tracks?offset={last_offset}&limit=50'
|
||||||
|
track_data = get_json_from_api(remainder_url, access_token)
|
||||||
|
|
||||||
|
if not track_data or not track_data.get('items'):
|
||||||
|
break
|
||||||
|
|
||||||
|
items = track_data.get('items', [])
|
||||||
|
remaining_tracks.extend(items)
|
||||||
|
|
||||||
|
if len(items) < 50:
|
||||||
|
break
|
||||||
|
|
||||||
|
last_offset += len(items)
|
||||||
|
num_batches += 1
|
||||||
|
|
||||||
|
if delay > 0:
|
||||||
|
sleep(delay)
|
||||||
|
|
||||||
|
tracks.extend(remaining_tracks)
|
||||||
|
raw_data['tracks']['items'] = tracks
|
||||||
|
raw_data['_batch_count'] = num_batches
|
||||||
|
else:
|
||||||
|
tracks = []
|
||||||
|
tracks_url = f'{album_base_url.format(url_info["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]
|
||||||
|
|
||||||
|
raw_data['tracks']['items'] = tracks
|
||||||
|
raw_data['_batch_enabled'] = False
|
||||||
|
|
||||||
raw_data['tracks']['items'] = tracks
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Failed to get album data: {str(e)}"}
|
return {"error": f"Failed to get album data: {str(e)}"}
|
||||||
|
|
||||||
@@ -178,7 +301,7 @@ def get_raw_spotify_data(spotify_url):
|
|||||||
try:
|
try:
|
||||||
track_data = get_json_from_api(
|
track_data = get_json_from_api(
|
||||||
track_base_url.format(url_info["id"]),
|
track_base_url.format(url_info["id"]),
|
||||||
token["accessToken"]
|
access_token
|
||||||
)
|
)
|
||||||
if not track_data:
|
if not track_data:
|
||||||
return {"error": "Failed to get track data"}
|
return {"error": "Failed to get track data"}
|
||||||
@@ -191,10 +314,10 @@ def get_raw_spotify_data(spotify_url):
|
|||||||
|
|
||||||
def format_track_data(track_data):
|
def format_track_data(track_data):
|
||||||
artists = []
|
artists = []
|
||||||
for artist in track_data['artists']:
|
for artist in track_data.get('artists', []):
|
||||||
artists.append(artist['name'])
|
artists.append(artist['name'])
|
||||||
|
|
||||||
image_url = track_data.get('album', {}).get('images', [{}])[0].get('url', '')
|
image_url = track_data.get('album', {}).get('images', [{}])[0].get('url', '') if track_data.get('album', {}).get('images') else ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"track": {
|
"track": {
|
||||||
@@ -211,10 +334,10 @@ def format_track_data(track_data):
|
|||||||
|
|
||||||
def format_album_data(album_data):
|
def format_album_data(album_data):
|
||||||
artists = []
|
artists = []
|
||||||
for artist in album_data['artists']:
|
for artist in album_data.get('artists', []):
|
||||||
artists.append(artist['name'])
|
artists.append(artist['name'])
|
||||||
|
|
||||||
image_url = album_data.get('images', [{}])[0].get('url', '')
|
image_url = album_data.get('images', [{}])[0].get('url', '') if album_data.get('images') else ''
|
||||||
|
|
||||||
track_list = []
|
track_list = []
|
||||||
for track in album_data.get('tracks', {}).get('items', []):
|
for track in album_data.get('tracks', {}).get('items', []):
|
||||||
@@ -233,28 +356,38 @@ def format_album_data(album_data):
|
|||||||
"external_urls": track.get('external_urls', {}).get('spotify', '')
|
"external_urls": track.get('external_urls', {}).get('spotify', '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
album_info = {
|
||||||
|
"total_tracks": album_data.get('total_tracks', 0),
|
||||||
|
"name": album_data.get('name', ''),
|
||||||
|
"release_date": album_data.get('release_date', ''),
|
||||||
|
"artists": ", ".join(artists),
|
||||||
|
"images": image_url
|
||||||
|
}
|
||||||
|
|
||||||
|
if album_data.get('_batch_enabled', False):
|
||||||
|
album_info["batch"] = f"{album_data.get('_batch_count', 1)}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"album_info": {
|
"album_info": album_info,
|
||||||
"total_tracks": album_data.get('total_tracks', 0),
|
|
||||||
"name": album_data.get('name', ''),
|
|
||||||
"release_date": album_data.get('release_date', ''),
|
|
||||||
"artists": ", ".join(artists),
|
|
||||||
"images": image_url
|
|
||||||
},
|
|
||||||
"track_list": track_list
|
"track_list": track_list
|
||||||
}
|
}
|
||||||
|
|
||||||
def format_playlist_data(playlist_data):
|
def format_playlist_data(playlist_data):
|
||||||
image_url = playlist_data.get('images', [{}])[0].get('url', '')
|
image_url = playlist_data.get('images', [{}])[0].get('url', '') if playlist_data.get('images') else ''
|
||||||
|
|
||||||
track_list = []
|
track_list = []
|
||||||
for item in playlist_data.get('tracks', {}).get('items', []):
|
for item in playlist_data.get('tracks', {}).get('items', []):
|
||||||
track = item.get('track', {})
|
track = item.get('track', {})
|
||||||
|
if not track:
|
||||||
|
continue
|
||||||
|
|
||||||
artists = []
|
artists = []
|
||||||
for artist in track.get('artists', []):
|
for artist in track.get('artists', []):
|
||||||
artists.append(artist['name'])
|
artists.append(artist['name'])
|
||||||
|
|
||||||
track_image = track.get('album', {}).get('images', [{}])[0].get('url', '')
|
track_image = ''
|
||||||
|
if track.get('album', {}).get('images'):
|
||||||
|
track_image = track.get('album', {}).get('images', [{}])[0].get('url', '')
|
||||||
|
|
||||||
track_list.append({
|
track_list.append({
|
||||||
"artists": ", ".join(artists),
|
"artists": ", ".join(artists),
|
||||||
@@ -267,16 +400,21 @@ def format_playlist_data(playlist_data):
|
|||||||
"external_urls": track.get('external_urls', {}).get('spotify', '')
|
"external_urls": track.get('external_urls', {}).get('spotify', '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
playlist_info = {
|
||||||
|
"tracks": {"total": playlist_data.get('tracks', {}).get('total', 0)},
|
||||||
|
"followers": {"total": playlist_data.get('followers', {}).get('total', 0)},
|
||||||
|
"owner": {
|
||||||
|
"display_name": playlist_data.get('owner', {}).get('display_name', ''),
|
||||||
|
"name": playlist_data.get('name', ''),
|
||||||
|
"images": image_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if playlist_data.get('_batch_enabled', False):
|
||||||
|
playlist_info["batch"] = f"{playlist_data.get('_batch_count', 1)}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"playlist_info": {
|
"playlist_info": playlist_info,
|
||||||
"tracks": {"total": playlist_data.get('tracks', {}).get('total', 0)},
|
|
||||||
"followers": {"total": playlist_data.get('followers', {}).get('total', 0)},
|
|
||||||
"owner": {
|
|
||||||
"display_name": playlist_data.get('owner', {}).get('display_name', ''),
|
|
||||||
"name": playlist_data.get('name', ''),
|
|
||||||
"images": image_url
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"track_list": track_list
|
"track_list": track_list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +434,8 @@ def process_spotify_data(raw_data, data_type):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Error processing data: {str(e)}"}
|
return {"error": f"Error processing data: {str(e)}"}
|
||||||
|
|
||||||
def get_filtered_data(spotify_url):
|
def get_filtered_data(spotify_url, batch=False, delay=1.0):
|
||||||
raw_data = get_raw_spotify_data(spotify_url)
|
raw_data = get_raw_spotify_data(spotify_url, batch=batch, delay=delay)
|
||||||
if raw_data and "error" not in raw_data:
|
if raw_data and "error" not in raw_data:
|
||||||
url_info = parse_uri(spotify_url)
|
url_info = parse_uri(spotify_url)
|
||||||
filtered_data = process_spotify_data(raw_data, url_info['type'])
|
filtered_data = process_spotify_data(raw_data, url_info['type'])
|
||||||
@@ -305,11 +443,11 @@ def get_filtered_data(spotify_url):
|
|||||||
return {"error": "Failed to get raw data"}
|
return {"error": "Failed to get raw data"}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
playlist = "https://open.spotify.com/playlist/37i9dQZEVXbNG2KDcFcKOF"
|
playlist = "https://open.spotify.com/playlist/5Qvz8wZIRYbEUUFoPueKI5"
|
||||||
album = "https://open.spotify.com/album/7kFyd5oyJdVX2pIi6P4iHE"
|
album = "https://open.spotify.com/album/7kFyd5oyJdVX2pIi6P4iHE"
|
||||||
song = "https://open.spotify.com/track/4wJ5Qq0jBN4ajy7ouZIV1c"
|
song = "https://open.spotify.com/track/4wJ5Qq0jBN4ajy7ouZIV1c"
|
||||||
|
|
||||||
filtered_playlist = get_filtered_data(playlist)
|
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))
|
||||||
|
|
||||||
filtered_album = get_filtered_data(album)
|
filtered_album = get_filtered_data(album)
|
||||||
|
|||||||
+39
-33
@@ -6,28 +6,21 @@ import re
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
class TrackDownloader:
|
class TrackDownloader:
|
||||||
def __init__(self, use_fallback=False):
|
def __init__(self, use_fallback=False, timeout=30):
|
||||||
self.client = requests.Session()
|
self.client = requests.Session()
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
}
|
}
|
||||||
self.progress_callback = None
|
self.progress_callback = None
|
||||||
self.filename_format = 'title_artist'
|
|
||||||
self.use_fallback = use_fallback
|
self.use_fallback = use_fallback
|
||||||
|
self.timeout = timeout
|
||||||
self.base_domain = "lucida.su" if use_fallback else "lucida.to"
|
self.base_domain = "lucida.su" if use_fallback else "lucida.to"
|
||||||
|
|
||||||
def set_progress_callback(self, callback):
|
def set_progress_callback(self, callback):
|
||||||
self.progress_callback = callback
|
self.progress_callback = callback
|
||||||
|
|
||||||
def set_filename_format(self, format_type):
|
|
||||||
self.filename_format = format_type
|
|
||||||
|
|
||||||
def generate_filename(self, metadata):
|
def generate_filename(self, track_id, service):
|
||||||
if self.filename_format == 'artist_title':
|
return f"{track_id}_{service}.flac"
|
||||||
filename = f"{metadata['artists']} - {metadata['title']}.flac"
|
|
||||||
else:
|
|
||||||
filename = f"{metadata['title']} - {metadata['artists']}.flac"
|
|
||||||
return self.sanitize_filename(filename)
|
|
||||||
|
|
||||||
async def get_track_info(self, track_id, service="amazon", use_fallback=None):
|
async def get_track_info(self, track_id, service="amazon", use_fallback=None):
|
||||||
if use_fallback is None:
|
if use_fallback is None:
|
||||||
@@ -42,6 +35,8 @@ class TrackDownloader:
|
|||||||
if "error" in result:
|
if "error" in result:
|
||||||
raise Exception(f"Failed to get track info: {result['error']}")
|
raise Exception(f"Failed to get track info: {result['error']}")
|
||||||
|
|
||||||
|
result["track_id"] = track_id
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def convert_spotify_link(self, spotify_url, target_service="amazon", domain_type="to"):
|
def convert_spotify_link(self, spotify_url, target_service="amazon", domain_type="to"):
|
||||||
@@ -73,13 +68,13 @@ class TrackDownloader:
|
|||||||
}
|
}
|
||||||
|
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
session.verify = False
|
session.verify = True
|
||||||
|
|
||||||
response = session.get(
|
response = session.get(
|
||||||
base_url,
|
base_url,
|
||||||
params=request_params,
|
params=request_params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=30
|
timeout=self.timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
html_content = response.text
|
html_content = response.text
|
||||||
@@ -129,9 +124,7 @@ class TrackDownloader:
|
|||||||
"token": {
|
"token": {
|
||||||
"primary": None,
|
"primary": None,
|
||||||
"expiry": None
|
"expiry": None
|
||||||
},
|
}
|
||||||
"title": "Title",
|
|
||||||
"artists": "Artist"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
@@ -149,27 +142,18 @@ class TrackDownloader:
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
return {"error": str(error)}
|
return {"error": str(error)}
|
||||||
|
|
||||||
def sanitize_filename(self, filename):
|
def download(self, metadata, output_dir, is_paused_callback=None, is_stopped_callback=None):
|
||||||
invalid_chars = '<>:"/\\|?*'
|
|
||||||
for char in invalid_chars:
|
|
||||||
filename = filename.replace(char, '')
|
|
||||||
|
|
||||||
filename = ' '.join(filename.split())
|
|
||||||
filename = filename.replace(' ,', ',')
|
|
||||||
filename = filename.replace(',', ', ')
|
|
||||||
while ' ' in filename:
|
|
||||||
filename = filename.replace(' ', ' ')
|
|
||||||
filename = filename.rsplit('.', 1)
|
|
||||||
filename[0] = filename[0].strip()
|
|
||||||
return '.'.join(filename)
|
|
||||||
|
|
||||||
def download(self, metadata, output_dir):
|
|
||||||
track_url = metadata['url']
|
track_url = metadata['url']
|
||||||
primary_token = metadata['token']['primary']
|
primary_token = metadata['token']['primary']
|
||||||
expiry = metadata['token']['expiry']
|
expiry = metadata['token']['expiry']
|
||||||
|
track_id = metadata['track_id']
|
||||||
|
service = metadata['service']
|
||||||
|
|
||||||
print(f"Starting download for: {track_url}")
|
print(f"Starting download for: {track_url}")
|
||||||
|
|
||||||
|
if is_stopped_callback and is_stopped_callback():
|
||||||
|
raise Exception("Download stopped by user")
|
||||||
|
|
||||||
initial_request = {
|
initial_request = {
|
||||||
"account": {"id": "auto", "type": "country"},
|
"account": {"id": "auto", "type": "country"},
|
||||||
"compat": "false",
|
"compat": "false",
|
||||||
@@ -201,12 +185,20 @@ class TrackDownloader:
|
|||||||
handoff = initial_response["handoff"]
|
handoff = initial_response["handoff"]
|
||||||
server = initial_response["server"]
|
server = initial_response["server"]
|
||||||
|
|
||||||
file_name = self.generate_filename(metadata)
|
file_name = self.generate_filename(track_id, service)
|
||||||
|
|
||||||
completion_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}"
|
completion_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}"
|
||||||
|
|
||||||
print("Waiting for track processing to complete")
|
print("Waiting for track processing to complete")
|
||||||
while True:
|
while True:
|
||||||
|
if is_stopped_callback and is_stopped_callback():
|
||||||
|
raise Exception("Download stopped by user")
|
||||||
|
|
||||||
|
while is_paused_callback and is_paused_callback():
|
||||||
|
time.sleep(0.1)
|
||||||
|
if is_stopped_callback and is_stopped_callback():
|
||||||
|
raise Exception("Download stopped by user")
|
||||||
|
|
||||||
completion_response = self.client.get(completion_url, headers=self.headers).json()
|
completion_response = self.client.get(completion_url, headers=self.headers).json()
|
||||||
|
|
||||||
status = completion_response["status"]
|
status = completion_response["status"]
|
||||||
@@ -250,6 +242,20 @@ class TrackDownloader:
|
|||||||
last_update_time = start_time
|
last_update_time = start_time
|
||||||
|
|
||||||
for chunk in response.iter_content(chunk_size=8192):
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
if is_stopped_callback and is_stopped_callback():
|
||||||
|
file.close()
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
raise Exception("Download stopped by user")
|
||||||
|
|
||||||
|
while is_paused_callback and is_paused_callback():
|
||||||
|
time.sleep(0.1)
|
||||||
|
if is_stopped_callback and is_stopped_callback():
|
||||||
|
file.close()
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
raise Exception("Download stopped by user")
|
||||||
|
|
||||||
if chunk:
|
if chunk:
|
||||||
file.write(chunk)
|
file.write(chunk)
|
||||||
downloaded_size += len(chunk)
|
downloaded_size += len(chunk)
|
||||||
@@ -289,7 +295,7 @@ async def main():
|
|||||||
|
|
||||||
output_dir = "."
|
output_dir = "."
|
||||||
track_id = "2plbrEY59IikOBgBGLjaoe"
|
track_id = "2plbrEY59IikOBgBGLjaoe"
|
||||||
service = "amazon"
|
service = "tidal"
|
||||||
|
|
||||||
def progress_update(current, total):
|
def progress_update(current, total):
|
||||||
if total > 0:
|
if total > 0:
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "2.1"
|
"version": "2.5"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user