diff --git a/.gitignore b/.gitignore index 1eea57b..a4581de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,13 @@ +# Python cache and virtual enviroment. +__pycache__ +.venv # Build data. Build/Windows/Release -pornhub-dl.spec - -# Visual Studio project files. -PornHub Downloader.pyproj -PornHub Downloader.sln -.vs - +pornhub-dlp.spec # Downloaded videos and it's data. Downloads.ytdl Downloads - # Advertisement animation file. Advertisement.gif - -# Python cache. -__pycache__ \ No newline at end of file +# Libs. +yt-dlp \ No newline at end of file diff --git a/Build/Windows/build.bat b/Build/Windows/build.bat index 7b0e18a..fba2615 100644 --- a/Build/Windows/build.bat +++ b/Build/Windows/build.bat @@ -2,14 +2,14 @@ cd ..\..\ :: Сборка приложения. -pyinstaller --distpath %~dp0\Release --i icon.ico --version-file Build\Windows\metadata.txt --onefile main.py --name pornhub-dl +pyinstaller --distpath %~dp0\Release --i icon.ico --version-file Build\Windows\metadata.txt --onefile main.py --name pornhub-dlp :: Копирование в директорию сборки необходимых компонентов приложения. -xcopy /Y /I Source\GUI\Qt\Locales.json Build\Windows\Release\Source\GUI\Qt\ -xcopy /Y /I yt-dlp Build\Windows\Release\yt-dlp +xcopy /Y /I /S Locales Build\Windows\Release\Locales xcopy /Y Advertisement.gif Build\Windows\Release xcopy /Y icon.ico Build\Windows\Release xcopy /Y Settings.json Build\Windows\Release :: Удаление файлов сборки приложения. -rmdir /q /s Build\pornhub-dl \ No newline at end of file +rmdir /q /s Build\pornhub-dlp +del pornhub-dlp.spec \ No newline at end of file diff --git a/Build/Windows/metadata.txt b/Build/Windows/metadata.txt index 98d5602..8e0bdc7 100644 --- a/Build/Windows/metadata.txt +++ b/Build/Windows/metadata.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(1, 3, 2, 0), - prodvers=(1, 3, 2, 0), + filevers=(2, 0, 0, 0), + prodvers=(2, 0, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, @@ -15,12 +15,12 @@ VSVersionInfo( StringTable( u'040904B0', [StringStruct(u'CompanyName', u'DUB1401'), - StringStruct(u'FileDescription', u'PornHub video downloader.'), - StringStruct(u'FileVersion', u'1.3.2'), - StringStruct(u'LegalCopyright', u'Copyright © DUB1401. 2023-2024.'), - StringStruct(u'OriginalFilename', u'PornHub Downloader.exe'), - StringStruct(u'ProductName', u'PornHub Downloader'), - StringStruct(u'ProductVersion', u'1.3.2')]) + StringStruct(u'FileDescription', u'PornHub multiple video downloader.'), + StringStruct(u'FileVersion', u'2.0.0'), + StringStruct(u'LegalCopyright', u'Copyright © DUB1401. 2023-2025.'), + StringStruct(u'OriginalFilename', u'pornhub-dlp.exe'), + StringStruct(u'ProductName', u'PornHub-dlp'), + StringStruct(u'ProductVersion', u'2.0.0')]) ]) ] ) \ No newline at end of file diff --git a/LICENSE b/LICENSE index 35ae720..31ee2d9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright © 2023-2024. DUB1401. + Copyright © 2023-2025. DUB1401. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Locales/PornHub-dlp.pot b/Locales/PornHub-dlp.pot new file mode 100644 index 0000000..6f9e965 --- /dev/null +++ b/Locales/PornHub-dlp.pot @@ -0,0 +1,63 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2024-12-28 13:49+0300\n" +"PO-Revision-Date: 2024-12-28 13:48+0300\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.4\n" +"X-Poedit-Basepath: ../Source\n" +"X-Poedit-SearchPath-0: .\n" + +#: GUI/Qt/QtWindow.py:133 +msgid "Реклама" +msgstr "" + +#: GUI/Qt/QtWindow.py:139 +msgid "Очистить" +msgstr "" + +#: GUI/Qt/QtWindow.py:145 +msgid "Копировать вывод" +msgstr "" + +#: GUI/Qt/QtWindow.py:156 +msgid "Скачать" +msgstr "" + +#: GUI/Qt/QtWindow.py:161 +msgid "Вставьте сюда ссылки на видео" +msgstr "" + +#: GUI/Qt/QtWindow.py:174 +msgid "Вывод" +msgstr "" + +#: GUI/Qt/QtWindow.py:181 +msgid "Вставить ссылки" +msgstr "" + +#: GUI/Qt/QtWindow.py:193 +msgid "Настройки" +msgstr "" + +#: GUI/Qt/QtWindow.py:203 +msgid "Качество" +msgstr "" + +#: GUI/Qt/QtWindow.py:211 +msgid "Разрешение скачиваемых видео." +msgstr "" + +#: GUI/Qt/QtWindow.py:216 +msgid "По моделям" +msgstr "" + +#: GUI/Qt/QtWindow.py:217 +msgid "Сортировать видео по каталогам в соответствии с авторами." +msgstr "" diff --git a/Locales/en/LC_MESSAGES/PornHub-dlp.mo b/Locales/en/LC_MESSAGES/PornHub-dlp.mo new file mode 100644 index 0000000..8c9f942 Binary files /dev/null and b/Locales/en/LC_MESSAGES/PornHub-dlp.mo differ diff --git a/Locales/en/LC_MESSAGES/PornHub-dlp.po b/Locales/en/LC_MESSAGES/PornHub-dlp.po new file mode 100644 index 0000000..ad2c040 --- /dev/null +++ b/Locales/en/LC_MESSAGES/PornHub-dlp.po @@ -0,0 +1,63 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2024-12-28 13:49+0300\n" +"PO-Revision-Date: 2024-12-28 13:50+0300\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.4\n" +"X-Poedit-Basepath: ../Source\n" +"X-Poedit-SearchPath-0: .\n" + +#: GUI/Qt/QtWindow.py:133 +msgid "Реклама" +msgstr "" + +#: GUI/Qt/QtWindow.py:139 +msgid "Очистить" +msgstr "Clear" + +#: GUI/Qt/QtWindow.py:145 +msgid "Копировать вывод" +msgstr "" + +#: GUI/Qt/QtWindow.py:156 +msgid "Скачать" +msgstr "" + +#: GUI/Qt/QtWindow.py:161 +msgid "Вставьте сюда ссылки на видео" +msgstr "" + +#: GUI/Qt/QtWindow.py:174 +msgid "Вывод" +msgstr "" + +#: GUI/Qt/QtWindow.py:181 +msgid "Вставить ссылки" +msgstr "" + +#: GUI/Qt/QtWindow.py:193 +msgid "Настройки" +msgstr "" + +#: GUI/Qt/QtWindow.py:203 +msgid "Качество" +msgstr "" + +#: GUI/Qt/QtWindow.py:211 +msgid "Разрешение скачиваемых видео." +msgstr "" + +#: GUI/Qt/QtWindow.py:216 +msgid "По моделям" +msgstr "" + +#: GUI/Qt/QtWindow.py:217 +msgid "Сортировать видео по каталогам в соответствии с авторами." +msgstr "" diff --git a/README.md b/README.md index 62124b8..5abea60 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,70 @@ -# PornHub Downloader -**PornHub Downloader** – это приложение с графическим интерфейсом для массовой загрузки видео с [PornHub](https://www.pornhub.com/), поддерживающее сортировку по моделям и выбор предпочитаемого качества роликов. +# PornHub-dlp +**PornHub-dlp** – это приложение для массовой загрузки видео с [PornHub](https://www.pornhub.com/), поддерживающее сортировку по моделям и выбор предпочитаемого качества роликов. Доступны графический и консольный интерфейсы. ## Порядок установки и использования | Исполняемый файл Windows -1. Загрузить последний релиз исполняемой версии. Распаковать. -2. Запустить _pornhub-dl.exe_. Вставить в поле ввода список ссылок на видео и нажать кнопку загрузки. -3. Дождаться скачивания видео в папку _Downloads_, в директории скрипта. +1. Загрузить последний релиз для платформы Windows. Распаковать. +2. Запустить _pornhub-dlp.exe_. При первом запуске будет произведена загрузка зависимостей, что может занять некоторое время. +4. Вставить в поле ввода список ссылок на видео и нажать кнопку загрузки. +3. Дождаться скачивания видео в папку _Downloads_, в директории скрипта. ## Порядок установки и использования | Скрипт Python -1. Загрузить последний релиз скрипта. Распаковать. -2. Установить Python версии не старше 3.10. Рекомендуется добавить в PATH. -3. В среду исполнения установить следующие пакеты: [pyinstaller](https://github.com/pyinstaller/pyinstaller), [pyperclip](https://github.com/asweigart/pyperclip), [requests](https://github.com/psf/requests), [pyqt6](https://www.riverbankcomputing.com/software/pyqt). +1. Скачать и распаковать последний релиз. +2. Убедиться в доступности на вашем устройстве Python версии **3.12** или новее. +3. Открыть каталог со скриптом в терминале: можно воспользоваться командой `cd` или встроенными возможностями файлового менеджера. +4. Создать виртуальное окружение Python. ``` -pip install pyinstaller -pip install pyperclip -pip install requests -pip install pyqt6 +python -m venv .venv ``` -Либо установить сразу все пакеты при помощи следующей команды, выполненной из директории скрипта. +5. Активировать вирутальное окружение. +``` +# Для Windows. +.venv\Scripts\activate.bat + +# Для Linux или MacOS. +source .venv/bin/activate +``` +6. Установить зависимости. ``` pip install -r requirements.txt ``` -4. Запустить _main.py_. Вставить в поле ввода список ссылок на видео и нажать кнопку загрузки. -5. Дождаться скачивания видео в папку _Downloads_, в директории скрипта. +7. Разработчики [yt-dlp](https://github.com/yt-dlp) настоятельно рекомендуют установить библиотеку **ffmpeg** для поддержки расширенных сценариев загрузки и постпроцессинга. Ниже приведено несколько примеров. +```Bash +# Fedora +sudo dnf install ffmpeg-free +# Arch Linux +pacman -S ffmpeg +# Ubuntu +sudo apt install ffmpeg +``` +8. В вирутальном окружении указать для выполнения интерпретатором файл `main.py`. По умолчанию будет выбран графический режим с использованием библиотеки [PyQt6](https://pypi.org/project/PyQt6/). При желании воспользоваться CLI, передайте главному файлу соответствующую команду `main.py run -live`. # Скриншот -![Qt](Screenshots/Qt.png) +![image](https://github.com/user-attachments/assets/9fed05cd-5d2a-4f4b-9667-ceded975c03f) -# Сборка +# Сборка для Windows 1. Подготовить скрипт Python к работе согласно инструкции из порядка установки и использования. -2. Перейти в каталог _Build/Windows_, внутри директории скрипта. -3. Запустить файл _build.bat_ и дождаться завершения работы. -4. Исполняемая версия будет помещена по адресу _Build/Windows/Release_ вместе со всеми зависимостями. +2. Открыть терминал в директории скрипта и активировать виртуальное окружение. +```bat +.venv\Scripts\activate.bat +``` +3. Перейти в каталог _Build/Windows_ и запустить сценарий сборки _build.bat_. +```bat +cd Build\Windows +build.bat +``` +4. Исполняемая версия будет помещена по пути _Build/Windows/Release_ вместе со всеми зависимостями. ## Локализация -Для добавления сторонней локализации необходимо отредактировать файл [Locales.json](Source/GUI/Qt/Locales.json): в нём указываются списки используемых программой строк на целевом языке, ключём должен являться двухбуквенный тег языка в верхнем регистре по стандарту **ISO 639-1**. - -Доступные локализации: `EN`, `RU`. +В скрипт внедрена начальная поддержка локализации через средство [GNU gettext](https://www.gnu.org/software/gettext/manual/gettext.html), что позволяет любому принять участие в переводе. -## Версии поставляемых бинарных файлов +### Версии загружаемых бинарных файлов | Файл | Версия | Источник | |-------------|-------------------------------|--------------------------------------------------------------------| -| yt-dlp | _2023.12.30_ | [ссылка](https://github.com/yt-dlp/yt-dlp/releases/tag/2023.12.30) | +| yt-dlp / yt-dlp.exe | _2025.01.12_ | [ссылка](https://github.com/yt-dlp/yt-dlp/releases/tag/2025.01.12) | | ffmpeg.exe | _6.0 2023-03-04 (essentials)_ | [ссылка](https://github.com/GyanD/codexffmpeg/releases/tag/6.0) | | ffprobe.exe | _6.0 2023-03-04 (essentials)_ | [ссылка](https://github.com/GyanD/codexffmpeg/releases/tag/6.0) | # Благодарность -* [@yt-dlp](https://github.com/yt-dlp) – библиотека загрузки потокового видео. +* [yt-dlp](https://github.com/yt-dlp) – библиотека для скачивания видео из множества источников с широким дополнительным функционалом. -_Copyright © DUB1401. 2023-2024._ \ No newline at end of file +_Copyright © DUB1401. 2023-2025._ \ No newline at end of file diff --git a/Screenshots/Qt.png b/Screenshots/Qt.png deleted file mode 100644 index 1da8324..0000000 Binary files a/Screenshots/Qt.png and /dev/null differ diff --git a/Settings.json b/Settings.json index d8b5ad8..0afde93 100644 --- a/Settings.json +++ b/Settings.json @@ -1,7 +1,6 @@ -{ - "sort-by-models": false, - "downloads-directory": "", - "cuality": 5, - "debug": false, - "advertisement": "https://xn--80aaalhzvfe9b4a.xn--80asehdb/" +{ + "sorting": false, + "directory": "", + "quality": 2, + "advertisement": "" } \ No newline at end of file diff --git a/Source/Core.py b/Source/Core.py deleted file mode 100644 index 10d6007..0000000 --- a/Source/Core.py +++ /dev/null @@ -1 +0,0 @@ -прив \ No newline at end of file diff --git a/Source/Core/Application.py b/Source/Core/Application.py new file mode 100644 index 0000000..d6bbe57 --- /dev/null +++ b/Source/Core/Application.py @@ -0,0 +1,70 @@ +from Source.UI.Qt.QtWindow import QtWindow +from Source.UI.LiveCLI import LiveCLI + +from PyQt6.QtWidgets import QApplication +from PyQt6 import QtGui + +import enum +import sys + +#==========================================================================================# +# >>>>> ДОПОЛНИТЕЛЬНЫЕ ТИПЫ ДАННЫХ <<<<< # +#==========================================================================================# + +class Interfaces(enum.Enum): + """Типы интерфейсов.""" + + GTK = "gtk" + Qt = "qt" + LiveCLI = "live" + +#==========================================================================================# +# >>>>> ОСНОВНОЙ КЛАСС <<<<< # +#==========================================================================================# + +class Application: + """Менеджер запуска приложения.""" + + #==========================================================================================# + # >>>>> ИНИЦИАЛИЗАТОРЫ ОКНА <<<<< # + #==========================================================================================# + + def __InitializeLiveCLI(self): + """Инициализирует Live CLI режим приложения.""" + + LiveCLI(self.__Settings).run() + + def __InitializeQt(self): + """Инициализирует приложение Qt.""" + + Application = QApplication(sys.argv) + Application.setWindowIcon(QtGui.QIcon("icon.ico")) + Window = QtWindow(self.__Settings) + Window.show() + Application.exec() + + #==========================================================================================# + # >>>>> ПУБЛИЧНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __init__(self, settings: dict): + """ + Менеджер запуска приложения. + settings — словарь глобальных настроек. + """ + #---> Генерация динамических атрибутов. + #==========================================================================================# + self.__Settings = settings.copy() + + def run(self, toolkit: Interfaces | None = None): + """ + Запускает приложение. + toolkit — выбранный интерфейс. + """ + + toolkit = toolkit or Interfaces.LiveCLI + + { + Interfaces.LiveCLI: self.__InitializeLiveCLI, + Interfaces.Qt: self.__InitializeQt + }[toolkit]() \ No newline at end of file diff --git a/Source/Core/Downloader.py b/Source/Core/Downloader.py new file mode 100644 index 0000000..87679f2 --- /dev/null +++ b/Source/Core/Downloader.py @@ -0,0 +1,167 @@ +from dublib.Engine.Bus import ExecutionError, ExecutionStatus +from dublib.Methods.Filesystem import NormalizePath + +import urllib.request +import subprocess +import zipfile +import json +import sys +import os + +import re + +class VideoDownloader: + """Загрузчик видео.""" + + #==========================================================================================# + # >>>>> ПРИВАТНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __CheckLibs(self): + """Проверяет, загружены ли нужные библиотеки.""" + + if not os.path.exists(f"yt-dlp/{self.__LibName}"): + if not os.path.exists("yt-dlp"): os.makedirs("yt-dlp") + print("Downloading yt-dlp... ", end = "", flush = True) + urllib.request.urlretrieve(f"https://github.com/yt-dlp/yt-dlp/releases/download/2025.01.12/{self.__LibName}", f"yt-dlp/{self.__LibName}") + print("Done.") + + if sys.platform == "linux": + print("Making yt-dlp executable... ", end = "") + os.system("chmod u+x yt-dlp/yt-dlp") + print("Done.") + + if sys.platform == "win32" and not os.path.exists("yt-dlp/ffmpeg.exe"): + print("Downloading ffmpeg 7.1 Essentials (Windows build)... ", end = "", flush = True) + urllib.request.urlretrieve("https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-essentials_build.zip", "yt-dlp/ffmpeg-essentials.zip") + print("Done.") + + with zipfile.ZipFile("yt-dlp/ffmpeg-essentials.zip", "r") as ZipReader: + print("Exracting files...", flush = True) + with open("yt-dlp/ffmpeg.exe", "wb") as FileWriter: FileWriter.write(ZipReader.read("ffmpeg-7.1-essentials_build/bin/ffmpeg.exe")) + print("ffmpeg.exe") + with open("yt-dlp/ffprobe.exe", "wb") as FileWriter: FileWriter.write(ZipReader.read("ffmpeg-7.1-essentials_build/bin/ffprobe.exe")) + print("ffprobe.exe") + print("Done.") + os.remove("yt-dlp/ffmpeg-essentials.zip") + print("Temporary files removed.") + + #==========================================================================================# + # >>>>> ПУБЛИЧНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __init__(self): + + #---> Генерация динамических атрибутов. + #==========================================================================================# + self.__IsSortingEnabled = None + self.__DownloadsDirectory = "Downloads" + self.__LibName = "yt-dlp.exe" if sys.platform == "win32" else "yt-dlp" + + self.__CheckLibs() + + def check_link(self, link: str) -> bool: + """ + Проверяет, подходит ли ссылка по формату. + link – ссылка на видео. + """ + + return bool(re.match(r"https:\/\/.{0,4}?pornhub\.com\/view_video\.php\?viewkey=\S+\b", link)) + + def download_video(self, link: str, quality: int | str) -> ExecutionStatus: + """ + Возвращает словарь данных видео. + link – ссылка на видео;\n + quality – предпочитаемое качество видео. + """ + + Status = ExecutionStatus(0) + quality = self.get_video_height(quality) + VideoInfoStatus = self.get_video_info(link) + + if VideoInfoStatus.code != 0: return VideoInfoStatus + if self.__DownloadsDirectory == "Downloads" and not os.path.exists(self.__DownloadsDirectory): os.makedirs(self.__DownloadsDirectory) + + FfmpegPath = "" + + if sys.platform == "win32": FfmpegPath = "--ffmpeg-location yt-dlp/ffmpeg.exe" + + try: + Data = VideoInfoStatus.value + Filename = Data["filename"] + Uploader = "" + if self.__IsSortingEnabled: Uploader = "/" + Data["uploader"] + Path = NormalizePath(f"yt-dlp/{self.__LibName}") + ExitCode = os.system(f"{Path} -f \"bv*[height<={quality}]+ba/b[height<={quality}]\" -o \"{self.__DownloadsDirectory}{Uploader}/{Filename}\" {link} {FfmpegPath}") + if ExitCode != 0: Status = ExecutionError(ExitCode, "Unable to download video.") + + except Exception as ExceptionData: + Status = ExecutionError(-1, str(ExceptionData)) + + return Status + + def enable_sorting(self, status: bool): + """ + Переключает сортировку по каталогам в соответствии с автором видео. + status – статус использования. + """ + + self.__IsSortingEnabled = status + + def get_video_height(self, quality: int | str) -> int | None: + """ + Возвращает высоту кадра видео. + quality – предпочитаемое качество видео. + """ + + quality = str(quality) + + QualityTypes = { + "4k": 4096, + "2k": 2048, + "fullhd": 1080, + "hd": 720, + "480p": 480, + "360p": 360, + "240p": 240 + } + Quality = None + + if quality.isdigit() and len(quality) == 1: + Index = int(quality) + Quality = tuple(QualityTypes.values())[Index] + + elif quality.isdigit(): + Quality = int(quality) + + elif quality.lower() in QualityTypes.keys(): + Quality = QualityTypes[quality.lower()] + + return Quality + + def get_video_info(self, link: str) -> ExecutionStatus: + """ + Возвращает словарь данных видео. + link – ссылка на видео. + """ + + Status = ExecutionStatus(0) + + try: + Path = NormalizePath(f"yt-dlp/{self.__LibName}") + Output = subprocess.getoutput(f"{Path} --dump-json {link}") + Status.value = json.loads(Output) + + except Exception as ExceptionData: + Status = ExecutionError(-1, str(ExceptionData), Output) + + return Status + + def set_downloads_directory(self, path: str): + """ + Задаёт путь к каталогу для скачивания видео. + path – путь. + """ + + if not os.path.exists(): raise FileNotFoundError(path) + self.__DownloadsDirectory = NormalizePath(path) \ No newline at end of file diff --git a/Source/GUI/GTK4/main.py b/Source/GUI/GTK4/main.py deleted file mode 100644 index 649e435..0000000 --- a/Source/GUI/GTK4/main.py +++ /dev/null @@ -1,34 +0,0 @@ -import gi - -# Запрос требуемых версий библиотек. -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -from gi.repository import Gtk, Adw - -from Source.MainWindow import MainWindow - -import sys - -# Приложение Adwaita. -class MyApp(Adw.Application): - - def __OnActivate(self, Application: Adw.Application): - # Инициализация главного окна. - self.win = MainWindow(application = Application) - # Представление окна. - self.win.present() - - # Конструктор. - def __init__(self, **kwargs): - # Наследование конструктора базового класса. - super().__init__(**kwargs) - # Подключение: при активации приложения. - self.connect("activate", self.__OnActivate) - -# Если точка входа – приложение. -if __name__ == "__main__": - # Создание приложения. - Application = MyApp() - # Выполнение приложения. - exit(Application.run(sys.argv)) \ No newline at end of file diff --git a/Source/GUI/Qt/Locale.py b/Source/GUI/Qt/Locale.py deleted file mode 100644 index cd73df5..0000000 --- a/Source/GUI/Qt/Locale.py +++ /dev/null @@ -1,30 +0,0 @@ -from dublib.Methods import ReadJSON - -import ctypes -import locale -import sys - -# Словарь локализаций. -LOCALES = ReadJSON("Source/GUI/Qt/Locales.json") - -# Текущая локализация. -CURRENT_LOCALE = LOCALES["EN"] -# Тег текущего языка. -LanguageTag = None - -# Если устройство работает под управлением ОС семейства Linux. -if sys.platform in ["linux", "linux2"]: - # Получение тега текущего языка. - LanguageTag = locale.getlocale()[0].split('_')[0].upper() - -# Если устройство работает под управлением ОС семейства Windows. -elif sys.platform == "win32": - # Получение сведений о системе Windows. - WinDLL = ctypes.windll.kernel32 - WinDLL.GetUserDefaultUILanguage() - # Получение тега текущего языка. - LanguageTag = locale.windows_locale[WinDLL.GetUserDefaultUILanguage()].split('_')[0].upper() - -# Если существует локализация, переключиться на неё. -if LanguageTag in LOCALES.keys(): - CURRENT_LOCALE = LOCALES[LanguageTag] \ No newline at end of file diff --git a/Source/GUI/Qt/Locales.json b/Source/GUI/Qt/Locales.json deleted file mode 100644 index de2c70d..0000000 --- a/Source/GUI/Qt/Locales.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "EN": [ - "Advertisement", - "Clear", - "Copy output", - "Download", - "Paste here links to videos", - "Output logs", - "Paste links", - "Settings", - "Cuality", - "Resolution of the downloaded video.", - "Theme", - "Style of the program window.", - "Sort by models" - ], - "RU": [ - "Реклама", - "Очистить", - "Копировать логи", - "Скачать", - "Вставьте сюда ссылки на видео", - "Логи", - "Вставить ссылки", - "Настройки", - "Качество", - "Разрешение загружаемого видео.", - "Тема", - "Стиль окна программы.", - "По моделям" - ] -} \ No newline at end of file diff --git a/Source/GUI/Qt/QtWindow.py b/Source/GUI/Qt/QtWindow.py deleted file mode 100644 index f0d2678..0000000 --- a/Source/GUI/Qt/QtWindow.py +++ /dev/null @@ -1,467 +0,0 @@ -from PyQt6.QtWidgets import QApplication, QStyleFactory -from PyQt6 import QtGui -from PyQt6.QtWidgets import ( - QApplication, - QCheckBox, - QComboBox, - QGroupBox, - QLabel, - QMainWindow, - QProgressBar, - QPushButton, - QStyleFactory, - QTextEdit, - QVBoxLayout -) -from PyQt6.QtGui import QCursor, QDesktopServices, QMovie, QTextCursor -from Source.GUI.Qt.QLabelAdvertisement import QLabelAdvertisement -from PyQt6.QtCore import Qt,QSize, QThread, QUrl -from Source.GUI.Qt.Locale import CURRENT_LOCALE -from Source.GUI.Qt.yt_dlp import yt_dlp - -import pyperclip -import json -import time -import os -import re - -# Обработчик взаимодействий с главным окном. -class QtWindow(QMainWindow): - - #==========================================================================================# - # >>>>> СВОЙСТВА <<<<< # - #==========================================================================================# - - # Список поддерживаемых разрешений. - __Resolutions = ["4096", "2048", "1080", "720", "480", "240"] - # Поток загрузки видео. - __DownloadingThread = None - # Список URL видео. - __VideoLinks = list() - # Экземпляр приложения. - __Application = None - # Время начала загрузки. - __StartTime = None - # Глобальные настройки. - __Settings = None - # Словарь важных значений. - __ComData = None - # Индекс обрабатываемого видео. - __VideoIndex = 0 - - #==========================================================================================# - # >>>>> ОБРАБОТЧИКИ СИГНАЛОВ <<<<< # - #==========================================================================================# - - # Очищает все данные процесса. - def __Clear(self): - self.Input.clear() - self.Output.clear() - self.ProgressBar.setValue(0) - self.__VideoLinks = list() - - # Копирует содержимое псевдоконсоли в буфер обмена. - def __CopyOutput(self): - pyperclip.copy(self.Output.toPlainText()) - - # Запускает потоковый обработчик загрузки видео. - def __DownloadVideos(self): - # Очистка содержимого псевдоконсоли. - self.Output.clear() - # Удалить повторяющиеся ссылки. - self.__RemoveRepeatedLinks() - # Деактивация управляющих элементов. - self.Clear.setEnabled(False) - self.Download.setEnabled(False) - self.Output.setReadOnly(True) - self.Paste.setEnabled(False) - # Получение списка URL видео. - self.__VideoLinks = list(filter(None, self.Input.toPlainText().strip().split('\n'))) - # Настройка индикатора прогресса. - self.ProgressBar.setMaximum(len(self.__VideoLinks)) - self.ProgressBar.setValue(0) - self.ProgressBar.setVisible(True) - # Запуск загрузчика. - self.__StartDownloading() - - # Форматирует поле ввода. - def __FormatInput(self): - # Получение содержимого поля ввода. - InputText = self.Input.toPlainText() - # Разбитие содержимого на отдельные строки. - InputLines = InputText.split('\n') - # Обработанные строки. - FormattedLines = list() - # Результирующие строки. - ResultLines = list() - # Результирующий текст. - ResultText = None - - # Для каждой строки. - for Line in InputLines: - # Попытаться разбить строку по вхождению протокола. - Bufer = Line.replace("https", "\nhttps").strip("\n \t") - # Сохранение разбитых строк. - FormattedLines += Bufer.split('\n') - - # Для каждой обработанной строки. - for Line in FormattedLines: - # Очистка строки от аргументов. - Line = Line.split('&')[0] - - # Если строка соответствует шаблону, то сохранить её. - if bool(re.match(r"https:\/\/rt\.pornhub\.com\/view_video\.php\?viewkey=\S+\b", Line)) == True: - ResultLines.append(Line) - - # Построение результирующего текста. - ResultText = "\n".join(ResultLines) + "\n" - - # Если результирующий текст не содержит символов. - if ResultText.strip("\n \t") == "": - # Обнулить результирующий текст. - ResultText = "" - # Деактивировать кнопку загрузки. - self.Download.setEnabled(False) - - elif self.__VideoIndex == 0: - # Активировать кнопку загрузки. - self.Download.setEnabled(True) - - # Если текст отличается, то поместить отформатированный список ссылок в поле ввода. - if ResultText != self.Input.toPlainText(): - self.Input.setText(ResultText) - - # Перемещение каретки в конец поля ввода. - self.Input.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor) - - # Открывает в браузере рекламируемую страницу. - def __OpenAdvertisement(self): - QDesktopServices.openUrl(QUrl(self.__Settings["advertisement"])) - - # Открывает в браузере страницу проекта на GitHub. - def __OpenGitHub(self): - QDesktopServices.openUrl(QUrl("https://github.com/DUB1401/PornHub-Downloader")) - - # Добавляет ссылку из буфера обмена. - def __Paste(self): - self.Input.setText(self.Input.toPlainText() + pyperclip.paste().strip("\n \t") + "\n") - - # Сохраняет настройку. - def __SaveSetting(self, Key: str, Value): - # Обновление значения поля настройки. - self.__Settings[Key] = Value - # Копирование настроек. - Bufer = self.__Settings.copy() - - # Удаление пути к стандартной папке загрузок. - if Bufer["downloads-directory"] == os.getcwd() + "/Downloads": - Bufer["downloads-directory"] = "" - - # Сохранение настройки. - with open("Settings.json", "w", encoding = "utf-8") as FileWrite: - json.dump(Bufer, FileWrite, ensure_ascii = False, indent = '\t', separators = (",", ": ")) - - # Прокручивает псевдоконсоль вниз. - def __ScrollOutputToEnd(self): - self.Output.moveCursor(QTextCursor.MoveOperation.End) - - #==========================================================================================# - # >>>>> МЕТОДЫ <<<<< # - #==========================================================================================# - - # Создание группы GUI: реклама. - def __CreatAdvertisementGroupUI(self): - # Слой рекламного блока. - AdvertisementLayout = QVBoxLayout() - # Установка слоя для элемента QGroupBox. - self.AdsBox.setLayout(AdvertisementLayout) - - # Создание объекта GUI: рекламная анимация. - AdvertisementGIF = QMovie("Advertisement.gif") - AdvertisementGIF.setScaledSize(QSize(180, 260)) - AdvertisementGIF.start() - - # Создание объекта GUI: рекламная ссылка. - Advertisement = QLabelAdvertisement(self) - Advertisement.clicked.connect(self.__OpenAdvertisement) - Advertisement.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) - Advertisement.setMovie(AdvertisementGIF) - - # Добавление объекта GUI в слой. - AdvertisementLayout.addWidget(Advertisement) - - # Создание базовых элементов GUI. - def __CreateBasicUI(self): - - # Создание объекта GUI: контейнер рекламы. - self.AdsBox = QGroupBox(self) - self.AdsBox.move(870, 170) - self.AdsBox.resize(200, 300) - self.AdsBox.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.AdsBox.setTitle(f"📰 {CURRENT_LOCALE[0]}") - - # Создание объекта GUI: кнока очистки вывода. - self.Clear = QPushButton(self) - self.Clear.clicked.connect(self.__Clear) - self.Clear.move(870, 590) - self.Clear.resize(200, 40) - self.Clear.setText(f"🧹 {CURRENT_LOCALE[1]}") - - # Создание объекта GUI: кнока копирования вывода. - self.Copy = QPushButton(self) - self.Copy.clicked.connect(self.__CopyOutput) - self.Copy.move(870, 540) - self.Copy.resize(200, 40) - self.Copy.setText(f"📋 {CURRENT_LOCALE[2]}") - - # Создание объекта GUI: подпись защиты прав. - self.Copyright = QLabel(self) - self.Copyright.setText(self.__ComData["copyright"]) - self.Copyright.move(10, 690) - self.Copyright.adjustSize() - - # Создание объекта GUI: кнока загрузки. - self.Download = QPushButton(self) - self.Download.clicked.connect(self.__DownloadVideos) - self.Download.move(870, 640) - self.Download.resize(200, 40) - self.Download.setEnabled(False) - self.Download.setText(f"⬇ {CURRENT_LOCALE[3]}") - - # Создание объекта GUI: поле ввода ссылок на видео. - self.Input = QTextEdit(self) - self.Input.move(10, 10) - self.Input.resize(850, 420) - self.Input.setPlaceholderText(CURRENT_LOCALE[4]) - self.Input.textChanged.connect(self.__FormatInput) - - # Создание объекта GUI: ссылка на GitHub. - self.Link = QLabel(self) - self.Link.linkActivated.connect(self.__OpenGitHub) - self.Link.setText("GitHub") - self.Link.adjustSize() - self.Link.move(1080 - self.Link.size().width() - 10, 690) - - # Создание объекта GUI: поле псевдоконсольного вывода. - self.Output = QTextEdit(self) - self.Output.move(10, 490) - self.Output.resize(850, 190) - self.Output.setReadOnly(True) - self.Output.setPlaceholderText(CURRENT_LOCALE[5]) - self.Output.textChanged.connect(self.__ScrollOutputToEnd) - - # Создание объекта GUI: кнока добавления ссылки в очередь. - self.Paste = QPushButton(self) - self.Paste.clicked.connect(self.__Paste) - self.Paste.move(870, 490) - self.Paste.resize(200, 40) - self.Paste.setText(f"📖 {CURRENT_LOCALE[6]}") - - # Создание объекта GUI: индикатор прогресса. - self.ProgressBar = QProgressBar(self) - self.ProgressBar.move(10, 450) - self.ProgressBar.resize(850, 20) - self.ProgressBar.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.ProgressBar.setValue(0) - - # Создание объекта GUI: контейнер настроек. - self.SettingsBox = QGroupBox(self) - self.SettingsBox.move(870, 10) - self.SettingsBox.resize(200, 160) - self.SettingsBox.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.SettingsBox.setTitle(f"🔧 {CURRENT_LOCALE[7]}") - - # Создание группы GUI: настройки. - def __CreateSettingsGroupUI(self): - # Слой настроек. - SettingsLayout = QVBoxLayout() - # Установка слоя для элемента QGroupBox. - self.SettingsBox.setLayout(SettingsLayout) - - #---> Создание объектов GUI. - #==========================================================================================# - - # Создание объекта GUI: заголовок выбора качества. - CualityTitle = QLabel(self) - CualityTitle.setText(f"{CURRENT_LOCALE[8]}:") - CualityTitle.adjustSize() - - # Создание объекта GUI: селектор качества. - CualitySelecter = QComboBox(self) - CualitySelecter.addItems(["4K", "2K", "1080", "720", "480", "360", "240"]) - CualitySelecter.setCurrentIndex(self.__Settings["cuality"]) - CualitySelecter.currentIndexChanged.connect(lambda: self.__SaveSetting("cuality", CualitySelecter.currentIndex())) - CualitySelecter.resize(180, 40) - CualitySelecter.setToolTip(CURRENT_LOCALE[9]) - - # Создание объекта GUI: флаговая кнопка включения сортировки по моделям. - SortByModel = QCheckBox(self) - SortByModel.clicked.connect(lambda: self.__SaveSetting("sort-by-models", SortByModel.isChecked())) - SortByModel.setChecked(self.__Settings["sort-by-models"]) - SortByModel.setText(CURRENT_LOCALE[12]) - SortByModel.setToolTip("Sorting videos into the folders by uploader nickname.") - SortByModel.adjustSize() - - #---> Добавление объектов GUI в слой. - #==========================================================================================# - SettingsLayout.addWidget(SortByModel) - SettingsLayout.addWidget(CualityTitle) - SettingsLayout.addWidget(CualitySelecter) - SettingsLayout.addStretch() - - # Обрабатывает завершение загрузки видео. - def __EndDownloading(self, ExitCode: int): - # Инкремент индекса загружаемого видео. - self.__VideoIndex += 1 - # Увеличение процента заполнение в индикаторе прогресса. - self.ProgressBar.setValue(self.__VideoIndex) - - # Если загрузка завершилась успешно, то вывести в псевдоконсоль время выполнения, иначе вывести ошибку. - if ExitCode == 0: - self.Print("Done! (" + self.__FormatExecutionTime(round(float(time.time() - self.__StartTime), 2)) + ")", True) - - else: - self.Print("Error! See CLI output for more information.", True) - - # Удаление первого в очереди URL. - self.Input.setText('\n'.join(self.Input.toPlainText().split('\n')[1:])) - - # Если остались незагруженные видео. - if self.__VideoIndex < len(self.__VideoLinks): - # Начать загрузку следующего видео. - self.__StartDownloading() - - else: - # Вывод в псевдоконсоль: работа завершена. - self.Print("Complete.") - # Активация управляющих элементов. - self.Clear.setEnabled(True) - self.Download.setEnabled(True) - self.Output.setReadOnly(False) - self.Paste.setEnabled(True) - # Обнуление индекса загружаемого видео. - self.__VideoIndex = 0 - # Очистка поля ввода. - self.Input.setText("") - - # Форматирует время выполнения задачи. - def __FormatExecutionTime(self, ExecutionTime: float) -> str: - # Результат форматирования. - Result = "" - # Получение количества прошедших минут. - ElapsedMinutes = int(ExecutionTime / 60.0) - - # Если прошло больше минуты. - if ElapsedMinutes > 0: - # Добавление количества прошедших минут в формат. - Result += str(ElapsedMinutes) + " minutes " - # Вычисление оставшихся секунд. - ElapsedSeconds = round(ExecutionTime % 60.0, 2) - # Добавление количества оставшихся секунд в формат. - Result += str(ElapsedSeconds) + " seconds" - - else: - # Форматирование прошедших секунд. - Result += str(ExecutionTime) + " seconds" - - return Result - - # Удаляет повторяющиеся ссылки. - def __RemoveRepeatedLinks(self): - # Получение содержимого поля ввода. - InputText = self.Input.toPlainText() - # Разбитие содержимого на отдельные строки. - InputLines = InputText.split('\n') - # Удаление дубликатов ссылок. - ResultLines = [*set(InputLines)] - - # Если количество ссылок отличается от изначального. - if len(InputLines) != len(ResultLines): - # Построение результирующего текста. - ResultText = "\n".join(ResultLines) + "\n" - # Поместить отсортированный список ссылок в поле ввода. - self.Input.setText(ResultText) - # Вычисление количества удалённых повторов. - RepeatedLinksCount = len(InputLines) - len(ResultLines) - # Вывод в псевдоконсоль: количество удалённых повторов. - self.Print("Removed identical links count: " + str(RepeatedLinksCount), True) - - # Обрабатывает начало загрузки видео. - def __StartDownloading(self): - # Директория загрузки. - SaveDirectory = self.__Settings["downloads-directory"] - - # Сохранение времени начала загрузки. - self.__StartTime = time.time() - - # Если остались незагруженные видео. - if self.__VideoIndex < len(self.__VideoLinks): - # Текущая ссылка. - CurrentLink = self.__VideoLinks[self.__VideoIndex] - # Вывод в псевдоконсоль: начало загрузки. - self.Print("Downloading: " + str(self.__VideoIndex + 1) + " / " + str(len(self.__VideoLinks))) - # Вывод в псевдоконсоль: URL текущей задачи. - self.Print("Current task: " + self.__VideoLinks[self.__VideoIndex] + "") - # Настройка и запуск обработчика библиотеки в отдельном потоке. - self.Subprocess = yt_dlp(SaveDirectory, CurrentLink, self.__Settings["sort-by-models"], self.__Resolutions[self.__Settings["cuality"]]) - self.Subprocess.moveToThread(self.__DownloadingThread) - self.__DownloadingThread.quit() - self.__DownloadingThread.started.connect(self.Subprocess.run) - self.Subprocess.finished.connect(self.__EndDownloading) - self.Subprocess.finished.connect(self.__DownloadingThread.quit) - self.__DownloadingThread.start() - - # Конструктор: задаёт экземпляр приложения, словарь важных значений и глобальные настройки. - def __init__(self, Application: QApplication, ComData: dict, Settings: dict): - # Обеспечение доступа к оригиналам наследованных методов. - super().__init__() - - #---> Генерация свойств. - #==========================================================================================# - self.__ComData = ComData - self.__Settings = Settings - self.__DownloadingThread = QThread() - self.__Application = Application - - #---> Инициализация графического интерфейса. - #==========================================================================================# - - # Настройка окна. - self.setFixedSize(1080, 720) - self.setWindowTitle("PornHub Downloader v" + ComData["version"]) - - # Создание базовых элементов и групп GUI. - self.__CreateBasicUI() - self.__CreateSettingsGroupUI() - - # Если включено отображение рекламы. - if self.__Settings["advertisement"] != "" and self.__Settings["advertisement"] != None and os.path.exists("Advertisement.gif"): - # Генерация рекламного блока. - self.__CreatAdvertisementGroupUI() - - else: - # Отключение видимости рекламного блока. - self.AdsBox.setVisible(False) - - # Если включён режим отладки, то добавить две тестовые ссылки в поле ввода. - if self.__Settings["debug"] == True: - self.Input.setText("https://rt.pornhub.com/view_video.php?viewkey=ph5c7ad8fa8b178\nhttps://rt.pornhub.com/view_video.php?viewkey=ph5d302376d91be\n") - - # Отправляет сообщение в псевдоконсоль. - def Print(self, Message: str, Separator: bool = False): - # Содержимое псевдоконсоли. - Text = None - - # Если псевдоконсоль пуста, то задать пустой текст (исправляет наличие пустой строки). - if self.Output.toPlainText() == "": - Text = "" - - else: - Text = self.Output.toHtml() - - # Если указано аргументами, то добавить разделитель после сообщения. - if Separator == True: - Message += "
==========================================================================================" - - # Добавление сообщения в конец. - self.Output.setHtml(Text + Message) \ No newline at end of file diff --git a/Source/GUI/Qt/yt_dlp.py b/Source/GUI/Qt/yt_dlp.py deleted file mode 100644 index 049a9f8..0000000 --- a/Source/GUI/Qt/yt_dlp.py +++ /dev/null @@ -1,83 +0,0 @@ -from PyQt6.QtCore import QObject, pyqtSignal - -import subprocess -import json -import sys -import os - -# Потоковый обработчик взаимодейтсвий с библиотекой pornhub_dl. -class yt_dlp(QObject): - - #==========================================================================================# - # >>>>> СВОЙСТВА <<<<< # - #==========================================================================================# - - # Текущая директория. - __CurrentDirectory = None - # Сигнал: завершение потока. Содержит: завершающий код вызова библиотеки. - finished = pyqtSignal(int) - # Состояние: требуется ли сортировать видео по никам загрузивших. - __SortByUploader = None - # Директория сохранения. - __SaveDirectory = None - # Выбранное качество. - __Cuality = None - # Дамп данных видео. - __Dump = None - # Ссылка на видео. - __Link = None - - #==========================================================================================# - # >>>>> МЕТОДЫ <<<<< # - #==========================================================================================# - - # Конструктор: задаёт команду для выполнения. - def __init__(self, SaveDirectory: str, Link: str, SortByUploader: bool, Cuality: str): - # Обеспечение доступа к оригиналам наследованных методов. - super().__init__() - - #---> Генерация свойств. - #==========================================================================================# - self.__CurrentDirectory = os.getcwd().replace("\\", "/") - self.__SaveDirectory = SaveDirectory - self.__Link = Link - self.__SortByUploader = SortByUploader - self.__Cuality = Cuality - - # Возвращает словарь описания предварительного процессирования yt-dlp. - def dump(self) -> dict: - # Расширение файла. - Type = ".exe" if sys.platform == "win32" else "" - # Дампирование видео. - Result = subprocess.getoutput(f"{self.__CurrentDirectory}/yt-dlp/yt-dlp{Type} --dump-json {self.__Link}") - - # Если нет ошибки дампирования. - if Result.startswith("ERROR") == False: - # Получение дампа через вывод yt-dlp. - self.__Dump = json.loads(Result) - - return self.__Dump - - # Запускает выполнение команды. - def run(self): - # Дампирование видео. - Result = self.dump() - # Код выполнения. - ExitCode = 1 - - # Если дампирование успешно. - if Result != None: - # Получение имени файла и расширения. - Filename = self.__Dump["filename"] - # Получение имени загрузившего для сортировки. - Uploader = "/" + self.__Dump["uploader"] - - # Если сортировка отключена, обнулить загрузившего. - if self.__SortByUploader == False: - Uploader = "" - - # Выполнение команды. - ExitCode = os.system(f"{self.__CurrentDirectory}/yt-dlp/yt-dlp -f \"bv*[height<={self.__Cuality}]+ba/b[height<={self.__Cuality}]\" -o \"{self.__SaveDirectory}{Uploader}/{Filename}\" {self.__Link}") - - # Генерация сигнала с завершающим кодом приложения. - self.finished.emit(ExitCode) \ No newline at end of file diff --git a/Source/GUI/GTK4/Source/MainWindow.py b/Source/UI/GTK4/Source/MainWindow.py similarity index 97% rename from Source/GUI/GTK4/Source/MainWindow.py rename to Source/UI/GTK4/Source/MainWindow.py index 403a0d7..dd305b5 100644 --- a/Source/GUI/GTK4/Source/MainWindow.py +++ b/Source/UI/GTK4/Source/MainWindow.py @@ -1,122 +1,122 @@ -import gi - -# Запрос требуемых версий библиотек. -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -from gi.repository import Gtk, Adw - -# Главное окно. -class MainWindow(Gtk.ApplicationWindow): - - #==========================================================================================# - # >>>>> МЕТОДЫ ВЗАИМОДЕЙСТВИЯ ИНТЕРФЕЙСА <<<<< # - #==========================================================================================# - - # Изменяет статус загрузки. - def __ChangeDownloadingStatus(self): - - # Если нажата кнопка начала загрузки. - if "🌎" in self.__Button_Downloading.get_label(): - # Изменение статуса. - self.__IsDownloading = True - # Изменение текста кнопки. - self.__Button_Downloading.set_label("🟥 Stop") - else: - # Изменение статуса. - self.__IsDownloading = False - # Изменение текста кнопки. - self.__Button_Downloading.set_label("🌎 Start") - - #==========================================================================================# - # >>>>> МЕТОДЫ ГЕНЕРАЦИИ ИНТЕРФЕЙСА <<<<< # - #==========================================================================================# - - # Строит интерфейс. - def __BuildInterface(self): - - # Настройка главного контейнера. - self.__MainBox.set_spacing(20) - self.set_child(self.__MainBox) - - # HeaderBar. - self.Header = Adw.HeaderBar() - self.set_titlebar(self.Header) - self.set_default_size(720, 480) - HeaderLabel = Gtk.Label() - HeaderLabel.set_markup("PornHub Downloader") - self.Header.set_title_widget(HeaderLabel) - - # Построение верхней панели. - self.__BuildUpPanel() - - # Строит верхнюю панель. - def __BuildUpPanel(self): - - # Box: верхняя панель. - UpPanelBox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) - UpPanelBox.set_spacing(7) - UpPanelBox.set_homogeneous(True) - - # Button: открытие файла. - Button_Open = Gtk.Button(label = "🗃️ Open") - Button_Open.set_margin_start(7) - Button_Open.set_margin_top(7) - - # Button: управление загрузкой. - self.__Button_Downloading = Gtk.Button(label = "🌎 Start") - self.__Button_Downloading.set_margin_end(7) - self.__Button_Downloading.set_margin_top(7) - self.__Button_Downloading.connect("clicked", lambda _: self.__ChangeDownloadingStatus()) - - # Помещение элементов в контейнеры. - UpPanelBox.append(Button_Open) - UpPanelBox.append(self.__Button_Downloading) - self.__MainBox.append(UpPanelBox) - - #==========================================================================================# - # >>>>> МЕТОДЫ <<<<< # - #==========================================================================================# - - # Конструктор. - def __init__(self, *args, **kwargs): - # Наследование конструктора базового класса. - super().__init__(*args, **kwargs) - - #---> Генерация динамических свойств. - #==========================================================================================# - # Состояние: выполняется ли загрузка. - self.__IsDownloading = False - # Главный контейнер. - self.__MainBox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - - # Инициализация интерфейса. - self.__BuildInterface() - - # # Контейнер: содержимое. - # ContentBox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - # ContentBox.set_spacing(10) - # # Заголовок ввода. - # Label_Input = Gtk.Label() - # Label_Input.set_markup("Input") - # ContentBox.append(Label_Input) - # # Поле ввода. - # TextView_Input = Gtk.TextView() - # TextView_Input.get_buffer().set_text("\n") - # TextView_Input.set_size_request(1080, 300) - # #ContentBox.append(TextView_Input) - # # Скролл-окно. - # ScrolledWindow_Input = Gtk.ScrolledWindow() - # ScrolledWindow_Input.set_child(TextView_Input) - # ContentBox.append(ScrolledWindow_Input) - # # Заголовок ввода. - # Label_Input = Gtk.Label() - # Label_Input.set_markup("Output") - # ContentBox.append(Label_Input) - # # Поле вывода. - # TextView_Output = Gtk.TextView() - # TextView_Output.get_buffer().set_text("\n") - # TextView_Output.set_editable(False) - # ContentBox.append(TextView_Output) - - # self.__MainBox.append(ContentBox) +import gi + +# Запрос требуемых версий библиотек. +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +from gi.repository import Gtk, Adw + +# Главное окно. +class MainWindow(Gtk.ApplicationWindow): + + #==========================================================================================# + # >>>>> МЕТОДЫ ВЗАИМОДЕЙСТВИЯ ИНТЕРФЕЙСА <<<<< # + #==========================================================================================# + + # Изменяет статус загрузки. + def __ChangeDownloadingStatus(self): + + # Если нажата кнопка начала загрузки. + if "🌎" in self.__Button_Downloading.get_label(): + # Изменение статуса. + self.__IsDownloading = True + # Изменение текста кнопки. + self.__Button_Downloading.set_label("🟥 Stop") + else: + # Изменение статуса. + self.__IsDownloading = False + # Изменение текста кнопки. + self.__Button_Downloading.set_label("🌎 Start") + + #==========================================================================================# + # >>>>> МЕТОДЫ ГЕНЕРАЦИИ ИНТЕРФЕЙСА <<<<< # + #==========================================================================================# + + # Строит интерфейс. + def __BuildInterface(self): + + # Настройка главного контейнера. + self.__MainBox.set_spacing(20) + self.set_child(self.__MainBox) + + # HeaderBar. + self.Header = Adw.HeaderBar() + self.set_titlebar(self.Header) + self.set_default_size(720, 480) + HeaderLabel = Gtk.Label() + HeaderLabel.set_markup("PornHub Downloader") + self.Header.set_title_widget(HeaderLabel) + + # Построение верхней панели. + self.__BuildUpPanel() + + # Строит верхнюю панель. + def __BuildUpPanel(self): + + # Box: верхняя панель. + UpPanelBox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) + UpPanelBox.set_spacing(7) + UpPanelBox.set_homogeneous(True) + + # Button: открытие файла. + Button_Open = Gtk.Button(label = "🗃️ Open") + Button_Open.set_margin_start(7) + Button_Open.set_margin_top(7) + + # Button: управление загрузкой. + self.__Button_Downloading = Gtk.Button(label = "🌎 Start") + self.__Button_Downloading.set_margin_end(7) + self.__Button_Downloading.set_margin_top(7) + self.__Button_Downloading.connect("clicked", lambda _: self.__ChangeDownloadingStatus()) + + # Помещение элементов в контейнеры. + UpPanelBox.append(Button_Open) + UpPanelBox.append(self.__Button_Downloading) + self.__MainBox.append(UpPanelBox) + + #==========================================================================================# + # >>>>> МЕТОДЫ <<<<< # + #==========================================================================================# + + # Конструктор. + def __init__(self, *args, **kwargs): + # Наследование конструктора базового класса. + super().__init__(*args, **kwargs) + + #---> Генерация динамических свойств. + #==========================================================================================# + # Состояние: выполняется ли загрузка. + self.__IsDownloading = False + # Главный контейнер. + self.__MainBox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + + # Инициализация интерфейса. + self.__BuildInterface() + + # # Контейнер: содержимое. + # ContentBox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + # ContentBox.set_spacing(10) + # # Заголовок ввода. + # Label_Input = Gtk.Label() + # Label_Input.set_markup("Input") + # ContentBox.append(Label_Input) + # # Поле ввода. + # TextView_Input = Gtk.TextView() + # TextView_Input.get_buffer().set_text("\n") + # TextView_Input.set_size_request(1080, 300) + # #ContentBox.append(TextView_Input) + # # Скролл-окно. + # ScrolledWindow_Input = Gtk.ScrolledWindow() + # ScrolledWindow_Input.set_child(TextView_Input) + # ContentBox.append(ScrolledWindow_Input) + # # Заголовок ввода. + # Label_Input = Gtk.Label() + # Label_Input.set_markup("Output") + # ContentBox.append(Label_Input) + # # Поле вывода. + # TextView_Output = Gtk.TextView() + # TextView_Output.get_buffer().set_text("\n") + # TextView_Output.set_editable(False) + # ContentBox.append(TextView_Output) + + # self.__MainBox.append(ContentBox) diff --git a/Source/UI/GTK4/main.py b/Source/UI/GTK4/main.py new file mode 100644 index 0000000..d1190e9 --- /dev/null +++ b/Source/UI/GTK4/main.py @@ -0,0 +1,6 @@ +import gi + + + self.win = MainWindow(application = Application) +if __name__ == "__main__": + Application = MyApp() \ No newline at end of file diff --git a/Source/UI/LiveCLI/__init__.py b/Source/UI/LiveCLI/__init__.py new file mode 100644 index 0000000..86d5c2d --- /dev/null +++ b/Source/UI/LiveCLI/__init__.py @@ -0,0 +1,118 @@ +from Source.Core.Downloader import VideoDownloader + +from dublib.CLI.Terminalyzer import Command, ParametersTypes, ParsedCommandData, Terminalyzer +from dublib.CLI.TextStyler import Colors, Decorations, TextStyler +from dublib.Methods.Filesystem import ReadTextFile +from dublib.Methods.System import Clear + +import shlex +import os + +try: import readline +except ImportError: pass + +class LiveCLI: + """Live CLI режим работы приложения.""" + + #==========================================================================================# + # >>>>> СВОЙСТВА <<<<< # + #==========================================================================================# + + @property + def commands(self) -> list[Command]: + """Список команд Live режима.""" + + CommandsList = list() + + Com = Command("clear", "Cleare console.") + CommandsList.append(Com) + + Com = Command("exit", "Exit live mode.") + CommandsList.append(Com) + + return CommandsList + + #==========================================================================================# + # >>>>> ПРИВАТНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __ProcessCommand(self, command: ParsedCommandData): + """ + Проводит обработку комманды. + command – комманда. + """ + + if command.name == "exit": + exit(0) + + elif command.name == "clear": + Clear() + + def __ProcessMacros(self, macros: str) -> bool: + """ + Проводит обработку макросов. + macros – макрос. + """ + + IsProcessed = False + + if self.__Downloader.check_link(macros): + IsProcessed = True + self.__Downloader.download_video(macros, self.__Settings["quality"]) + + elif os.path.exists(macros): + IsProcessed = True + Links = ReadTextFile(macros, "\n") + Links = filter(lambda Element: bool(Element), Links) + for Link in Links: self.__Downloader.download_video(Link, self.__Settings["quality"]) + + return IsProcessed + + #==========================================================================================# + # >>>>> ПУБЛИЧНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __init__(self, settings: dict): + """ + Live CLI режим работы приложения. + settings – глобальные настройки. + """ + + #---> Генерация динамических атрибутов. + #==========================================================================================# + self.__Settings = settings.copy() + + self.__Analyzer = Terminalyzer() + self.__Downloader = VideoDownloader() + + self.__Analyzer.enable_help() + + def run(self): + """Запускает цикл ввода команд.""" + + Clear() + ExitBold = TextStyler("exit").decorate.bold + print(TextStyler("PornHub-dlp v2.0.0").decorate.bold) + print(f"Вы находитесь в Live-режиме консольного интерфейса. Для выхода выполните {ExitBold} или нажмите Ctrl + C.") + print("Введите ссылку на видеоролик или путь к текстовому файлу, из которого нужно извлечь список ссылок.") + print("Проект на GitHub:" + " ", end = "") + TextStyler("https://github.com/DUB1401/PornHub-dlp", text_color = Colors.Cyan, decorations = Decorations.Italic).print() + print() + + while True: + Input = None + + try: + Input = input("PornHub-dlp > ").strip() + + except KeyboardInterrupt: + print("exit") + exit(0) + + if Input: + if self.__ProcessMacros(Input): continue + self.__Analyzer.set_source(shlex.split(Input)) + ParsedCommand = self.__Analyzer.check_commands(self.commands) + + if ParsedCommand: self.__ProcessCommand(ParsedCommand) + elif not Input.startswith("help"): print(TextStyler("[ERROR] Неизвестная команда.").colorize.red) \ No newline at end of file diff --git a/Source/GUI/Qt/QLabelAdvertisement.py b/Source/UI/Qt/QLabelAdvertisement.py similarity index 62% rename from Source/GUI/Qt/QLabelAdvertisement.py rename to Source/UI/Qt/QLabelAdvertisement.py index 939e8de..2e1e104 100644 --- a/Source/GUI/Qt/QLabelAdvertisement.py +++ b/Source/UI/Qt/QLabelAdvertisement.py @@ -1,21 +1,20 @@ -from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import QLabel - -# Контейнер рекламной анимации. -class QLabelAdvertisement(QLabel): - - #==========================================================================================# - # >>>>> СВОЙСТВА <<<<< # - #==========================================================================================# - - # Сигнал: надпись кликнули мышью. - clicked = pyqtSignal() - - #==========================================================================================# - # >>>>> МЕТОДЫ <<<<< # - #==========================================================================================# - - # Обрабатывает нажатие мышью. - def mousePressEvent(self, Value): - # Генерация сигнала. +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QLabel + +from typing import Any + +class QLabelAdvertisement(QLabel): + """Контейнер рекламной анимации.""" + + #==========================================================================================# + # >>>>> СТАТИЧЕСКИЕ АТРИБУТЫ <<<<< # + #==========================================================================================# + + clicked = pyqtSignal() + + #==========================================================================================# + # >>>>> ПУБЛИЧНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def mousePressEvent(self, value: Any): self.clicked.emit() \ No newline at end of file diff --git a/Source/UI/Qt/QtWindow.py b/Source/UI/Qt/QtWindow.py new file mode 100644 index 0000000..318b675 --- /dev/null +++ b/Source/UI/Qt/QtWindow.py @@ -0,0 +1,345 @@ +from Source.UI.Qt.QLabelAdvertisement import QLabelAdvertisement +from Source.UI.Qt.yt_dlp import yt_dlp + +from PyQt6.QtWidgets import ( + QCheckBox, + QComboBox, + QGroupBox, + QLabel, + QMainWindow, + QProgressBar, + QPushButton, + QTextEdit, + QVBoxLayout +) +from PyQt6.QtGui import QCursor, QDesktopServices, QMovie, QTextCursor +from PyQt6.QtCore import Qt,QSize, QThread, QUrl +from dublib.Engine.GetText import _ + +import pyperclip +import json +import time +import os +import re + +class PlainTextEdit(QTextEdit): + """Поле ввода неформатированного текста.""" + + def __init__(self, parent = None): + super().__init__(parent) + + def insertFromMimeData(self, source): + self.insertPlainText(source.text()) + +class QtWindow(QMainWindow): + """Главное окно (Qt).""" + + #==========================================================================================# + # >>>>> ОБРАБОТЧИКИ СИГНАЛОВ <<<<< # + #==========================================================================================# + + def __Clear(self): + self.Input.clear() + self.Output.clear() + self.ProgressBar.setValue(0) + self.__VideoLinks = list() + + def __CopyOutput(self): + + try: pyperclip.copy(self.Output.toPlainText()) + except pyperclip.PyperclipException: self.Print("On GNU/Linux you can install xclip or xselect to enable a copy/paste mechanism.") + + def __DownloadVideos(self): + self.Output.clear() + self.__RemoveRepeatedLinks() + self.Clear.setEnabled(False) + self.Download.setEnabled(False) + self.Output.setReadOnly(True) + self.Paste.setEnabled(False) + self.__VideoLinks = list(filter(None, self.Input.toPlainText().strip().split('\n'))) + self.ProgressBar.setMaximum(len(self.__VideoLinks)) + self.ProgressBar.setValue(0) + self.ProgressBar.setVisible(True) + self.__StartDownloading() + + def __FormatInput(self): + InputText = self.Input.toPlainText() + InputLines = InputText.split('\n') + FormattedLines = list() + ResultLines = list() + ResultText = None + + for Line in InputLines: + Bufer = Line.replace("https", "\nhttps").strip("\n \t") + FormattedLines += Bufer.split('\n') + + for Line in FormattedLines: + Line = Line.split('&')[0] + + if bool(re.match(r"https:\/\/.{0,4}?pornhub\.com\/view_video\.php\?viewkey=\S+\b", Line)) == True: + ResultLines.append(Line) + + ResultText = "\n".join(ResultLines) + "\n" + + if ResultText.strip("\n \t") == "": + ResultText = "" + self.Download.setEnabled(False) + + elif self.__VideoIndex == 0: + self.Download.setEnabled(True) + + if ResultText != self.Input.toPlainText(): + self.Input.setText(ResultText) + + self.Input.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor) + + def __OpenAdvertisement(self): + QDesktopServices.openUrl(QUrl(self.__Settings["advertisement"])) + + def __OpenGitHub(self): + QDesktopServices.openUrl(QUrl("https://github.com/DUB1401/PornHub-dlp")) + + def __Paste(self): + try: self.Input.setText(self.Input.toPlainText() + pyperclip.paste().strip("\n \t") + "\n") + except pyperclip.PyperclipException: self.Print("On GNU/Linux you can install xclip or xselect to enable a copy/paste mechanism.") + + def __SaveSetting(self, Key: str, Value): + self.__Settings[Key] = Value + Bufer = self.__Settings.copy() + + if Bufer["directory"] == os.getcwd() + "/Downloads": + Bufer["directory"] = "" + + with open("Settings.json", "w", encoding = "utf-8") as FileWrite: + json.dump(Bufer, FileWrite, ensure_ascii = False, indent = '\t', separators = (",", ": ")) + + def __ScrollOutputToEnd(self): + self.Output.moveCursor(QTextCursor.MoveOperation.End) + + #==========================================================================================# + # >>>>> ПРИВАТНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def __CreatAdvertisementGroupUI(self): + AdvertisementLayout = QVBoxLayout() + self.AdsBox.setLayout(AdvertisementLayout) + + AdvertisementGIF = QMovie("Advertisement.gif") + AdvertisementGIF.setScaledSize(QSize(180, 260)) + AdvertisementGIF.start() + + Advertisement = QLabelAdvertisement(self) + Advertisement.clicked.connect(self.__OpenAdvertisement) + Advertisement.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + Advertisement.setMovie(AdvertisementGIF) + + AdvertisementLayout.addWidget(Advertisement) + + def __CreateBasicUI(self): + + self.AdsBox = QGroupBox(self) + self.AdsBox.move(870, 170) + self.AdsBox.resize(200, 300) + self.AdsBox.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.AdsBox.setTitle(f"📰 " + _("Реклама")) + + self.Clear = QPushButton(self) + self.Clear.clicked.connect(self.__Clear) + self.Clear.move(870, 590) + self.Clear.resize(200, 40) + self.Clear.setText(f"🧹 " + _("Очистить")) + + self.Copy = QPushButton(self) + self.Copy.clicked.connect(self.__CopyOutput) + self.Copy.move(870, 540) + self.Copy.resize(200, 40) + self.Copy.setText(f"📋 " + _("Копировать вывод")) + self.Copyright = QLabel(self) + self.Copyright.setText("Copyright © 2023-2025. DUB1401.") + self.Copyright.move(10, 690) + self.Copyright.adjustSize() + + self.Download = QPushButton(self) + self.Download.clicked.connect(self.__DownloadVideos) + self.Download.move(870, 640) + self.Download.resize(200, 40) + self.Download.setEnabled(False) + self.Download.setText(f"⬇ " + _("Скачать")) + + self.Input = PlainTextEdit(self) + self.Input.move(10, 10) + self.Input.resize(850, 420) + self.Input.setPlaceholderText(_("Вставьте сюда ссылки на видео")) + self.Input.textChanged.connect(self.__FormatInput) + + self.Link = QLabel(self) + self.Link.linkActivated.connect(self.__OpenGitHub) + self.Link.setText("GitHub") + self.Link.adjustSize() + self.Link.move(1080 - self.Link.size().width() - 10, 690) + + self.Output = QTextEdit(self) + self.Output.move(10, 490) + self.Output.resize(850, 190) + self.Output.setReadOnly(True) + self.Output.setPlaceholderText(_("Вывод")) + self.Output.textChanged.connect(self.__ScrollOutputToEnd) + + self.Paste = QPushButton(self) + self.Paste.clicked.connect(self.__Paste) + self.Paste.move(870, 490) + self.Paste.resize(200, 40) + self.Paste.setText(f"📖 " + _("Вставить ссылки")) + + self.ProgressBar = QProgressBar(self) + self.ProgressBar.move(10, 450) + self.ProgressBar.resize(850, 20) + self.ProgressBar.setValue(0) + + self.SettingsBox = QGroupBox(self) + self.SettingsBox.move(870, 10) + self.SettingsBox.resize(200, 160) + self.SettingsBox.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.SettingsBox.setTitle(f"🔧 " + _("Настройки")) + + def __CreateSettingsGroupUI(self): + SettingsLayout = QVBoxLayout() + self.SettingsBox.setLayout(SettingsLayout) + + #---> Создание объектов GUI. + #==========================================================================================# + + CualityTitle = QLabel(self) + CualityTitle.setText(_("Качество") + ":") + CualityTitle.adjustSize() + + CualitySelecter = QComboBox(self) + CualitySelecter.addItems(("4K", "2K", "FullHD", "HD", "480p", "360p", "240p")) + CualitySelecter.setCurrentIndex(self.__Settings["quality"]) + CualitySelecter.currentIndexChanged.connect(lambda: self.__SaveSetting("quality", CualitySelecter.currentIndex())) + CualitySelecter.resize(180, 40) + CualitySelecter.setToolTip(_("Разрешение скачиваемых видео.")) + + SortByModel = QCheckBox(self) + SortByModel.clicked.connect(lambda: self.__SaveSetting("sorting", SortByModel.isChecked())) + SortByModel.setChecked(self.__Settings["sorting"]) + SortByModel.setText(_("По моделям")) + SortByModel.setToolTip(_("Сортировать видео по каталогам в соответствии с авторами.")) + SortByModel.adjustSize() + + #---> Добавление объектов GUI в слой. + #==========================================================================================# + SettingsLayout.addWidget(SortByModel) + SettingsLayout.addWidget(CualityTitle) + SettingsLayout.addWidget(CualitySelecter) + SettingsLayout.addStretch() + + def __EndDownloading(self, ExitCode: int): + self.__VideoIndex += 1 + self.ProgressBar.setValue(self.__VideoIndex) + + if ExitCode == 0: + self.Print("Done! (" + self.__FormatExecutionTime(round(float(time.time() - self.__StartTime), 2)) + ")", True) + + else: + self.Print("Error! See console output for more information.", True) + + self.Input.setText('\n'.join(self.Input.toPlainText().split('\n')[1:])) + + if self.__VideoIndex < len(self.__VideoLinks): + self.__StartDownloading() + + else: + self.Print("Complete.") + self.Clear.setEnabled(True) + self.Download.setEnabled(True) + self.Output.setReadOnly(False) + self.Paste.setEnabled(True) + self.__VideoIndex = 0 + self.Input.setText("") + + def __FormatExecutionTime(self, ExecutionTime: float) -> str: + Result = "" + ElapsedMinutes = int(ExecutionTime / 60.0) + + if ElapsedMinutes > 0: + Result += str(ElapsedMinutes) + " minutes " + ElapsedSeconds = round(ExecutionTime % 60.0, 2) + Result += str(ElapsedSeconds) + " seconds" + + else: + Result += str(ExecutionTime) + " seconds" + + return Result + + def __RemoveRepeatedLinks(self): + InputText = self.Input.toPlainText() + InputLines = InputText.split('\n') + ResultLines = [*set(InputLines)] + + if len(InputLines) != len(ResultLines): + ResultText = "\n".join(ResultLines) + "\n" + self.Input.setText(ResultText) + RepeatedLinksCount = len(InputLines) - len(ResultLines) + self.Print("Removed identical links count: " + str(RepeatedLinksCount), True) + + def __StartDownloading(self): + SaveDirectory = self.__Settings["directory"] + + self.__StartTime = time.time() + + if self.__VideoIndex < len(self.__VideoLinks): + CurrentLink = self.__VideoLinks[self.__VideoIndex] + self.Print("Downloading: " + str(self.__VideoIndex + 1) + " / " + str(len(self.__VideoLinks))) + self.Print("Current task: " + self.__VideoLinks[self.__VideoIndex] + "") + self.Subprocess = yt_dlp(SaveDirectory, CurrentLink, self.__Settings["sorting"], self.__Resolutions[self.__Settings["quality"]]) + self.Subprocess.moveToThread(self.__DownloadingThread) + self.__DownloadingThread.quit() + self.__DownloadingThread.started.connect(self.Subprocess.run) + self.Subprocess.finished.connect(self.__EndDownloading) + self.Subprocess.finished.connect(self.__DownloadingThread.quit) + self.__DownloadingThread.start() + + def __init__(self, settings: dict): + """ + Главное окно (Qt). + settings – словарь глобальных настроек. + """ + + super().__init__() + + self.__Resolutions = ("4096", "2048", "1080", "720", "480", "360", "240") + self.__DownloadingThread = None + self.__VideoLinks = list() + self.__StartTime = None + self.__VideoIndex = 0 + + #---> Генерация динамических атрибутов. + #==========================================================================================# + self.__Settings = settings.copy() + + self.__DownloadingThread = QThread() + + #---> Инициализация графического интерфейса. + #==========================================================================================# + + self.setFixedSize(1080, 720) + self.setWindowTitle("PornHub-dlp v2.0.0") + + self.__CreateBasicUI() + self.__CreateSettingsGroupUI() + + if self.__Settings["advertisement"] and os.path.exists("Advertisement.gif"): self.__CreatAdvertisementGroupUI() + else: self.AdsBox.setVisible(False) + + #==========================================================================================# + # >>>>> ПУБЛИЧНЫЕ МЕТОДЫ <<<<< # + #==========================================================================================# + + def Print(self, Message: str, Separator: bool = False): + + Text = "" + if not self.Output.toPlainText(): Text = "" + else: Text = self.Output.toHtml() + if Separator: Message += "
==========================================================================================" + self.Output.setHtml(Text + Message) \ No newline at end of file diff --git a/Source/UI/Qt/yt_dlp.py b/Source/UI/Qt/yt_dlp.py new file mode 100644 index 0000000..35703ec --- /dev/null +++ b/Source/UI/Qt/yt_dlp.py @@ -0,0 +1,35 @@ +from Source.Core.Downloader import VideoDownloader + +from PyQt6.QtCore import QObject, pyqtSignal + +class yt_dlp(QObject): + """Потоковый обработчик взаимодейтсвий с библиотекой yt-dlp.""" + + finished = pyqtSignal(int) + + def __init__(self, directory: str, link: str, sorting: bool, quality: str): + """ + Потоковый обработчик взаимодейтсвий с библиотекой pornhub_dl. + directory — каталог загрузок;\n + link — ссылка на видео;\n + sorting — переключает сортировку по автору;\n + quality — предпочитаемое качество. + """ + + super().__init__() + + #---> Генерация статических атрибутов. + #==========================================================================================# + self.__SaveDirectory = directory + self.__Link = link + self.__SortByUploader = sorting + self.__Quality = quality + + self.__Downloader = VideoDownloader() + self.__Downloader.enable_sorting(self.__SortByUploader) + + def run(self): + """Запускает процесс скачивания.""" + + Status = self.__Downloader.download_video(self.__Link, self.__Quality) + self.finished.emit(Status.code) \ No newline at end of file diff --git a/Source/Window.py b/Source/Window.py deleted file mode 100644 index 4cbc255..0000000 --- a/Source/Window.py +++ /dev/null @@ -1,75 +0,0 @@ -from Source.GUI.Qt.QtWindow import QtWindow -from PyQt6.QtWidgets import QApplication -from PyQt6 import QtGui - -import ctypes -import enum -import sys - -#==========================================================================================# -# >>>>> ДОПОЛНИТЕЛЬНЫЕ ТИПЫ ДАННЫХ <<<<< # -#==========================================================================================# - -# Типы графических библиотек. -class Toolkits(enum.Enum): - GTK = "gtk" - Qt = "qt" - -#==========================================================================================# -# >>>>> ОСНОВНОЙ КЛАСС <<<<< # -#==========================================================================================# - -# Дескриптор окна. -class Window: - - # Сворачивает терминал Windows. - def __MinimizeCMD(self): - # Если не включён режим отладки, свернуть консоль. - if self.__Settings["debug"] == False and sys.platform == "win32": ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 6) - - # Инициализирует приложение Qt. - def __InitApplication_Qt(self): - # Создание экземпляра приложения. - self.__Application = QApplication(sys.argv) - # Настройка внешнего вида. - self.__Application.setStyle("Fusion") - self.__Application.setWindowIcon(QtGui.QIcon("icon.ico")) - # Создание окна. - self.__Window = QtWindow(self.__Application, self.__Veriables, self.__Settings) - - # Конструктор. - def __init__(self, variables: dict, settings: dict): - - #---> Генерация динамических свойств. - #==========================================================================================# - # Словарь важных значений. - self.__Veriables = variables - # Глобальные настройки. - self.__Settings = settings - # Экзмепляр приложения. - self.__Application = None - # Экземпляр окна. - self.__Window = None - - # Отображает окно. - def show(self, toolkit: Toolkits) -> int: - # Код завершения. - ExitCode = 0 - - # Если используется Qt: - if toolkit == Toolkits.Qt: - # Инициализация приложения. - self.__InitApplication_Qt() - # Открытие окна. - self.__Window.show() - # Сворачивание терминала на Windows. - self.__MinimizeCMD() - # Запуск приложения Qt. - ExitCode = self.__Application.exec() - - # Если используется GTK: - if toolkit == Toolkits.GTK: - # Если используется ОС семейства Windows, выбросить исключение. - if sys.platform == "win32": raise ImportError("GTK isn't support on Windows.") - - return ExitCode \ No newline at end of file diff --git a/main.py b/main.py index 2bc4363..acca8c5 100644 --- a/main.py +++ b/main.py @@ -1,72 +1,39 @@ -from dublib.Methods import CheckPythonMinimalVersion, ReadJSON -from dublib.Terminalyzer import Command, Terminalyzer -from Source.Window import Window, Toolkits +from Source.Core.Application import Application, Interfaces -import ctypes -import json -import sys -import os +from dublib.Methods.System import CheckPythonMinimalVersion +from dublib.CLI.Terminalyzer import Command, Terminalyzer +from dublib.Methods.Filesystem import ReadJSON +from dublib.Engine.GetText import GetText #==========================================================================================# -# >>>>> ИНИЦИАЛИЗАЦИЯ СКРИПТА <<<<< # +# >>>>> ИНИЦИАЛИЗАЦИЯ <<<<< # #==========================================================================================# -# Проверка минимальной требуемой версии. CheckPythonMinimalVersion(3, 10) -# Словарь важных значений. -VARIABLES = { - "version": "1.4.0", - "copyright": "Copyright © 2023-2024. DUB1401." -} - -#==========================================================================================# -# >>>>> ЧТЕНИЕ НАСТРОЕК <<<<< # -#==========================================================================================# - -# Чтение настроек. +GetText.initialize("PornHub-dlp", "ru", "Locales") Settings = ReadJSON("Settings.json") - -# Если директория для загрузки не указана. -if Settings["downloads-directory"] == "": - # Формирование пути. - Settings["downloads-directory"] = os.getcwd() + "/Downloads" - # Если стандартной папки не существует, создать. - if os.path.exists("Downloads") == False: os.makedirs("Downloads") +WindowObject = Application(Settings) #==========================================================================================# -# >>>>> НАСТРОЙКА ОБРАБОТЧИКА КОМАНД <<<<< # +# >>>>> ГЕНЕРАЦИЯ ОПИСАНИЙ КОМАНД <<<<< # #==========================================================================================# -# Список описаний обрабатываемых команд. CommandsList = list() +Com = Command("run", "Run application.") +ComPos = Com.create_position("MODE", "Mode of launching.") +ComPos.add_flag("qt", "PyQt6 mode.") +ComPos.add_flag("gtk", "GTK4 mode.") +ComPos.add_flag("live", "Live CLI mode.") +CommandsList.append(Com) -# Создание команды: run. -COM_run = Command("run") -COM_run.add_flag_position(["qt", "gtk"]) -CommandsList.append(COM_run) - -# Инициализация обработчика консольных аргументов. -CAC = Terminalyzer() -# Получение информации о проверке команд. -CommandDataStruct = CAC.check_commands(CommandsList) +Analyzer = Terminalyzer() +Analyzer.enable_help() +ParsedCommand = Analyzer.check_commands(CommandsList) #==========================================================================================# # >>>>> ОБРАБОТКА КОМАНД <<<<< # #==========================================================================================# -# Запуск стандартного окна. -WindowObject = Window(VARIABLES, Settings) -# Стандартная графическая библиотека. -Toolkit = Toolkits.Qt - -# Обработка отсутствия команды. -if CommandDataStruct == None: - # Запуск стандартного окна. - WindowObject.show(Toolkit) - -# Обработка команды: run -if CommandDataStruct.name == "run": - # Если указана GTK, выполнить запуск с её использованием. - if "gtk" in CommandDataStruct.flags: Toolkit = Toolkits.GTK - # Запуск окна. - WindowObject.show(Toolkit) \ No newline at end of file +if not ParsedCommand: WindowObject.run(Interfaces.Qt) +elif ParsedCommand.check_flag("live"): WindowObject.run(Interfaces.LiveCLI) +elif ParsedCommand.check_flag("qt"): WindowObject.run(Interfaces.Qt) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 60f5ba3..5c79b4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyinstaller -pyperclip -requests +pyinstaller; sys_platform=="win32" +pyperclip +dublib==0.15.3 pyqt6 \ No newline at end of file diff --git a/yt-dlp/ffmpeg.exe b/yt-dlp/ffmpeg.exe deleted file mode 100644 index 132728c..0000000 Binary files a/yt-dlp/ffmpeg.exe and /dev/null differ diff --git a/yt-dlp/ffprobe.exe b/yt-dlp/ffprobe.exe deleted file mode 100644 index 453de7e..0000000 Binary files a/yt-dlp/ffprobe.exe and /dev/null differ diff --git a/yt-dlp/yt-dlp b/yt-dlp/yt-dlp deleted file mode 100644 index 112634b..0000000 Binary files a/yt-dlp/yt-dlp and /dev/null differ diff --git a/yt-dlp/yt-dlp.exe b/yt-dlp/yt-dlp.exe deleted file mode 100644 index dc0d6da..0000000 Binary files a/yt-dlp/yt-dlp.exe and /dev/null differ