From 23b63c735e358f5c61553920c0af01c70fa193e2 Mon Sep 17 00:00:00 2001 From: Justin Donofrio Date: Tue, 3 Dec 2024 20:03:39 -0500 Subject: [PATCH] Expose queue worker toggle, reuse icons to avoid high mem usage --- docs/usage.md | 1 + src/onthespot/api/spotify.py | 13 ++- src/onthespot/cli.py | 6 +- src/onthespot/gui/mainui.py | 146 ++++++++++++++++++-------- src/onthespot/gui/qtui/main.ui | 79 +++++++++++++- src/onthespot/gui/settings.py | 55 ++++------ src/onthespot/otsconfig.py | 1 + src/onthespot/parse_item.py | 1 + src/onthespot/templates/settings.html | 5 + src/onthespot/web.py | 10 +- 10 files changed, 222 insertions(+), 95 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index b31092c..bb99006 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -108,6 +108,7 @@ Customize **OnTheSpot** to fit your preferences by adjusting the settings in the | **Raw Media Download** | Downloads an unmodified file from whatever service is selected. With this enabled file conversion and the embedding of any metadata is skipped. | | **Download Delay** | Time (in seconds) to wait before initiating the next download. Helps prevent Spotify's rate limits. | | **Download Chunk Size** | The chunk size in which to download files. | +| **Maximum Queue Workers** | Set the maximum number of queue workers. Setting a higher number will queue songs faster, only change this setting if you know what you're doing. | | **Maximum Download Workers** | Set the maximum number of download workers. Only change this setting if you know what you're doing. | | **Translate File Path** | Translate file paths into the application language. | | **Metadata Separator** | Set the separator for metadata fields with multiple values (default: `; `). | diff --git a/src/onthespot/api/spotify.py b/src/onthespot/api/spotify.py index 8227bb9..b9b134a 100644 --- a/src/onthespot/api/spotify.py +++ b/src/onthespot/api/spotify.py @@ -131,11 +131,11 @@ def spotify_new_session(): with open(session_json_path, 'r') as file: zeroconf_login = json.load(file) except FileNotFoundError: - print(f"Error: The file {session_json_path} was not found.") + logger.error(f"Error: The file {session_json_path} was not found.") except json.JSONDecodeError: - print("Error: Failed to decode JSON from the file.") + logger.error("Error: Failed to decode JSON from the file.") except Exception as e: - print(f"An error occurred: {e}") + logger.error(f"An error occurred: {e}") cfg_copy = config.get('accounts').copy() new_user = { "uuid": uuid_uniq, @@ -158,20 +158,19 @@ def spotify_new_session(): def spotify_login_user(account): try: # I'd prefer to use 'Session.Builder().stored(credentials).create but - # it seems to be broken, loading from credentials file instead + # I can't get it to work, loading from credentials file instead. uuid = account['uuid'] username = account['login']['username'] session_dir = os.path.join(cache_dir(), "onthespot", "sessions") os.makedirs(session_dir, exist_ok=True) session_json_path = os.path.join(session_dir, f"ots_login_{uuid}.json") - print(session_json_path) try: with open(session_json_path, 'w') as file: json.dump(account['login'], file) - print(f"Login information for '{username[:4]}*******' written to {session_json_path}") + logger.info(f"Login information for '{username[:4]}*******' written to {session_json_path}") except IOError as e: - print(f"Error writing to file {session_json_path}: {e}") + logger.error(f"Error writing to file {session_json_path}: {e}") config = Session.Configuration.Builder().set_stored_credential_file(session_json_path).build() diff --git a/src/onthespot/cli.py b/src/onthespot/cli.py index b864dff..585bf66 100644 --- a/src/onthespot/cli.py +++ b/src/onthespot/cli.py @@ -51,6 +51,7 @@ def run(self): else: time.sleep(0.2) + def main(): print('\033[32mLogging In...\033[0m\n', end='', flush=True) @@ -65,8 +66,9 @@ def main(): thread.daemon = True thread.start() - queue_worker = QueueWorker() - queue_worker.start() + for i in range(config.get('maximum_queue_workers')): + queue_worker = QueueWorker() + queue_worker.start() for i in range(config.get('maximum_download_workers')): downloadworker = DownloadWorker(gui=True) diff --git a/src/onthespot/gui/mainui.py b/src/onthespot/gui/mainui.py index ec0ba27..8741967 100644 --- a/src/onthespot/gui/mainui.py +++ b/src/onthespot/gui/mainui.py @@ -23,14 +23,21 @@ logger = get_logger('gui.main_ui') -class QueueWorker(QThread): +class QueueWorker(QObject): add_item_to_download_list = pyqtSignal(dict, dict) + def __init__(self): super().__init__() + self.is_running = True + self.thread = threading.Thread(target=self.run) + + + def start(self): + self.thread.start() def run(self): - while True: + while self.is_running: if pending: try: local_id = next(iter(pending)) @@ -39,6 +46,10 @@ def run(self): token = get_account_token(item['item_service']) item_metadata = globals()[f"{item['item_service']}_get_{item['item_type']}_metadata"](token, item['item_id']) self.add_item_to_download_list.emit(item, item_metadata) + # Padding for 'GLib-ERROR : Creating pipes for GWakeup: Too many open files Trace/breakpoint trap' + # when mass downloading cached responses with download queue thumbnails enabled. + if config.get('show_download_thumbnails'): + time.sleep(0.1) continue except Exception as e: logger.error(f"Unknown Exception for {item}: {str(e)}") @@ -48,6 +59,12 @@ def run(self): time.sleep(0.2) + def stop(self): + logger.info('Stopping Queue Worker') + self.is_running = False + self.thread.join() + + class MainWindow(QMainWindow): # Remove Later @@ -56,18 +73,20 @@ def contribute(self): url = "https://github.com/justin025/OnTheSpot/tree/main#contributing" open_item(url) + def closeEvent(self, event): if config.get('close_to_tray') and get_init_tray(): event.ignore() self.hide() + def __init__(self, _dialog, start_url=''): super(MainWindow, self).__init__() self.path = os.path.dirname(os.path.realpath(__file__)) - icon_path = os.path.join(config.app_root, 'resources', 'icons', 'onthespot.png') + self.icon_cache = {} QApplication.setStyle("fusion") uic.loadUi(os.path.join(self.path, "qtui", "main.ui"), self) - self.setWindowIcon(QtGui.QIcon(icon_path)) + self.setWindowIcon(self.get_icon('onthespot')) self.start_url = start_url self.inp_version.setText(config.get("version")) @@ -87,9 +106,10 @@ def __init__(self, _dialog, start_url=''): fillaccountpool.progress.connect(self.__show_popup_dialog) fillaccountpool.start() - queueworker = QueueWorker() - queueworker.add_item_to_download_list.connect(self.add_item_to_download_list) - queueworker.start() + for i in range(config.get('maximum_queue_workers')): + queueworker = QueueWorker() + queueworker.add_item_to_download_list.connect(self.add_item_to_download_list) + queueworker.start() for i in range(config.get('maximum_download_workers')): downloadworker = DownloadWorker(gui=True) @@ -112,11 +132,11 @@ def __init__(self, _dialog, start_url=''): self.theme_path = os.path.join(config.app_root,'resources', 'themes', f'{self.theme}.qss') if self.theme == "dark": self.toggle_theme_button.setText(self.tr(" Light Theme")) - theme_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'light.png')) + theme_icon = 'light' elif self.theme == "light": self.toggle_theme_button.setText(self.tr(" Dark Theme")) - theme_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'dark.png')) - self.toggle_theme_button.setIcon(theme_icon) + theme_icon = 'dark' + self.toggle_theme_button.setIcon(self.get_icon(theme_icon)) with open(self.theme_path, 'r') as f: theme = f.read() @@ -128,32 +148,40 @@ def __init__(self, _dialog, start_url=''): logger.info("Main window init completed !") + def get_icon(self, name): + if name not in self.icon_cache: + icon_path = os.path.join(config.app_root, 'resources', 'icons', f'{name}.png') + self.icon_cache[name] = QIcon(icon_path) + return self.icon_cache[name] + + def load_dark_theme(self): self.theme = "dark" self.theme_path = os.path.join(config.app_root,'resources', 'themes', f'{self.theme}.qss') - theme_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', f'light.png')) - self.toggle_theme_button.setIcon(theme_icon) + self.toggle_theme_button.setIcon(self.get_icon('light')) self.toggle_theme_button.setText(self.tr(" Light Theme")) with open(self.theme_path, 'r') as f: dark_theme = f.read() self.setStyleSheet(dark_theme) + def load_light_theme(self): self.theme = "light" self.theme_path = os.path.join(config.app_root,'resources', 'themes', f'{self.theme}.qss') - theme_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', f'dark.png')) - self.toggle_theme_button.setIcon(theme_icon) + self.toggle_theme_button.setIcon(self.get_icon('dark')) self.toggle_theme_button.setText(self.tr(" Dark Theme")) with open(self.theme_path, 'r') as f: light_theme = f.read() self.setStyleSheet(light_theme) + def toggle_theme(self): if self.theme == "light": self.load_dark_theme() elif self.theme == "dark": self.load_light_theme() + def bind_button_inputs(self): # Connect button click signals self.btn_search.clicked.connect(self.fill_search_table) @@ -170,12 +198,10 @@ def bind_button_inputs(self): self.inp_search_term.returnPressed.connect(self.fill_search_table) self.btn_progress_clear_complete.clicked.connect(self.remove_completed_from_download_list) - collapse_down_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'collapse_down.png')) - collapse_up_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'collapse_up.png')) self.btn_search_filter_toggle.clicked.connect(lambda toggle: self.group_search_items.show() if self.group_search_items.isHidden() else self.group_search_items.hide()) - self.btn_search_filter_toggle.clicked.connect(lambda switch: self.btn_search_filter_toggle.setIcon(collapse_down_icon) if self.group_search_items.isHidden() else self.btn_search_filter_toggle.setIcon(collapse_up_icon)) + self.btn_search_filter_toggle.clicked.connect(lambda switch: self.btn_search_filter_toggle.setIcon(self.get_icon('collapse_down')) if self.group_search_items.isHidden() else self.btn_search_filter_toggle.setIcon(self.get_icon('collapse_up'))) self.btn_download_filter_toggle.clicked.connect(lambda toggle: self.group_download_items.show() if self.group_download_items.isHidden() else self.group_download_items.hide()) - self.btn_download_filter_toggle.clicked.connect(lambda switch: self.btn_download_filter_toggle.setIcon(collapse_up_icon) if self.group_download_items.isHidden() else self.btn_download_filter_toggle.setIcon(collapse_down_icon)) + self.btn_download_filter_toggle.clicked.connect(lambda switch: self.btn_download_filter_toggle.setIcon(self.get_icon('collapse_up')) if self.group_download_items.isHidden() else self.btn_download_filter_toggle.setIcon(self.get_icon('collapse_down'))) self.inp_download_queue_show_waiting.stateChanged.connect(self.update_table_visibility) self.inp_download_queue_show_failed.stateChanged.connect(self.update_table_visibility) @@ -191,6 +217,7 @@ def bind_button_inputs(self): self.inp_mirror_spotify_playback.stateChanged.connect(self.manage_mirror_spotify_playback) + def set_table_props(self): window_width = self.width() logger.info(f"Setting table item properties {window_width}") @@ -231,20 +258,24 @@ def set_table_props(self): self.set_login_fields() return True + def reset_app_config(self): config.rollback() self.__show_popup_dialog("The application setting was cleared successfully !\n Please restart the application.") + def __select_dir(self): dir_path = QFileDialog.getExistingDirectory(None, 'Select a folder:', os.path.expanduser("~")) if dir_path.strip() != '': self.inp_download_root.setText(QDir.toNativeSeparators(dir_path)) + def __select_tmp_dir(self): dir_path = QFileDialog.getExistingDirectory(None, 'Select a folder:', os.path.expanduser("~")) if dir_path.strip() != '': self.inp_tmp_dl_root.setText(QDir.toNativeSeparators(dir_path)) + def __show_popup_dialog(self, txt, btn_hide=False, download=False): if download and config.get('disable_bulk_dl_notices'): return @@ -255,6 +286,7 @@ def __show_popup_dialog(self, txt, btn_hide=False, download=False): self.__splash_dialog.btn_close.show() self.__splash_dialog.show() + def session_load_done(self): self.__splash_dialog.hide() self.__splash_dialog.btn_close.show() @@ -271,6 +303,7 @@ def session_load_done(self): if is_latest_release() == False: self.__show_popup_dialog(self.tr("

