diff --git a/youtility/cut.py b/youtility/cut.py index 6d7b81f..282019c 100644 --- a/youtility/cut.py +++ b/youtility/cut.py @@ -1,12 +1,15 @@ +import logging import sys -from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject, pyqtSlot from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, \ QSpacerItem, QLabel, QFileDialog from pytube import YouTube from qfluentwidgets import (LineEdit, ListWidget, PushButton, MessageBox, ProgressBar, TextEdit) +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + class Stream(QObject): new_text = pyqtSignal(str) @@ -20,7 +23,6 @@ def flush(self): class DownloaderThread(QThread): download_finished = pyqtSignal() - on_progress = pyqtSignal(int) new_text = pyqtSignal(str) def __init__(self, link, start_time, end_time, save_path): @@ -45,7 +47,6 @@ def run(self): 'verbose': True, 'download_ranges': download_range_func(None, [(start_time, end_time)]), 'force_keyframes_at_cuts': True, - 'progress_hooks': [self.progress_hook], } # Redirect stdout and stderr @@ -75,14 +76,6 @@ def show_message_box(self, title, message): w.yesButton.setText('OK') w.exec() - def progress_hook(self, d): - if d['status'] == 'downloading': - total_bytes = d.get('total_bytes') or d.get('total_bytes_estimate') - downloaded_bytes = d.get('downloaded_bytes') - if total_bytes and downloaded_bytes: - progress = int(downloaded_bytes / total_bytes * 100) - self.on_progress.emit(progress) - def hhmmss_to_seconds(self, hhmmss): h, m, s = map(int, hhmmss.split(':')) return h * 3600 + m * 60 + s @@ -130,9 +123,6 @@ def __init__(self): self.main_layout.addSpacerItem(spacer_item_medium) - self.progress_bar = ProgressBar() - self.main_layout.addWidget(self.progress_bar) - self.main_layout.addSpacerItem(spacer_item_medium) # Console Output @@ -151,12 +141,17 @@ def __init__(self): self.download_button.clicked.connect(self.download) self.button_layout.addWidget(self.download_button) - # GIF Loading Screen - self.gif_layout = QHBoxLayout() - self.main_layout.addLayout(self.gif_layout) self.loading_label = QLabel() self.main_layout.addWidget(self.loading_label) + self.main_layout.addSpacerItem(spacer_item_medium) + self.main_layout.addSpacerItem(spacer_item_medium) + self.main_layout.addSpacerItem(spacer_item_medium) + self.main_layout.addSpacerItem(spacer_item_medium) + + disclaimer = QLabel("*** This feature uses YT-DLP and requires ffmpeg and will take slightly longer time to render, and the quality is also NOT adjustable.") + self.main_layout.addWidget(disclaimer) + self.count_layout = QHBoxLayout() self.download_list_widget = ListWidget() self.count_layout.addWidget(self.download_list_widget) @@ -175,14 +170,12 @@ def download(self): if save_path: thread = DownloaderThread(link, start_time, end_time, save_path) thread.download_finished.connect(self.show_download_finished_message) - thread.on_progress.connect(self.update_progress_bar) thread.new_text.connect(self.append_text) thread.start() - def update_progress_bar(self, value): - self.progress_bar.setValue(value) - + @pyqtSlot(str) def append_text(self, text): + logging.debug(f"Appending text: {text}") self.console_output.append(text) def init_timers(self): @@ -197,8 +190,8 @@ def init_timers(self): length = (video.length) length = self.seconds_to_hhmmss(length) self.end_time.setText(length) - except: - pass + except Exception as e: + logging.error(f"Failed to initialize timers: {e}", exc_info=True) def hhmmss_to_seconds(self, hhmmss): h, m, s = map(int, hhmmss.split(':')) diff --git a/youtility/downloader.py b/youtility/downloader.py index d4c5f73..cb7c9bd 100644 --- a/youtility/downloader.py +++ b/youtility/downloader.py @@ -46,12 +46,6 @@ def __init__(self, link, quality, download_captions, copy_thumbnail_link, dwnld_ self.filename = filename def run(self): - def get_gif(): - gifs = ["loading.gif", "loading_2.gif"] - gif = random.choice(gifs) - gif_path = "resources/misc/" + gif - return gif_path - caption_file_path = os.path.join(self.save_path, "captions.xml") self.loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -161,10 +155,13 @@ def __init__(self): self.quality_menu.setPlaceholderText("Video Quality (Enter link to view)") # self.quality_menu.addItems(["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p"]) self.quality_layout.addWidget(self.quality_menu) + self.options_layout.addSpacerItem(spacer_item_medium) self.thumbnail_url_checkbox = CheckBox('Copy Thumbnail Link', self) + self.audio_only_checkbox = CheckBox('Download Audio Only', self) self.audio_only_checkbox.stateChanged.connect(self.update_audio_format) + self.captions_checkbox = CheckBox('Download Captions', self) self.captions_checkbox.stateChanged.connect(self.trigger_captions_list) diff --git a/youtility/playlist.py b/youtility/playlist.py index 1becdd5..883f7a9 100644 --- a/youtility/playlist.py +++ b/youtility/playlist.py @@ -1,6 +1,8 @@ import json +import os import random import re +import subprocess import pytube.exceptions from PyQt6.QtCore import Qt, QThread, pyqtSignal @@ -9,7 +11,7 @@ QSpacerItem, QLabel, QListWidgetItem from pytube import Playlist from qfluentwidgets import (LineEdit, - CheckBox, ListWidget, TextEdit, PushButton) + CheckBox, ListWidget, TextEdit, PushButton, ComboBox) with open("resources/misc/config.json", "r") as themes_file: _themes = json.load(themes_file) @@ -21,7 +23,7 @@ class DownloaderThread(QThread): download_finished = pyqtSignal() def __init__(self, link, quality, dwnld_list_widget, quality_menu, - loading_label, main_window, save_path, progress_text, mp3_only, folder_path=None, + loading_label, main_window, save_path, progress_text, mp3_only, filename, audio_format, folder_path=None, copy_thumbnail_link=None): super().__init__() self.link = link @@ -35,6 +37,8 @@ def __init__(self, link, quality, dwnld_list_widget, quality_menu, self.progress_textbox = progress_text self.main_window = main_window self.mp3_only = mp3_only + self.audio_format = audio_format + self.filename = filename def run(self): def get_gif(): @@ -73,14 +77,22 @@ def get_gif(): self.progress_textbox.append( 'Downloading: {} with URL: {}'.format((video.title + " -audio"), video.watch_url)) self.progress_textbox.append("\n") - filtered_streams = video.streams.filter(only_audio=True).first() + filtered_streams.download(output_path=self.save_path, filename=(video.title + ".mp3")) + self.progress_textbox.append('Downloaded: {}'.format(video.title)) - # selected_stream = filtered_streams.filter(only_audio=True).first() - - filtered_streams.download(output_path=self.save_path) + if self.audio_format == "FLAC": + input_file = os.path.join(self.save_path, (video.title + ".mp3")).replace("\\", "/") + output_file = os.path.join(self.save_path, (video.title + ".flac")).replace("\\", "/") - self.progress_textbox.append('Downloaded: {}'.format(video.title)) + # Run the ffmpeg command to convert mp4 to flac + ffmpeg_command = f'ffmpeg -i "{input_file}" "{output_file}"' + try: + subprocess.run(ffmpeg_command, shell=True, check=True) + os.remove(input_file) + except subprocess.CalledProcessError as e: + print(f"Error during conversion: {e}") + self.list_item.setText((title + " - Download failed during conversion")) self.download_finished.emit() self.list_item.setText((title + " - Downloaded")) @@ -94,6 +106,7 @@ def __init__(self): spacer_item_medium = QSpacerItem(0, 20) self.setObjectName("Playlist") + self.audio_format_choice = ComboBox() self.main_layout = QVBoxLayout() self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) @@ -114,7 +127,7 @@ def __init__(self): self.options_layout = QHBoxLayout() self.main_layout.addLayout(self.quality_layout) self.main_layout.addLayout(self.options_layout) - self.quality_menu = QComboBox() + self.quality_menu = ComboBox() self.quality_menu.setPlaceholderText("Video Quality (Applies to all videos)") if progressive == "True": self.quality_menu.addItems(["720p", "480p", "360p", "240p", "144p"]) @@ -124,11 +137,13 @@ def __init__(self): self.options_layout.addSpacerItem(spacer_item_medium) self.thumbnail_url_checkbox = CheckBox('Copy Thumbnail URL', self) self.audio_only_checkbox = CheckBox('Download Audio Only', self) + self.audio_only_checkbox.stateChanged.connect(self.audio_format_init) self.options_group = QGroupBox("Additional Options") self.options_group_layout = QVBoxLayout(self.options_group) self.options_group_layout.addWidget(self.thumbnail_url_checkbox) self.options_group_layout.addWidget(self.audio_only_checkbox) + self.options_group_layout.addSpacerItem(spacer_item_medium) self.options_layout.addWidget(self.options_group) self.main_layout.addSpacerItem(spacer_item_small) @@ -146,15 +161,10 @@ def __init__(self): self.download_button.clicked.connect(self.download) self.button_layout.addWidget(self.download_button) - # GIF Loading Screen - self.gif_layout = QHBoxLayout() - self.main_layout.addLayout(self.gif_layout) self.loading_label = QLabel() self.main_layout.addWidget(self.loading_label) - # Progress Area self.count_layout = QHBoxLayout() - # Create a QListWidget to display downloading status self.download_list_widget = ListWidget() self.download_list_text = TextEdit() self.download_list_text.setReadOnly(True) @@ -165,6 +175,16 @@ def __init__(self): self.setLayout(self.main_layout) self.caption_list = None + def audio_format_init(self): + if self.audio_only_checkbox.isChecked(): + audio_formats = ["MP3", "FLAC"] + self.audio_format_choice = ComboBox() + self.audio_format_choice.setCurrentText(_themes["def-audio-format"]) + self.audio_format_choice.addItems(audio_formats) + self.options_group_layout.addWidget(self.audio_format_choice) + else: + self.audio_format_choice.hide() + def get_quality(self): url = self.link_entry.text() set_progressive = True @@ -198,8 +218,10 @@ def download(self): except pytube.exceptions.RegexMatchError: title = "Untitled" - # Open file dialog to get save path + audio_format = self.audio_format_choice.currentText() save_path, _ = QFileDialog.getSaveFileName(self, "Save file", title) + filename = os.path.basename(save_path) + filename_without_extension, _ = os.path.splitext(filename) self.downloader_thread = DownloaderThread( link=link, @@ -210,7 +232,9 @@ def download(self): quality_menu=self.quality_menu, main_window=self, progress_text=self.download_list_text, - mp3_only=mp3_only + mp3_only=mp3_only, + audio_format=audio_format, + filename=filename_without_extension ) self.downloader_thread.download_finished.connect(self.show_download_finished_message) self.downloader_thread.start() diff --git a/youtility/settings.py b/youtility/settings.py index 2168374..150ba01 100644 --- a/youtility/settings.py +++ b/youtility/settings.py @@ -1,7 +1,7 @@ import json -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QGroupBox, QPushButton, QComboBox -from qfluentwidgets import (LineEdit, StrongBodyLabel, MessageBox, CheckBox) +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QGroupBox, QPushButton, QComboBox, QSpacerItem +from qfluentwidgets import (LineEdit, StrongBodyLabel, MessageBox, CheckBox, ComboBox) with open("resources/misc/config.json", "r") as themes_file: _themes = json.load(themes_file) @@ -14,6 +14,10 @@ def __init__(self): self.initUI() def initUI(self): + + spacer_item_small = QSpacerItem(0, 10) + spacer_item_medium = QSpacerItem(0, 20) + layout = QVBoxLayout() layout.addStretch() @@ -36,15 +40,15 @@ def initUI(self): def_sub_format_label = StrongBodyLabel("Default Subtitle Format: ", self) pref_layout.addWidget(def_sub_format_label) - self.def_sub_format = QComboBox() + self.def_sub_format = ComboBox() self.def_sub_format.addItems(["SRT", "XML"]) self.def_sub_format.setCurrentText(_themes["def_sub_format"]) pref_layout.addWidget(self.def_sub_format) - set_progressive_label = StrongBodyLabel("Allow higher res downloads (audio may be missing): ", self) - pref_layout.addWidget(set_progressive_label) + pref_layout.addSpacerItem(spacer_item_medium) + self.set_progressive = CheckBox() - self.set_progressive.setText("Allow") + self.set_progressive.setText("Allow higher res downloads (audio may be missing): ") if _themes["progressive"] == "False": self.set_progressive.setChecked(True) else: