diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dfdeaa5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/main.py", + "console": "integratedTerminal" + } + ] +} diff --git a/README.md b/README.md index 536bbb1..3f6bd6d 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ All images were created with the _Show-Thumbnails_ setting: **Off** [![](showcase/Select_Playlist_Range.png)](#playlist) +## Precise-Selection-Dialog for playlists + +[![](showcase/Select_Playlist_Precise.png)](#playlist) + ## Download-Page for playlists [![](showcase/Download_Playlist.png)](#playlist) diff --git a/appdata/changelog.md b/appdata/changelog.md index 6370ed7..6f60277 100644 --- a/appdata/changelog.md +++ b/appdata/changelog.md @@ -1,19 +1,11 @@ New Features:
Bug Fixes: diff --git a/appdata/style.qss b/appdata/style.qss index ec8376b..9bf9e10 100644 --- a/appdata/style.qss +++ b/appdata/style.qss @@ -141,7 +141,9 @@ QSlider { background-color: none; min-height: 20px; } - +QSlider::handle:disabled{ + background: #b2b2b2; +} QSlider::handle { background: white; height: 20px; @@ -254,6 +256,9 @@ QTableWidget QScrollBar{ border: 0; padding: 0; } +QTableWidget{ + border: 0; +} QTextBrowser { border: none; diff --git a/exe_installer_setup.iss b/exe_installer_setup.iss index 4572277..7c83148 100644 --- a/exe_installer_setup.iss +++ b/exe_installer_setup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "PyFlat Youtube Downloader" -#define MyAppVersion "1.3.0" +#define MyAppVersion "1.3.1" #define MyAppPublisher "PyFlat Studios" #define MyAppExeName "main.exe" diff --git a/main.py b/main.py index 36b39b8..3be32d7 100644 --- a/main.py +++ b/main.py @@ -23,11 +23,12 @@ def get_abs_path(relative_path): from src.CustomWidgets.ProgressDialog import ProgressDialog from src.Ui_MainWindow import Ui_MainWindow from src.CustomWidgets.SLabel import SLabel +from src.CustomWidgets.VideoSelectDialog import VideoSelectDialog from urllib.request import urlopen from urllib.error import URLError -VERSION = "1.3.0" +VERSION = "1.3.1" class noLogger: def error(msg): @@ -52,8 +53,6 @@ def __init__(self): self.ui.search_stack_widg.setCurrentIndex(0) self.ui.download_2.setCurrentIndex(0) - self.ui.tableWidget.verticalScrollBar().setObjectName("test") - self.ui.tableWidget.focusOutEvent = self.on_focus_out self.bind_keys() @@ -148,6 +147,8 @@ def __init__(self): mw.ui.download_button.clicked.connect(lambda: self.data.prepare_for_download()) mw.ui.next_page_btn.clicked.connect(lambda: mw.ui.download_2.setCurrentIndex(0)) mw.ui.last_page_btn.clicked.connect(lambda: mw.ui.download_2.setCurrentIndex(1)) + mw.ui.select_videos_btn.clicked.connect(lambda: self.show_video_select()) + mw.ui.playlist_range_slider.valueChanged.connect(lambda: self.change_download_range()) mw.ui.scrollArea.verticalScrollBar().valueChanged.connect(lambda: [self.fill_new_widgs()]) mw.search_shortcut.activated.connect(self.enter_pressed) mw.ui.tableWidget.cellClicked.connect(self.handle_clicked) @@ -158,6 +159,7 @@ def __init__(self): self.update_thread = None self.downloads = [] self.cur_process = [] + self.selected_ids = [] self.loading = False self.delete_exe_files() self.connect_menu_actions() @@ -169,6 +171,38 @@ def __init__(self): if getattr(sys, 'frozen', False) and self.update_check: self.search_for_updates() + def show_video_select(self): + videos = [] + for index, playlist_object in enumerate(self.data.playlist_data_objects): + videos.append({"title": playlist_object.title, + "uploader": playlist_object.author, + "playlist_index": index, + "selected": True if index + 1 in self.selected_ids else False + }) + self.video_select_dialog = VideoSelectDialog(mw, videos) + self.video_select_dialog.exec() + self.selected_ids = self.video_select_dialog.get_selected() + if self.selected_ids == [] or self.has_clear_range(self.selected_ids): + mw.ui.playlist_range_slider.setEnabled(True) + else: + mw.ui.playlist_range_slider.setEnabled(False) + + def change_download_range(self): + start, stop = mw.ui.playlist_range_slider.value() + self.selected_ids = [] + for num in range(start, stop + 1): + self.selected_ids.append(num) + + def has_clear_range(self, numbers): + numbers.sort() + + for i in range(len(numbers) - 1): + if numbers[i + 1] - numbers[i] != 1: + return False + mw.ui.playlist_range_slider.setValue((numbers[0], numbers[-1])) + return True + + def enter_pressed(self): if mw.ui.mainpages.currentIndex() == 0: mw.ui.searching_button.click() @@ -413,6 +447,8 @@ def yt_search(self, text, pl_items, req): def use_info(self, info, cur_link): self.loading = False + if not info: + info = {} if info != {} and info["webpage_url_domain"] != None and info["webpage_url_domain"] == "youtube.com" and info["channel"] != None and info != False: self.cur_link = cur_link if "?list=" in cur_link and ("&list=" not in cur_link and "?v=" not in cur_link): @@ -548,6 +584,7 @@ def update_main_frame(self): mw.invokeFunc(mw.ui.download_2, "setCurrentIndex", Qt.QueuedConnection, Q_ARG(int, 1)) mw.invokeFunc(mw.ui.info_range_slider_label, "setText", Qt.QueuedConnection, Q_ARG(str, "Select the Range you want to Download")) mw.ui.date_label.setText(f"Playlist Count: {self.data.playlist_count} Videos") + mw.ui.last_page_btn.setVisible(True) mw.invokeFunc2(mw, "setWidg2Range", Qt.QueuedConnection, Q_ARG(int, 1), Q_ARG(int, self.data.playlist_count)) mw.invokeFunc2(mw, "setWidg2Value", Qt.QueuedConnection, Q_ARG(int, 1), Q_ARG(int, self.data.playlist_count)) @@ -821,6 +858,7 @@ def create_data_objects(self, url, info, index): self.playlist_data_objects[index] = x if not None in self.playlist_data_objects: mw.ui.download_button.setEnabled(True) + mw.ui.select_videos_btn.setEnabled(True) def get_thumbnail_url(self): x = [] @@ -898,12 +936,16 @@ def check_if_exists(self, filename): self.download() def download_playlist(self): - start, stop = mw.ui.playlist_range_slider.value() - def download_next(i): - if i < stop: - self.playlist_data_objects[i].prepare_for_download() - QTimer.singleShot(1000, lambda: download_next(i + 1)) - download_next(start - 1) + if dl.selected_ids == []: + dl.yes_no_messagebox("No video chosen", QMessageBox.Warning, "Warning", QMessageBox.Ok) + return + def download_next(index): + if index < len(dl.selected_ids): + video_id = dl.selected_ids[index]-1 + self.playlist_data_objects[video_id].prepare_for_download() + QTimer.singleShot(1000, lambda: download_next(index + 1)) + + download_next(0) def download(self, row=None): if row == None: @@ -1307,8 +1349,16 @@ def run(self): screenshot = mw.grab() screenshot.save("showcase/Select_Playlist_Range.png", "png") + mw.ui.select_videos_btn.click() + self.msleep(1000) + + screenshot = dl.video_select_dialog.grab() + screenshot.save("showcase/Select_Playlist_Precise.png", "png") + mw.ui.next_page_btn.click() + self.msleep(1000) + screenshot = mw.grab() screenshot.save("showcase/Download_Playlist.png", "png") @@ -1331,6 +1381,6 @@ def qt_message_handler(mode, context, message): qInstallMessageHandler(qt_message_handler) mw = MainWindow() dl = Downloader() - #thread = ScreenShot(mw) - #thread.start() + # thread = ScreenShot(mw) + # thread.start() sys.exit(app.exec()) diff --git a/mainwindow.ui b/mainwindow.ui index 25f5b36..cc007e3 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -23,7 +23,7 @@ - Youtube Downloader v1.3.0 + Youtube Downloader v1.3.1 * { @@ -608,7 +608,7 @@ QTableWidget QScrollBar{ - 0 + 1 @@ -1139,6 +1139,9 @@ QTableWidget QScrollBar{ + + 0 + @@ -1181,16 +1184,41 @@ QTableWidget QScrollBar{ - - - - 150 - 0 - + + + QFrame::StyledPanel - - Next + + QFrame::Raised + + + 25 + + + + + false + + + Select Videos + + + + + + + + 150 + 0 + + + + Next + + + + diff --git a/setup.py b/setup.py index a7f3bca..1c87157 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup( name="youtube_downloader", - version="1.3.0", + version="1.3.1", description="Youtube Downloader", options={"build_exe": build_exe_options}, executables=executables, diff --git a/showcase/Download_Overview.png b/showcase/Download_Overview.png index 8512a27..53be03b 100644 Binary files a/showcase/Download_Overview.png and b/showcase/Download_Overview.png differ diff --git a/showcase/Download_Playlist.png b/showcase/Download_Playlist.png index 66dcc79..3a14e80 100644 Binary files a/showcase/Download_Playlist.png and b/showcase/Download_Playlist.png differ diff --git a/showcase/Search.png b/showcase/Search.png index 3e80e76..cd938c2 100644 Binary files a/showcase/Search.png and b/showcase/Search.png differ diff --git a/showcase/Select_Playlist_Precise.png b/showcase/Select_Playlist_Precise.png new file mode 100644 index 0000000..07a22fc Binary files /dev/null and b/showcase/Select_Playlist_Precise.png differ diff --git a/showcase/Select_Playlist_Range.png b/showcase/Select_Playlist_Range.png index 50ce146..8754cec 100644 Binary files a/showcase/Select_Playlist_Range.png and b/showcase/Select_Playlist_Range.png differ diff --git a/src/CustomWidgets/VideoSelectDialog.py b/src/CustomWidgets/VideoSelectDialog.py new file mode 100644 index 0000000..6d94ed4 --- /dev/null +++ b/src/CustomWidgets/VideoSelectDialog.py @@ -0,0 +1,138 @@ +from PySide6.QtWidgets import * +from PySide6.QtCore import * +from PySide6.QtGui import * + +from src.CustomWidgets.CustomTableWidget import CustomTableWidget + +class VideoSelectDialog(QDialog): + def __init__(self, parent=None, videos=None): + super().__init__(parent) + self.setMinimumSize(1150, 550) + self.setWindowTitle("Video Selector") + + self.search_title_checkbox = QCheckBox("Search Title") + self.search_title_checkbox.setChecked(True) + self.search_uploader_checkbox = QCheckBox("Search Uploader") + self.search_uploader_checkbox.setChecked(True) + self.search_index_checkbox = QCheckBox("Search Index") + self.search_index_checkbox.setChecked(False) + + self.video_table = CustomTableWidget() + self.video_table.setSortingEnabled(True) + self.video_table.setColumnCount(4) + self.video_table.setHorizontalHeaderLabels(["Select", "Title", "Uploader", "Playlist Index"]) + self.video_table.verticalHeader().setVisible(False) + self.video_table.horizontalHeader().setSortIndicatorShown(False) + self.video_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.video_table.setSelectionMode(QAbstractItemView.NoSelection) + + self.search_input = QLineEdit() + self.search_input.setAlignment(Qt.AlignCenter) + self.search_input.textChanged.connect(self.delayed_filter_videos) + + self.load_videos(videos) + + self.select_all_button = QPushButton("Select All") + self.select_all_button.clicked.connect(self.select_all_videos) + + self.deselect_all_button = QPushButton("Deselect All") + self.deselect_all_button.clicked.connect(self.deselect_all_videos) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.select_all_button) + button_layout.addWidget(self.deselect_all_button) + + search_criteria_group = QGroupBox("Search Criteria") + search_criteria_layout = QHBoxLayout() + search_criteria_layout.setSpacing(24) + search_criteria_layout.setAlignment(Qt.AlignLeft) + search_criteria_layout.addWidget(self.search_title_checkbox) + search_criteria_layout.addWidget(self.search_uploader_checkbox) + search_criteria_layout.addWidget(self.search_index_checkbox) + search_criteria_group.setLayout(search_criteria_layout) + + search_layout = QFormLayout() + search_layout.addRow("Search:", self.search_input) + + main_layout = QVBoxLayout() + main_layout.setSpacing(15) + main_layout.addLayout(search_layout) + main_layout.addWidget(search_criteria_group) + main_layout.addLayout(button_layout) + main_layout.addWidget(self.video_table) + + self.setLayout(main_layout) + + for i in range(4): + self.video_table.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch if i > 0 else QHeaderView.ResizeToContents) + + self.search_title_checkbox.stateChanged.connect(self.delayed_filter_videos) + self.search_uploader_checkbox.stateChanged.connect(self.delayed_filter_videos) + + self.search_title = True + self.search_uploader = True + + self.filter_timer = QTimer(self) + self.filter_timer.setSingleShot(True) + self.filter_timer.timeout.connect(self.filter_videos) + self.filter_delay = 300 + + def load_videos(self, videos=None): + self.video_table.setRowCount(len(videos)) + for row, video in enumerate(videos): + title_item = QTableWidgetItem(video["title"]) + uploader_item = QTableWidgetItem(video["uploader"]) + playlist_index_item = PlaylistIndexTableWidgetItem(str(video["playlist_index"]+1)) + checkbox_item = QCheckBox() + checkbox_item.setChecked(video["selected"]) + + self.video_table.setCellWidget(row, 0, checkbox_item) + self.video_table.setItem(row, 1, title_item) + self.video_table.setItem(row, 2, uploader_item) + self.video_table.setItem(row, 3, playlist_index_item) + + for col in range(1, self.video_table.columnCount()): + self.video_table.item(row, col).setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter) + + def delayed_filter_videos(self): + self.filter_timer.start(self.filter_delay) + + def filter_videos(self): + text = self.search_input.text().lower() + for row in range(self.video_table.rowCount()): + title_item = self.video_table.item(row, 1) + uploader_item = self.video_table.item(row, 2) + index_item = self.video_table.item(row, 3) + title_contains_text = self.search_title_checkbox.isChecked() and text in title_item.text().lower() + uploader_contains_text = self.search_uploader_checkbox.isChecked() and text in uploader_item.text().lower() + index_contains_text = self.search_index_checkbox.isChecked() and text in index_item.text() + if title_contains_text or uploader_contains_text or index_contains_text: + self.video_table.setRowHidden(row, False) + else: + self.video_table.setRowHidden(row, True) + + def get_selected(self): + ids = [] + for row in range(self.video_table.rowCount()): + checkbox_item = self.video_table.cellWidget(row, 0) + if checkbox_item.isChecked(): + index_item = self.video_table.item(row, 3) + ids.append(int(index_item.text())) + + return ids + + def select_all_videos(self): + for row in range(self.video_table.rowCount()): + checkbox_item = self.video_table.cellWidget(row, 0) + checkbox_item.setChecked(True) + + def deselect_all_videos(self): + for row in range(self.video_table.rowCount()): + checkbox_item = self.video_table.cellWidget(row, 0) + checkbox_item.setChecked(False) + +class PlaylistIndexTableWidgetItem(QTableWidgetItem): + def __lt__(self, other): + if (self.column() == 3 and other.column() == 3): + return int(self.text()) < int(other.text()) + return super().__lt__(other) \ No newline at end of file diff --git a/src/Ui_MainWindow.py b/src/Ui_MainWindow.py index bd72a1e..23fb4df 100644 --- a/src/Ui_MainWindow.py +++ b/src/Ui_MainWindow.py @@ -461,7 +461,7 @@ def setupUi(self, MainWindow): self.download_bar_page3 = QWidget() self.download_bar_page3.setObjectName(u"download_bar_page3") self.verticalLayout_8 = QVBoxLayout(self.download_bar_page3) - self.verticalLayout_8.setSpacing(10) + self.verticalLayout_8.setSpacing(0) self.verticalLayout_8.setContentsMargins(10, 10, 10, 10) self.verticalLayout_8.setObjectName(u"verticalLayout_8") self.info_range_slider_label = QLabel(self.download_bar_page3) @@ -481,11 +481,28 @@ def setupUi(self, MainWindow): self.verticalLayout_8.addWidget(self.playlist_range_slider) - self.next_page_btn = QPushButton(self.download_bar_page3) + self.frame_4 = QFrame(self.download_bar_page3) + self.frame_4.setObjectName(u"frame_4") + self.frame_4.setFrameShape(QFrame.StyledPanel) + self.frame_4.setFrameShadow(QFrame.Raised) + self.horizontalLayout = QHBoxLayout(self.frame_4) + self.horizontalLayout.setSpacing(25) + self.horizontalLayout.setContentsMargins(10, 10, 10, 10) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.select_videos_btn = QPushButton(self.frame_4) + self.select_videos_btn.setObjectName(u"select_videos_btn") + self.select_videos_btn.setEnabled(False) + + self.horizontalLayout.addWidget(self.select_videos_btn) + + self.next_page_btn = QPushButton(self.frame_4) self.next_page_btn.setObjectName(u"next_page_btn") self.next_page_btn.setMinimumSize(QSize(150, 0)) - self.verticalLayout_8.addWidget(self.next_page_btn, 0, Qt.AlignHCenter) + self.horizontalLayout.addWidget(self.next_page_btn) + + + self.verticalLayout_8.addWidget(self.frame_4, 0, Qt.AlignHCenter) self.download_2.addWidget(self.download_bar_page3) @@ -630,7 +647,7 @@ def setupUi(self, MainWindow): self.retranslateUi(MainWindow) - self.mainpages.setCurrentIndex(0) + self.mainpages.setCurrentIndex(1) self.search_stack_widg.setCurrentIndex(1) self.download_2.setCurrentIndex(1) @@ -639,7 +656,7 @@ def setupUi(self, MainWindow): # setupUi def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Youtube Downloader v1.3.0", None)) + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Youtube Downloader v1.3.1", None)) self.actionSearch_For_Updates.setText(QCoreApplication.translate("MainWindow", u"Search for Updates", None)) self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About", None)) self.actionShow_on_Github.setText(QCoreApplication.translate("MainWindow", u"Show on GitHub", None)) @@ -673,6 +690,7 @@ def retranslateUi(self, MainWindow): self.last_page_btn.setText(QCoreApplication.translate("MainWindow", u"Back", None)) self.download_button.setText(QCoreApplication.translate("MainWindow", u"Download", None)) self.info_range_slider_label.setText("") + self.select_videos_btn.setText(QCoreApplication.translate("MainWindow", u"Select Videos", None)) self.next_page_btn.setText(QCoreApplication.translate("MainWindow", u"Next", None)) ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(0) ___qtablewidgetitem.setText(QCoreApplication.translate("MainWindow", u"Channel", None));