An update is available at the link below,

https://github.com/justin025/onthespot/releases/latest")) + def fill_account_table(self): # Clear the table @@ -287,7 +320,7 @@ def fill_account_table(self): radiobutton.setChecked(True) btn = QPushButton(self.tbl_sessions) - trash_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'trash.png')) + trash_icon = self.get_icon('trash') btn.setIcon(trash_icon) #btn.setText(self.tr(" Remove ")) @@ -295,7 +328,7 @@ def fill_account_table(self): btn.setMinimumHeight(30) service = QTableWidgetItem(str(account["service"]).title()) - service.setIcon(QIcon(os.path.join(config.app_root, 'resources', 'icons', f'{account["service"]}.png'))) + service.setIcon(self.get_icon(account["service"])) self.tbl_sessions.insertRow(rows) self.tbl_sessions.setCellWidget(rows, 0, radiobutton) @@ -307,6 +340,7 @@ def fill_account_table(self): self.tbl_sessions.setCellWidget(rows, 6, btn) logger.info("Accounts table was populated !") + def add_item_to_download_list(self, item, item_metadata): # Skip rendering QButtons if they are not in use copy_btn = None @@ -320,23 +354,20 @@ def add_item_to_download_list(self, item, item_metadata): pbar.setMinimumHeight(30) if config.get("download_copy_btn"): copy_btn = QPushButton() - #copy_btn.setText('Retry') - copy_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'link.png')) - copy_btn.setIcon(copy_icon) + #copy_btn.setText('Copy') + copy_btn.setIcon(self.get_icon('link')) copy_btn.setToolTip(self.tr('Copy')) copy_btn.setMinimumHeight(30) copy_btn.hide() cancel_btn = QPushButton() # cancel_btn.setText('Cancel') - cancel_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'stop.png')) - cancel_btn.setIcon(cancel_icon) + cancel_btn.setIcon(self.get_icon('stop')) cancel_btn.setToolTip(self.tr('Cancel')) cancel_btn.setMinimumHeight(30) cancel_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) retry_btn = QPushButton() #retry_btn.setText('Retry') - retry_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'retry.png')) - retry_btn.setIcon(retry_icon) + retry_btn.setIcon(self.get_icon('retry')) retry_btn.setToolTip(self.tr('Retry')) retry_btn.setMinimumHeight(30) retry_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -344,8 +375,7 @@ def add_item_to_download_list(self, item, item_metadata): if config.get("download_open_btn"): open_btn = QPushButton() #open_btn.setText('Open') - open_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'file.png')) - open_btn.setIcon(open_icon) + open_btn.setIcon(self.get_icon('file')) open_btn.setToolTip(self.tr('Open')) open_btn.setMinimumHeight(30) open_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -353,8 +383,7 @@ def add_item_to_download_list(self, item, item_metadata): if config.get("download_locate_btn"): locate_btn = QPushButton() #locate_btn.setText('Locate') - locate_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'folder.png')) - locate_btn.setIcon(locate_icon) + locate_btn.setIcon(self.get_icon('folder')) locate_btn.setToolTip(self.tr('Locate')) locate_btn.setMinimumHeight(30) locate_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -362,8 +391,7 @@ def add_item_to_download_list(self, item, item_metadata): if config.get("download_delete_btn"): delete_btn = QPushButton() #delete_btn.setText('Delete') - delete_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'trash.png')) - delete_btn.setIcon(delete_icon) + delete_btn.setIcon(self.get_icon('trash')) delete_btn.setToolTip(self.tr('Delete')) delete_btn.setMinimumHeight(30) delete_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -382,7 +410,7 @@ def add_item_to_download_list(self, item, item_metadata): item_service = item["item_service"] service_label = QTableWidgetItem(str(item_service).title()) - service_label.setIcon(QIcon(os.path.join(config.app_root, 'resources', 'icons', f'{item_service}.png'))) + service_label.setIcon(self.get_icon(item_service)) status_label = QLabel(self.tbl_dl_progress) status_label.setText(self.tr("Waiting")) @@ -405,7 +433,7 @@ def add_item_to_download_list(self, item, item_metadata): self.tbl_dl_progress.setCellWidget(rows, 1, item_label) self.tbl_dl_progress.setItem(rows, 2, QTableWidgetItem(item_metadata['artists'])) self.tbl_dl_progress.setItem(rows, 3, QTableWidgetItem(item_category)) - self.tbl_dl_progress.setItem(rows, 4, QTableWidgetItem(service_label)) + self.tbl_dl_progress.setItem(rows, 4, service_label) self.tbl_dl_progress.setCellWidget(rows, 5, status_label) self.tbl_dl_progress.setCellWidget(rows, 6, actions) @@ -426,9 +454,11 @@ def add_item_to_download_list(self, item, item_metadata): 'playlist_by': playlist_by, 'playlist_number': item.get('playlist_number', ''), "gui": { + "item_label": item_label, "status_label": status_label, "progress_bar": pbar, "btn": { + 'actions': actions, "copy": copy_btn, "cancel": cancel_btn, "retry": retry_btn, @@ -476,12 +506,13 @@ def update_item_in_download_list(self, item, status, progress): item['gui']["btn"]['cancel'].show() return + def remove_completed_from_download_list(self): with download_queue_lock: check_row = 0 while check_row < self.tbl_dl_progress.rowCount(): local_id = self.tbl_dl_progress.item(check_row, 0).text() - logger.info(f'Removing Row : {check_row} and mediaid: {local_id}') + logger.info(f'Removing Row: {check_row} and mediaid: {local_id}') if local_id in download_queue: if download_queue[local_id]['item_status'] in ( "Cancelled", @@ -489,13 +520,36 @@ def remove_completed_from_download_list(self): "Downloaded", "Already Exists" ): - logger.info(f'Removing Row : {check_row} and mediaid: {local_id}') + logger.info(f'Removing Row: {check_row} and mediaid: {local_id}') + + # Clear the widget in the last column before removing the row + widget = self.tbl_dl_progress.cellWidget(check_row, 0) + if widget: + widget.deleteLater() # Schedule the widget for deletion + widget = self.tbl_dl_progress.cellWidget(check_row, 1) + if widget: + widget.deleteLater() # Schedule the widget for deletion + widget = self.tbl_dl_progress.cellWidget(check_row, 2) + if widget: + widget.deleteLater() # Schedule the widget for deletion + widget = self.tbl_dl_progress.cellWidget(check_row, 3) + if widget: + widget.deleteLater() # Schedule the widget for deletion + widget = self.tbl_dl_progress.cellWidget(check_row, 4) + if widget: + widget.deleteLater() # Schedule the widget for deletion + widget = self.tbl_dl_progress.cellWidget(check_row, 5) + if widget: + widget.deleteLater() # Schedule the widget for deletion + + # Remove the row from the table self.tbl_dl_progress.removeRow(check_row) download_queue.pop(local_id) else: - check_row = check_row + 1 + check_row += 1 # Move to the next row else: - check_row = check_row + 1 + check_row += 1 # Move to the next row + def cancel_all_downloads(self): with download_queue_lock: @@ -513,6 +567,7 @@ def cancel_all_downloads(self): row_count -= 1 self.update_table_visibility() + def retry_all_failed_downloads(self): with download_queue_lock: row_count = self.tbl_dl_progress.rowCount() @@ -527,6 +582,7 @@ def retry_all_failed_downloads(self): row_count -= 1 self.update_table_visibility() + def user_table_remove_click(self): button = self.sender() button_position = button.pos() @@ -546,9 +602,11 @@ def user_table_remove_click(self): self.__show_popup_dialog(self.tr("Account was removed successfully.")) + def update_config(self): save_config(self) + def set_login_fields(self): # Deezer if self.inp_login_service.currentIndex() == 0: @@ -615,6 +673,7 @@ def set_login_fields(self): youtube_add_account() ) + def add_spotify_account(self): logger.info('Add spotify account clicked ') self.btn_login_add.setText(self.tr("Waiting...")) @@ -625,6 +684,7 @@ def add_spotify_account(self): login_worker.daemon = True login_worker.start() + def add_spotify_account_worker(self): session = spotify_new_session() if session == True: @@ -637,6 +697,7 @@ def add_spotify_account_worker(self): self.btn_login_add.setText(self.tr("Add Account")) self.btn_login_add.setDisabled(False) + def fill_search_table(self): while self.tbl_search_results.rowCount() > 0: self.tbl_search_results.removeRow(0) @@ -671,11 +732,10 @@ def fill_search_table(self): self.inp_search_term.setText('') return - for result in results: btn = QPushButton(self.tbl_search_results) #btn.setText(btn_text.strip()) - btn.setIcon(QIcon(os.path.join(config.app_root, 'resources', 'icons', 'download.png'))) + btn.setIcon(self.get_icon('download')) item_url = result['item_url'] @@ -698,7 +758,7 @@ def download_btn_clicked(item_name, item_url, item_service, item_type, item_id, btn.setMinimumHeight(30) service = QTableWidgetItem(result['item_service'].title()) - service.setIcon(QIcon(os.path.join(config.app_root, 'resources', 'icons', f'{result["item_service"]}.png'))) + service.setIcon(self.get_icon(result["item_service"])) rows = self.tbl_search_results.rowCount() self.tbl_search_results.insertRow(rows) @@ -718,6 +778,7 @@ def download_btn_clicked(item_name, item_url, item_service, item_type, item_id, self.tbl_search_results.horizontalHeader().resizeSection(0, 450) self.inp_search_term.setText('') + def update_table_visibility(self): show_waiting = self.inp_download_queue_show_waiting.isChecked() show_failed = self.inp_download_queue_show_failed.isChecked() @@ -740,6 +801,7 @@ def update_table_visibility(self): else: self.tbl_dl_progress.showRow(row) # Show the row if the status is allowed + def manage_mirror_spotify_playback(self): if self.inp_mirror_spotify_playback.isChecked(): self.mirrorplayback.start() diff --git a/src/onthespot/gui/qtui/main.ui b/src/onthespot/gui/qtui/main.ui index 3abd6f0..31e317a 100644 --- a/src/onthespot/gui/qtui/main.ui +++ b/src/onthespot/gui/qtui/main.ui @@ -656,9 +656,9 @@ 0 - 0 - 660 - 3154 + -1557 + 627 + 3234 @@ -2297,6 +2297,54 @@ true + + + + + + + false + + + + + + Maximum Queue Workers + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 80 + 0 + + + + 1 + + + 999999 + + + + + + @@ -2345,6 +2393,18 @@ + + + + + + + + + + true + + @@ -2380,6 +2440,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + diff --git a/src/onthespot/gui/settings.py b/src/onthespot/gui/settings.py index 9ad4e91..9d1821e 100644 --- a/src/onthespot/gui/settings.py +++ b/src/onthespot/gui/settings.py @@ -18,43 +18,26 @@ def load_config(self): # Hide Popups self.group_search_items.hide() self.group_download_items.hide() - # Icons - en_US_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'en_US.png')) - self.inp_language.insertItem(0, en_US_icon, "English") - de_DE_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'de_DE.png')) - self.inp_language.insertItem(1, de_DE_icon, "Deutsch") - pt_PT_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'pt_PT.png')) - self.inp_language.insertItem(2, pt_PT_icon, "Português") - pirate_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'pirate_flag.png')) - self.inp_language.insertItem(999, pirate_icon, "Contribute") + # Icons + self.inp_language.insertItem(0, self.get_icon('en_US'), "English") + self.inp_language.insertItem(1, self.get_icon('de_DE'), "Deutsch") + self.inp_language.insertItem(2, self.get_icon('pt_PT'), "Português") + self.inp_language.insertItem(999, self.get_icon('pirate_flag'), "Contribute") self.inp_language.currentIndexChanged.connect(self.contribute) - deezer_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'deezer.png')) - self.inp_login_service.insertItem(0, deezer_icon, "") - - soundcloud_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'soundcloud.png')) - self.inp_login_service.insertItem(1, soundcloud_icon, "") - - spotify_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'spotify.png')) - self.inp_login_service.insertItem(2, spotify_icon, "") - - youtube_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'youtube.png')) - self.inp_login_service.insertItem(3, youtube_icon, "") - + self.inp_login_service.insertItem(0, self.get_icon('deezer'), "") + self.inp_login_service.insertItem(1, self.get_icon('soundcloud'), "") + self.inp_login_service.insertItem(2, self.get_icon('spotify'), "") + self.inp_login_service.insertItem(3, self.get_icon('youtube'), "") self.inp_login_service.setCurrentIndex(2) - save_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'save.png')) - self.btn_save_config.setIcon(save_icon) - folder_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'folder.png')) - self.btn_download_root_browse.setIcon(folder_icon) - self.btn_download_tmp_browse.setIcon(folder_icon) - search_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'search.png')) - self.btn_search.setIcon(search_icon) - collapse_down_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'collapse_down.png')) - collapse_up_icon = QIcon(os.path.join(config.app_root, 'resources', 'icons', 'collapse_up.png')) - self.btn_search_filter_toggle.setIcon(collapse_down_icon) - self.btn_download_filter_toggle.setIcon(collapse_up_icon) + self.btn_save_config.setIcon(self.get_icon('save')) + self.btn_download_root_browse.setIcon(self.get_icon('folder')) + self.btn_download_tmp_browse.setIcon(self.get_icon('folder')) + self.btn_search.setIcon(self.get_icon('search')) + self.btn_search_filter_toggle.setIcon(self.get_icon('collapse_down')) + self.btn_download_filter_toggle.setIcon(self.get_icon('collapse_up')) # Text self.inp_language.setCurrentIndex(config.get("language_index")) @@ -77,6 +60,7 @@ def load_config(self): self.inp_album_cover_format.setText(config.get("album_cover_format")) self.inp_search_thumb_height.setValue(config.get("search_thumb_height")) self.inp_metadata_seperator.setText(config.get("metadata_seperator")) + self.inp_maximum_queue_workers.setValue(config.get("maximum_queue_workers")) self.inp_maximum_download_workers.setValue(config.get("maximum_download_workers")) # Checkboxes @@ -156,6 +140,7 @@ def load_config(self): "inp_file_hertz", "inp_download_delay", "inp_chunk_size", + "inp_maximum_queue_workers", "inp_maximum_download_workers" ] @@ -210,13 +195,11 @@ def save_config(self): config.set_('search_thumb_height', self.inp_search_thumb_height.value()) config.set_('disable_bulk_dl_notices', self.inp_disable_bulk_popup.isChecked()) config.set_('metadata_seperator', self.inp_metadata_seperator.text()) - if 0 < self.inp_max_search_results.value() <= 50: - config.set_('max_search_results', self.inp_max_search_results.value()) - else: - config.set_('max_search_results', 5) + config.set_('max_search_results', self.inp_max_search_results.value()) config.set_('media_format', self.inp_media_format.text()) config.set_('podcast_media_format', self.inp_podcast_media_format.text()) config.set_('illegal_character_replacement', self.inp_illegal_character_replacement.text()) + config.set_('maximum_queue_workers', self.inp_maximum_queue_workers.value()) config.set_('maximum_download_workers', self.inp_maximum_download_workers.value()) # Checkboxes: config.set_('key', bool) diff --git a/src/onthespot/otsconfig.py b/src/onthespot/otsconfig.py index 12c37f2..f019e1e 100755 --- a/src/onthespot/otsconfig.py +++ b/src/onthespot/otsconfig.py @@ -48,6 +48,7 @@ def __init__(self, cfg_path=None): "download_root": os.path.join(os.path.expanduser("~"), "Music", "OnTheSpot"), # Root dir for downloads "download_delay": 3, # Seconds to wait before next download attempt "maximum_download_workers": 1, # Maximum number of download workers + "maximum_queue_workers": 1, # Maximum number of queue workers "track_path_formatter": "Tracks" + os.path.sep + "{album_artist}" + os.path.sep + "[{year}] {album}" + os.path.sep + "{track_number}. {name}", # Track path format string "podcast_path_formatter": "Episodes" + os.path.sep + "{album}" + os.path.sep + "{name}", # Episode path format string "playlist_path_formatter": "Playlists" + os.path.sep + "{playlist_name} by {playlist_owner}" + os.path.sep + "{name} - {artist}", # Playlist path format string diff --git a/src/onthespot/parse_item.py b/src/onthespot/parse_item.py index bd6561a..17d19a5 100755 --- a/src/onthespot/parse_item.py +++ b/src/onthespot/parse_item.py @@ -16,6 +16,7 @@ YOUTUBE_URL_REGEX = re.compile(r"https://(www\.|music\.)?youtube\.com/watch\?v=(?P[a-zA-Z0-9_-]+)(&list=(?P[a-zA-Z0-9_-]+))?") #QOBUZ_INTERPRETER_URL_REGEX = re.compile(r"https?://www\.qobuz\.com/\w\w-\w\w/interpreter/[-\w]+/([-\w]+)") + def parse_url(url): if re.match(DEEZER_URL_REGEX, url): match = re.search(DEEZER_URL_REGEX, url) diff --git a/src/onthespot/templates/settings.html b/src/onthespot/templates/settings.html index 5739bfe..aa0b7a1 100644 --- a/src/onthespot/templates/settings.html +++ b/src/onthespot/templates/settings.html @@ -186,6 +186,9 @@

