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:
-- The old settings page has been replaced by a menubar.
-- You can now set the default resolution.
-- There is now logging to make it easier to find errors.
-- The video search no longer takes place automatically instead you have to click on the button to start the search.
+- You can now select even more precisely which videos you would like to download from a playlist.
Bug Fixes:
- -
- Fix for issues
- #2 and
- #3
-
- - Fixed a bug where the program crashed when a video was opened in the download overview.
- - Other small bug fixes.
+ - Fixed a bug where the program got an error when searching for a video
+ - Fixed a bug where the button to go to the previous page for playlists disappeared
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));