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