Downloads



+ +

+

@@ -357,6 +360,7 @@

Metadata

const forceRaw = document.getElementById('force_raw').checked; const downloadDelay = document.getElementById('download_delay').value; const translateFilePath = document.getElementById('translate_file_path').value; + const maximumQueueWorkers = document.getElementById('maximum_queue_workers').value; const maximumDownloadWorkers = document.getElementById('maximum_download_workers').value; const metadataSeparator = document.getElementById('metadata_seperator').value; @@ -441,6 +445,7 @@

Metadata

force_raw: forceRaw, download_delay: downloadDelay, translate_file_path: translateFilePath, + maximum_queue_workers: maximumQueueWorkers, maximum_download_workers: maximumDownloadWorkers, metadata_separator: metadataSeparator, overwrite_existing_metadata: overwriteExistingMetadata, diff --git a/src/onthespot/web.py b/src/onthespot/web.py index 3e93068..d414b17 100644 --- a/src/onthespot/web.py +++ b/src/onthespot/web.py @@ -105,7 +105,6 @@ def download_queue_page(): config_path = os.path.join(config_dir(), 'onthespot', 'otsconfig.json') with open(config_path, 'r') as config_file: config_data = json.load(config_file) - print(config_data) return render_template('download_queue.html', config=config_data) @app.route('/') @@ -167,8 +166,8 @@ def update_settings(): def main(): fill_account_pool = FillAccountPool() - fill_account_pool.finished.connect(lambda: print("Finished filling account pool.")) - fill_account_pool.progress.connect(lambda message, status: print(f"{message} {'Success' if status else 'Failed'}")) + fill_account_pool.finished.connect(lambda: logger.info("Finished filling account pool.")) + fill_account_pool.progress.connect(lambda message, status: logger.info(f"{message} {'Success' if status else 'Failed'}")) fill_account_pool.start() @@ -176,8 +175,9 @@ def main(): thread.daemon = True thread.start() - queue_worker = QueueWorker() - queue_worker.start() + for i in range(config.get('maximum_queue_workers')): + queue_worker = QueueWorker() + queue_worker.start() for i in range(config.get('maximum_download_workers')): downloadworker = DownloadWorker(gui=True)