diff --git a/SpotiFLAC.py b/SpotiFLAC.py index d7e0fe6..5c4bbb2 100644 --- a/SpotiFLAC.py +++ b/SpotiFLAC.py @@ -20,7 +20,8 @@ from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException -from qobuzDL import QobuzDownloader +from qobuzAutoDL import QobuzDownloader as QobuzAutoDownloader +from qobuzRegionDL import QobuzDownloader as QobuzRegionDownloader from tidalDL import TidalDownloader from deezerDL import DeezerDownloader from amazonDL import LucidaDownloader @@ -35,6 +36,7 @@ class Track: duration_ms: int id: str isrc: str = "" + release_date: str = "" class MetadataFetchWorker(QThread): finished = pyqtSignal(dict) @@ -62,7 +64,7 @@ class DownloadWorker(QThread): 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, - use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us"): + use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us", qobuz_mode="auto"): super().__init__() self.tracks = tracks self.outpath = outpath @@ -76,6 +78,7 @@ class DownloadWorker(QThread): self.use_album_subfolders = use_album_subfolders self.service = service self.qobuz_region = qobuz_region + self.qobuz_mode = qobuz_mode self.is_paused = False self.is_stopped = False self.failed_tracks = [] @@ -92,7 +95,10 @@ class DownloadWorker(QThread): def run(self): try: if self.service == "qobuz": - downloader = QobuzDownloader(self.qobuz_region) + if self.qobuz_mode == "auto": + downloader = QobuzAutoDownloader() + else: + downloader = QobuzRegionDownloader(self.qobuz_region) elif self.service == "tidal": downloader = TidalDownloader() elif self.service == "deezer": @@ -361,13 +367,17 @@ class QobuzStatusChecker(QThread): status_updated = pyqtSignal(bool) error = pyqtSignal(str) - def __init__(self, region="us"): + def __init__(self, region="us", mode="auto"): super().__init__() self.region = region + self.mode = mode def run(self): try: - response = requests.get(f"https://{self.region}.qobuz.squid.wtf", timeout=5) + if self.mode == "auto": + response = requests.get("https://qobuz.squid.wtf", timeout=5) + else: + response = requests.get(f"https://{self.region}.qqdl.site", timeout=5) self.status_updated.emit(response.status_code == 200) except Exception as e: self.error.emit(f"Error checking Qobuz status: {str(e)}") @@ -569,8 +579,11 @@ class QobuzRegionComboBox(QComboBox): current_dir = os.path.dirname(os.path.abspath(__file__)) self.regions = [ + {'id': 'us', 'name': 'USA', 'icon': 'us.svg', 'online': False}, {'id': 'eu', 'name': 'Europe', 'icon': 'eu.svg', 'online': False}, - {'id': 'us', 'name': 'North America', 'icon': 'us.svg', 'online': False} + {'id': 'br', 'name': 'Brazil', 'icon': 'br.svg', 'online': False}, + {'id': 'jp', 'name': 'Japan', 'icon': 'jp.svg', 'online': False}, + {'id': 'au', 'name': 'Australia', 'icon': 'au.svg', 'online': False} ] for region in self.regions: @@ -612,7 +625,7 @@ class QobuzRegionComboBox(QComboBox): for region in self.regions: region_id = region['id'] - checker = QobuzStatusChecker(region_id) + checker = QobuzStatusChecker(region_id, "region") checker.status_updated.connect(lambda status, rid=region_id: self.handle_status_update(rid, status)) checker.start() self.status_checkers[region_id] = checker @@ -627,7 +640,7 @@ class QobuzRegionComboBox(QComboBox): class SpotiFLACGUI(QWidget): def __init__(self): super().__init__() - self.current_version = "4.4" + self.current_version = "4.5" self.tracks = [] self.all_tracks = [] self.reset_state() @@ -642,8 +655,11 @@ class SpotiFLACGUI(QWidget): self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool) self.service = self.settings.value('service', 'tidal') self.qobuz_region = self.settings.value('qobuz_region', 'us') + self.qobuz_mode = self.settings.value('qobuz_mode', 'auto') self.check_for_updates = self.settings.value('check_for_updates', True, type=bool) self.current_theme_color = self.settings.value('theme_color', '#2196F3') + self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration') + self.date_format = self.settings.value('date_format', 'dd_mm_yyyy') self.elapsed_time = QTime(0, 0, 0) self.timer = QTimer(self) @@ -662,6 +678,9 @@ class SpotiFLACGUI(QWidget): if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value: combobox.setCurrentIndex(i) return True + if combobox.itemData(i, Qt.ItemDataRole.UserRole) == target_value: + combobox.setCurrentIndex(i) + return True return False def check_updates(self): @@ -762,11 +781,74 @@ class SpotiFLACGUI(QWidget): self.update_track_list_display() + def format_track_date(self, release_date): + if not release_date: + return "" + + try: + if len(release_date) == 4: + date_obj = datetime.strptime(release_date, "%Y") + if self.date_format == "yyyy": + return date_obj.strftime('%Y') + else: + return date_obj.strftime('%Y') + elif len(release_date) == 7: + date_obj = datetime.strptime(release_date, "%Y-%m") + if self.date_format == "dd_mm_yyyy": + return date_obj.strftime('%m-%Y') + elif self.date_format == "yyyy_mm_dd": + return date_obj.strftime('%Y-%m') + else: + return date_obj.strftime('%Y') + else: + date_obj = datetime.strptime(release_date, "%Y-%m-%d") + if self.date_format == "dd_mm_yyyy": + return date_obj.strftime('%d-%m-%Y') + elif self.date_format == "yyyy_mm_dd": + return date_obj.strftime('%Y-%m-%d') + else: + return date_obj.strftime('%Y') + except ValueError: + return release_date + def update_track_list_display(self): self.track_list.clear() for i, track in enumerate(self.tracks, 1): duration = self.format_duration(track.duration_ms) - self.track_list.addItem(f"{i}. {track.title} - {track.artists} • {duration}") + formatted_date = self.format_track_date(track.release_date) + + if self.track_list_format == "artist_track_date_duration": + display_parts = [f"{i}. {track.artists} - {track.title}"] + if formatted_date: + display_parts.append(formatted_date) + display_parts.append(duration) + display_text = " • ".join(display_parts) + elif self.track_list_format == "track_artist_date": + display_parts = [f"{i}. {track.title} - {track.artists}"] + if formatted_date: + display_parts.append(formatted_date) + display_text = " • ".join(display_parts) + elif self.track_list_format == "artist_track_date": + display_parts = [f"{i}. {track.artists} - {track.title}"] + if formatted_date: + display_parts.append(formatted_date) + display_text = " • ".join(display_parts) + elif self.track_list_format == "track_artist_duration": + display_text = f"{i}. {track.title} - {track.artists} • {duration}" + elif self.track_list_format == "artist_track_duration": + display_text = f"{i}. {track.artists} - {track.title} • {duration}" + elif self.track_list_format == "track_artist": + display_text = f"{i}. {track.title} - {track.artists}" + elif self.track_list_format == "artist_track": + display_text = f"{i}. {track.artists} - {track.title}" + else: + display_parts = [f"{i}. {track.title} - {track.artists}"] + if formatted_date: + display_parts.append(formatted_date) + display_parts.append(duration) + display_text = " • ".join(display_parts) + + self.track_list.addItem(display_text) def browse_output(self): directory = QFileDialog.getExistingDirectory(self, "Select Output Directory") @@ -963,15 +1045,16 @@ class SpotiFLACGUI(QWidget): def setup_settings_tab(self): settings_tab = QWidget() settings_layout = QVBoxLayout() - settings_layout.setSpacing(10) - settings_layout.setContentsMargins(9, 9, 9, 9) + settings_layout.setSpacing(4) + settings_layout.setContentsMargins(10, 10, 10, 10) output_group = QWidget() output_layout = QVBoxLayout(output_group) - output_layout.setSpacing(5) + output_layout.setSpacing(2) + output_layout.setContentsMargins(0, 0, 0, 0) output_label = QLabel('Output Directory') - output_label.setStyleSheet("font-weight: bold;") + output_label.setStyleSheet("font-weight: bold; margin-top: 0px; margin-bottom: 5px;") output_layout.addWidget(output_label) output_dir_layout = QHBoxLayout() @@ -985,18 +1068,67 @@ class SpotiFLACGUI(QWidget): self.output_browse.clicked.connect(self.browse_output) output_dir_layout.addWidget(self.output_dir) + output_dir_layout.addSpacing(5) output_dir_layout.addWidget(self.output_browse) output_layout.addLayout(output_dir_layout) settings_layout.addWidget(output_group) + dashboard_group = QWidget() + dashboard_layout = QVBoxLayout(dashboard_group) + dashboard_layout.setSpacing(3) + dashboard_layout.setContentsMargins(0, 0, 0, 0) + + dashboard_label = QLabel('Dashboard Settings') + dashboard_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") + dashboard_layout.addWidget(dashboard_label) + + dashboard_controls_layout = QHBoxLayout() + + list_format_label = QLabel('Track List View:') + list_format_label.setFixedWidth(90) + + self.track_list_format_dropdown = QComboBox() + self.track_list_format_dropdown.addItem("Track - Artist - Date - Duration", "track_artist_date_duration") + self.track_list_format_dropdown.addItem("Artist - Track - Date - Duration", "artist_track_date_duration") + self.track_list_format_dropdown.addItem("Track - Artist - Date", "track_artist_date") + self.track_list_format_dropdown.addItem("Artist - Track - Date", "artist_track_date") + self.track_list_format_dropdown.addItem("Track - Artist - Duration", "track_artist_duration") + self.track_list_format_dropdown.addItem("Artist - Track - Duration", "artist_track_duration") + self.track_list_format_dropdown.addItem("Track - Artist", "track_artist") + self.track_list_format_dropdown.addItem("Artist - Track", "artist_track") + self.track_list_format_dropdown.currentIndexChanged.connect(self.save_track_list_format) + + dashboard_controls_layout.addWidget(list_format_label) + dashboard_controls_layout.addWidget(self.track_list_format_dropdown) + + dashboard_controls_layout.addSpacing(15) + + date_format_label = QLabel('Date Format:') + date_format_label.setFixedWidth(80) + + self.date_format_dropdown = QComboBox() + self.date_format_dropdown.addItem("DD-MM-YYYY", "dd_mm_yyyy") + self.date_format_dropdown.addItem("YYYY-MM-DD", "yyyy_mm_dd") + self.date_format_dropdown.addItem("YYYY", "yyyy") + self.date_format_dropdown.currentIndexChanged.connect(self.save_date_format) + + dashboard_controls_layout.addWidget(date_format_label) + dashboard_controls_layout.addWidget(self.date_format_dropdown) + dashboard_controls_layout.addStretch() + + dashboard_layout.addLayout(dashboard_controls_layout) + + settings_layout.addWidget(dashboard_group) + file_group = QWidget() file_layout = QVBoxLayout(file_group) - file_layout.setSpacing(5) + file_layout.setSpacing(2) + file_layout.setContentsMargins(0, 0, 0, 0) file_label = QLabel('File Settings') - file_label.setStyleSheet("font-weight: bold;") + file_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") file_layout.addWidget(file_label) format_layout = QHBoxLayout() @@ -1027,7 +1159,9 @@ class SpotiFLACGUI(QWidget): format_layout.addWidget(format_label) format_layout.addWidget(self.title_artist_radio) + format_layout.addSpacing(10) format_layout.addWidget(self.artist_title_radio) + format_layout.addSpacing(10) format_layout.addWidget(self.title_only_radio) format_layout.addStretch() file_layout.addLayout(format_layout) @@ -1039,14 +1173,16 @@ class SpotiFLACGUI(QWidget): self.artist_subfolder_checkbox.setChecked(self.use_artist_subfolders) self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting) checkbox_layout.addWidget(self.artist_subfolder_checkbox) + checkbox_layout.addSpacing(10) self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)') self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.album_subfolder_checkbox.setChecked(self.use_album_subfolders) self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting) checkbox_layout.addWidget(self.album_subfolder_checkbox) + checkbox_layout.addSpacing(10) - self.track_number_checkbox = QCheckBox('Track Number for Album') + self.track_number_checkbox = QCheckBox('Track Number') self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.track_number_checkbox.setChecked(self.use_track_numbers) self.track_number_checkbox.toggled.connect(self.save_track_numbering) @@ -1059,10 +1195,11 @@ class SpotiFLACGUI(QWidget): auth_group = QWidget() auth_layout = QVBoxLayout(auth_group) - auth_layout.setSpacing(5) + auth_layout.setSpacing(2) + auth_layout.setContentsMargins(0, 0, 0, 0) auth_label = QLabel('Service Settings') - auth_label.setStyleSheet("font-weight: bold;") + auth_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") auth_layout.addWidget(auth_label) service_fallback_layout = QHBoxLayout() @@ -1076,13 +1213,25 @@ class SpotiFLACGUI(QWidget): service_fallback_layout.addSpacing(10) - region_label = QLabel('Region:') + self.qobuz_mode_label = QLabel('Mode:') + self.qobuz_mode_dropdown = QComboBox() + self.qobuz_mode_dropdown.addItem("Auto", "auto") + self.qobuz_mode_dropdown.addItem("Region", "region") + self.qobuz_mode_dropdown.currentIndexChanged.connect(self.on_qobuz_mode_changed) + service_fallback_layout.addWidget(self.qobuz_mode_label) + service_fallback_layout.addWidget(self.qobuz_mode_dropdown) + + service_fallback_layout.addSpacing(10) + + self.region_label = QLabel('Region:') self.qobuz_region_dropdown = QobuzRegionComboBox() self.qobuz_region_dropdown.currentIndexChanged.connect(self.save_qobuz_region_setting) - service_fallback_layout.addWidget(region_label) + service_fallback_layout.addWidget(self.region_label) service_fallback_layout.addWidget(self.qobuz_region_dropdown) - region_label.hide() + self.qobuz_mode_label.hide() + self.qobuz_mode_dropdown.hide() + self.region_label.hide() self.qobuz_region_dropdown.hide() service_fallback_layout.addStretch() @@ -1093,7 +1242,10 @@ class SpotiFLACGUI(QWidget): settings_tab.setLayout(settings_layout) self.tab_widget.addTab(settings_tab, "Settings") self.set_combobox_value(self.service_dropdown, self.service) - self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region) + self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region) + self.set_combobox_value(self.qobuz_mode_dropdown, self.qobuz_mode) + self.set_combobox_value(self.track_list_format_dropdown, self.track_list_format) + self.set_combobox_value(self.date_format_dropdown, self.date_format) self.update_service_ui() @@ -1105,7 +1257,7 @@ class SpotiFLACGUI(QWidget): theme_tab = QWidget() theme_layout = QVBoxLayout() theme_layout.setSpacing(8) - theme_layout.setContentsMargins(15, 15, 15, 15) + theme_layout.setContentsMargins(8, 15, 15, 15) grid_layout = QVBoxLayout() @@ -1302,8 +1454,7 @@ class SpotiFLACGUI(QWidget): about_layout.addWidget(section_widget) - footer_label = QLabel(f"v{self.current_version} | August 2025") - footer_label.setStyleSheet("font-size: 12px; margin-top: 20px;") + footer_label = QLabel(f"v{self.current_version} | September 2025") about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter) about_tab.setLayout(about_layout) @@ -1318,26 +1469,38 @@ class SpotiFLACGUI(QWidget): self.update_service_ui() self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}") + def on_qobuz_mode_changed(self, index): + mode = self.qobuz_mode_dropdown.currentData() + self.qobuz_mode = mode + self.settings.setValue('qobuz_mode', mode) + self.settings.sync() + + self.update_qobuz_mode_ui() + self.log_output.append(f"Qobuz mode changed to: {self.qobuz_mode_dropdown.currentText()}") + def update_service_ui(self): service = self.service - - region_label = None - for widget in self.qobuz_region_dropdown.parentWidget().children(): - if isinstance(widget, QLabel) and widget.text() == "Region:": - region_label = widget - break if service == "qobuz": - if region_label: - region_label.show() - self.qobuz_region_dropdown.show() - elif service == "deezer": - if region_label: - region_label.hide() - self.qobuz_region_dropdown.hide() + self.qobuz_mode_label.show() + self.qobuz_mode_dropdown.show() + self.update_qobuz_mode_ui() else: - if region_label: - region_label.hide() + self.qobuz_mode_label.hide() + self.qobuz_mode_dropdown.hide() + self.region_label.hide() + self.qobuz_region_dropdown.hide() + + def update_qobuz_mode_ui(self): + mode = self.qobuz_mode_dropdown.currentData() + if mode is None: + mode = self.qobuz_mode + + if mode == "region": + self.region_label.show() + self.qobuz_region_dropdown.show() + else: + self.region_label.hide() self.qobuz_region_dropdown.hide() def save_url(self): @@ -1376,6 +1539,22 @@ class SpotiFLACGUI(QWidget): self.settings.sync() self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}") + def save_track_list_format(self): + format_value = self.track_list_format_dropdown.currentData() + self.track_list_format = format_value + self.settings.setValue('track_list_format', format_value) + self.settings.sync() + if self.tracks: + self.update_track_list_display() + + def save_date_format(self): + format_value = self.date_format_dropdown.currentData() + self.date_format = format_value + self.settings.setValue('date_format', format_value) + self.settings.sync() + if self.tracks: + self.update_track_list_display() + def save_settings(self): self.settings.setValue('output_path', self.output_dir.text().strip()) self.settings.sync() @@ -1437,7 +1616,8 @@ class SpotiFLACGUI(QWidget): track_number=1, duration_ms=track_data.get("duration_ms", 0), id=track_id, - isrc=track_data.get("isrc", "") + isrc=track_data.get("isrc", ""), + release_date=track_data.get("release_date", "") ) self.tracks = [track] @@ -1470,7 +1650,8 @@ class SpotiFLACGUI(QWidget): track_number=track["track_number"], duration_ms=track.get("duration_ms", 0), id=track_id, - isrc=track.get("isrc", "") + isrc=track.get("isrc", ""), + release_date=track.get("release_date", "") )) self.all_tracks = self.tracks.copy() @@ -1501,7 +1682,8 @@ class SpotiFLACGUI(QWidget): track_number=track.get("track_number", len(self.tracks) + 1), duration_ms=track.get("duration_ms", 0), id=track_id, - isrc=track.get("isrc", "") + isrc=track.get("isrc", ""), + release_date=track.get("release_date", "") )) self.all_tracks = self.tracks.copy() @@ -1674,6 +1856,7 @@ class SpotiFLACGUI(QWidget): def start_download_worker(self, tracks_to_download, outpath): service = self.service_dropdown.currentData() qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us" + qobuz_mode = self.qobuz_mode_dropdown.currentData() if service == "qobuz" else "auto" self.worker = DownloadWorker( tracks_to_download, @@ -1687,7 +1870,8 @@ class SpotiFLACGUI(QWidget): self.use_artist_subfolders, self.use_album_subfolders, service, - qobuz_region + qobuz_region, + qobuz_mode ) self.worker.finished.connect(self.on_download_finished) self.worker.progress.connect(self.update_progress) @@ -1773,9 +1957,7 @@ class SpotiFLACGUI(QWidget): if track in self.all_tracks: self.all_tracks.remove(track) - if self.is_playlist: - for i, track in enumerate(self.all_tracks, 1): - track.track_number = i + self.update_track_list_display() diff --git a/amazonDL.py b/amazonDL.py index 671ad51..4ac7e4f 100644 --- a/amazonDL.py +++ b/amazonDL.py @@ -5,9 +5,13 @@ import re import base64 import urllib3 from urllib.parse import unquote +from random import randrange urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +def get_random_user_agent(): + return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}" + def extract_data(html, patterns): for pattern in patterns: if match := re.search(pattern, html): @@ -17,7 +21,7 @@ def extract_data(html, patterns): def download_track(track_id, service="amazon", output_dir="."): client = requests.Session() client.verify = False - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} + headers = {'User-Agent': get_random_user_agent()} try: spotify_url = f"https://open.spotify.com/track/{track_id}" @@ -39,7 +43,7 @@ def download_track(track_id, service="amazon", output_dir="."): decoded_token = token clean_url = url.replace('\\/', '/') - print(f"Starting download for: {clean_url}") + print(f"Fetching: {clean_url}") request_data = { "account": {"id": "auto", "type": "country"}, @@ -61,18 +65,18 @@ def download_track(track_id, service="amazon", output_dir="."): raise Exception(f"Request failed: {data.get('error', 'Unknown error')}") completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}" - print("Processing track...") + print("Fetching URL...") while True: resp = client.get(completion_url, headers=headers).json() if resp["status"] == "completed": - print("Processing completed!") + print("URL found") break elif resp["status"] == "error": raise Exception(f"Processing failed: {resp.get('message', 'Unknown error')}") elif progress := resp.get("progress"): percent = int((progress.get("current", 0) / progress.get("total", 100)) * 100) - print(f"Progress: {percent}%") + print(f"\r{percent}%", end="") time.sleep(1) download_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}/download" @@ -88,14 +92,15 @@ def download_track(track_id, service="amazon", output_dir="."): file_name = file_name.strip() file_path = os.path.join(output_dir, file_name) - print(f"Downloading: {file_name}") + print(f"Downloading...") with open(file_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) - print(f"Download completed: {file_path}") + print("Download complete") + print("Done") return file_path except Exception as e: @@ -116,7 +121,8 @@ class LucidaDownloader: raise Exception(f"Amazon Music download failed: {str(e)}") if __name__ == "__main__": + print("=== AmazonDL - Amazon Music Downloader ===") track_id = "2plbrEY59IikOBgBGLjaoe" service = "amazon" - download_track(track_id, service) + download_track(track_id, service) \ No newline at end of file diff --git a/au.svg b/au.svg new file mode 100644 index 0000000..96e8076 --- /dev/null +++ b/au.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/br.svg b/br.svg new file mode 100644 index 0000000..719a763 --- /dev/null +++ b/br.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deezerDL.py b/deezerDL.py index 4d0d22b..6c4e422 100644 --- a/deezerDL.py +++ b/deezerDL.py @@ -3,12 +3,16 @@ import asyncio import os import sys from mutagen.flac import FLAC +from random import randrange + +def get_random_user_agent(): + return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}" class DeezerDownloader: def __init__(self): self.session = requests.Session() self.session.headers.update({ - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + 'User-Agent': get_random_user_agent() }) self.progress_callback = None diff --git a/getMetadata.py b/getMetadata.py index 26247c8..fd69339 100644 --- a/getMetadata.py +++ b/getMetadata.py @@ -58,7 +58,7 @@ 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/{}' headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'User-Agent': get_random_user_agent(), 'Accept': 'application/json', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', diff --git a/jp.svg b/jp.svg new file mode 100644 index 0000000..cc1c181 --- /dev/null +++ b/jp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/qobuzAutoDL.py b/qobuzAutoDL.py new file mode 100644 index 0000000..315c184 --- /dev/null +++ b/qobuzAutoDL.py @@ -0,0 +1,251 @@ +import requests +import time +import os +import re +from datetime import datetime +from mutagen.flac import FLAC, Picture +from mutagen.id3 import PictureType +from random import randrange + +def get_random_user_agent(): + return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}" + +class ProgressCallback: + def __call__(self, current, total): + if total > 0: + percent = (current / total) * 100 + print(f"\r{percent:.2f}% ({current}/{total})", end="") + else: + print(f"\r{current / (1024 * 1024):.2f} MB", end="") + +class QobuzDownloader: + def __init__(self, timeout=30): + self.timeout = timeout + self.session = requests.Session() + self.headers = { + 'User-Agent': get_random_user_agent() + } + self.base_api_url = "https://qobuz.squid.wtf/api" + self.download_chunk_size = 256 * 1024 + self.progress_callback = ProgressCallback() + + def set_progress_callback(self, callback): + self.progress_callback = callback + + def sanitize_filename(self, filename): + if not filename: + return "Unknown Track" + sanitized = re.sub(r'[\\/*?:"<>|]', "", str(filename)) + return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track" + + def get_track_info(self, isrc): + print(f"Fetching: {isrc}") + search_url = f"{self.base_api_url}/get-music" + params = {'q': isrc, 'offset': 0, 'limit': 10, 'region': 'auto'} + + try: + response = self.session.get(search_url, params=params, timeout=self.timeout) + response.raise_for_status() + data = response.json() + + selected_track = None + if data and data.get("success"): + items = data.get("data", {}).get("tracks", {}).get("items", []) + priority = {24: 1, 16: 2} + for track in items: + if track.get("isrc") == isrc: + current_prio = priority.get(track.get("maximum_bit_depth"), 3) + if selected_track is None or current_prio < priority.get(selected_track.get("maximum_bit_depth"), 3): + selected_track = track + if current_prio == 1: + break + + if not selected_track: + raise Exception(f"Track not found: {isrc}") + + title = selected_track.get('title', 'Unknown') + bit_depth = selected_track.get('maximum_bit_depth', 'Unknown') + print(f"Found: {title} ({bit_depth}b)") + return selected_track + + except requests.exceptions.RequestException as e: + raise Exception(f"Request error: {e}") + except Exception as e: + raise Exception(f"Error: {e}") + + def get_download_url(self, track_id): + print("Fetching URL...") + download_api_url = f"{self.base_api_url}/download-music" + params = {'track_id': track_id, 'quality': 27, 'region': 'auto'} + + try: + response = self.session.get(download_api_url, params=params, timeout=self.timeout) + response.raise_for_status() + data = response.json() + + if data and data.get("success") and data.get("data", {}).get("url"): + download_url = data["data"]["url"] + print("URL found") + return download_url + else: + error_msg = data.get('error', {}).get('message', 'Unknown API error') + raise Exception(f"API error: {error_msg}") + + except requests.exceptions.RequestException as e: + raise Exception(f"Request error: {e}") + except Exception as e: + raise Exception(f"Error: {e}") + + def download(self, isrc, output_dir=".", is_paused_callback=None, is_stopped_callback=None): + if output_dir != ".": + try: + os.makedirs(output_dir, exist_ok=True) + except OSError as e: + raise Exception(f"Directory error: {e}") + + track_info = self.get_track_info(isrc) + track_id = track_info.get("id") + + if not track_id: + raise Exception("No track ID found") + + artist_name = self.sanitize_filename(track_info.get('performer', {}).get('name')) + track_title = self.sanitize_filename(track_info.get('title')) + output_filename = os.path.join(output_dir, f"{artist_name} - {track_title}.flac") + + if os.path.exists(output_filename): + file_size = os.path.getsize(output_filename) + if file_size > 0: + print(f"File already exists: {output_filename} ({file_size / (1024 * 1024):.2f} MB)") + return output_filename + + download_url = self.get_download_url(track_id) + temp_filename = output_filename + ".part" + + print(f"Downloading...") + try: + response = self.session.get(download_url, timeout=900) + response.raise_for_status() + + if is_stopped_callback and is_stopped_callback(): + raise Exception("Download stopped") + + while is_paused_callback and is_paused_callback(): + time.sleep(0.1) + if is_stopped_callback and is_stopped_callback(): + raise Exception("Download stopped") + + with open(temp_filename, 'wb') as f: + f.write(response.content) + + downloaded_size = len(response.content) + total_size = downloaded_size + + if self.progress_callback: + self.progress_callback(downloaded_size, total_size) + + os.rename(temp_filename, output_filename) + print("Download complete") + + except requests.exceptions.RequestException as e: + if os.path.exists(temp_filename): + os.remove(temp_filename) + raise Exception(f"Download failed: {e}") + except Exception as e: + if os.path.exists(temp_filename): + os.remove(temp_filename) + raise Exception(f"File error: {e}") + + print("Adding metadata...") + try: + self._embed_metadata(output_filename, track_info) + print("Metadata saved") + except Exception as e: + print(f"Tagging failed: {e}") + + print(f"Done") + return output_filename + + def _embed_metadata(self, filename, track_info): + try: + audio = FLAC(filename) + audio.delete() + audio.clear_pictures() + + album_info = track_info.get('album', {}) + artist = track_info.get('performer', {}).get('name') + + if track_info.get('title'): + audio['TITLE'] = track_info['title'] + if artist: + audio['ARTIST'] = artist + if album_info.get('title'): + audio['ALBUM'] = album_info['title'] + if album_info.get('artist', {}).get('name', artist): + audio['ALBUMARTIST'] = album_info.get('artist', {}).get('name', artist) + if track_info.get('track_number'): + audio['TRACKNUMBER'] = str(track_info['track_number']) + if track_info.get('release_date_original'): + audio['DATE'] = track_info['release_date_original'] + try: + audio['YEAR'] = str(datetime.strptime(track_info['release_date_original'], '%Y-%m-%d').year) + except ValueError: + pass + if album_info.get('genre', {}).get('name'): + audio['GENRE'] = album_info['genre']['name'] + if track_info.get('copyright'): + audio['COPYRIGHT'] = track_info['copyright'] + if track_info.get('isrc'): + audio['ISRC'] = track_info['isrc'] + if album_info.get('label', {}).get('name'): + audio['ORGANIZATION'] = album_info['label']['name'] + + img_info = album_info.get('image', {}) + cover_url = img_info.get('large') or img_info.get('small') or img_info.get('thumbnail') + if cover_url: + try: + img_response = self.session.get(cover_url, timeout=30) + img_response.raise_for_status() + mime_type = img_response.headers.get('Content-Type', 'image/jpeg').lower() + if mime_type in ['image/jpeg', 'image/png']: + picture = Picture() + picture.data = img_response.content + picture.type = PictureType.COVER_FRONT + picture.mime = mime_type + audio.add_picture(picture) + print("Cover added") + except Exception as e: + print(f"Cover error: {str(e)}") + + audio.save() + + except Exception as e: + raise Exception(f"Metadata error: {e}") + +def main(): + print("=== QobuzDL - Qobuz Downloader (Auto) ===") + downloader = QobuzDownloader() + + isrc = "USAT22409172" + output_dir = "." + + try: + downloaded_file = downloader.download(isrc, output_dir) + print(f"Success: File saved as {downloaded_file}") + except Exception as e: + print(f"Error: {str(e)}") + +if __name__ == "__main__": + try: + import sys + if sys.platform == "win32": + import os + os.system("chcp 65001 > nul") + try: + sys.stdout.reconfigure(encoding='utf-8') + except: + pass + except: + pass + + main() \ No newline at end of file diff --git a/qobuzDL.py b/qobuzRegionDL.py similarity index 93% rename from qobuzDL.py rename to qobuzRegionDL.py index ada4e74..bafc02d 100644 --- a/qobuzDL.py +++ b/qobuzRegionDL.py @@ -5,6 +5,10 @@ import re from datetime import datetime from mutagen.flac import FLAC, Picture from mutagen.id3 import PictureType +from random import randrange + +def get_random_user_agent(): + return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}" class ProgressCallback: def __call__(self, current, total): @@ -16,16 +20,16 @@ class ProgressCallback: class QobuzDownloader: def __init__(self, region="us", timeout=30): - if region not in ["eu", "us"]: - raise ValueError("Region must be either 'us' or 'eu'") + if region not in ["us", "eu", "br", "jp", "au"]: + raise ValueError("Region must be one of: 'us', 'eu', 'br', 'jp', 'au'") self.region = region self.timeout = timeout self.session = requests.Session() 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': get_random_user_agent() } - self.base_api_url = f"https://{region}.qobuz.squid.wtf/api" + self.base_api_url = f"https://{region}.qqdl.site/api" self.download_chunk_size = 256 * 1024 self.progress_callback = ProgressCallback() @@ -223,7 +227,7 @@ class QobuzDownloader: raise Exception(f"Metadata error: {e}") def main(): - print("=== QobuzDL - Qobuz Downloader ===") + print("=== QobuzDL - Qobuz Downloader (Region) ===") downloader = QobuzDownloader(region="us") isrc = "USAT22409172"