From ce13313a620691c6680211089616e0bbd9effb82 Mon Sep 17 00:00:00 2001 From: devhotteok Date: Sun, 19 Jun 2022 14:45:06 +0900 Subject: [PATCH] 2.0.2 Update --- Core/Launcher.py | 2 +- Core/Meta.py | 2 +- Core/Qt/QtWidgets/QLabel.py | 5 +- Core/Ui.py | 6 +- Core/Updater.py | 16 +- Database/Database.py | 16 +- Database/Updater.py | 3 +- Download/DownloadHistory.py | 2 +- Download/DownloadInfo.py | 39 +- Download/DownloadManager.py | 7 +- Download/Downloader/Engine/Setup.py | 16 +- Download/Downloader/Engine/Video/Video.py | 12 +- Download/Downloader/FFmpeg/FFmpeg.py | 10 +- Download/Downloader/FFmpeg/OutputReader.py | 27 +- Services/Ad/AdManager.py | 117 ++--- Services/Ad/AdView.py | 56 +++ Services/Ad/AdWidget.py | 26 ++ Services/Ad/Config.py | 3 +- Services/Ad/__init__.py | 2 + Services/Document.py | 6 +- Services/Task/TaskManager.py | 6 +- Services/Twitch/Gql/TwitchGqlAPI.py | 4 +- Services/Twitch/Gql/TwitchGqlModels.py | 27 +- Ui/DocumentView.py | 24 +- Ui/DownloadMenu.py | 76 ++-- Ui/DownloadPreview.py | 24 +- Ui/Downloads.py | 6 + Ui/Home.py | 4 +- Ui/MainWindow.py | 3 +- Ui/Operators/DownloadsPage.py | 15 +- Ui/Operators/TermsOfService.py | 7 +- Ui/PropertyView.py | 22 +- Ui/Search.py | 22 +- Ui/SearchResult.py | 14 +- Ui/Settings.py | 8 +- requirements.txt | 10 +- .../translations/KeywordTranslations.json | 24 + resources/translations/Translations.json | 8 + resources/ui/documentView.ui | 18 - resources/ui/download.ui | 2 +- resources/ui/downloadMenu.ui | 410 ++++++++++-------- resources/ui/translators/ko/documentView.qm | Bin 154 -> 150 bytes resources/ui/translators/ko/downloadMenu.qm | Bin 794 -> 872 bytes 43 files changed, 668 insertions(+), 439 deletions(-) create mode 100644 Services/Ad/AdView.py create mode 100644 Services/Ad/AdWidget.py create mode 100644 Services/Ad/__init__.py diff --git a/Core/Launcher.py b/Core/Launcher.py index 9182720..8a5278d 100644 --- a/Core/Launcher.py +++ b/Core/Launcher.py @@ -19,7 +19,7 @@ class SingleApplicationLauncher(QtWidgets.QApplication): def __init__(self, guid, argv): super(SingleApplicationLauncher, self).__init__(argv) - self.logger = Logger(fileName=f"{Config.APP_NAME}_{id(self)}.txt") + self.logger = Logger(fileName=f"{Config.APP_NAME}_{id(self)}.log") self.logger.info(f"\n\n{Config.getProjectInfo()}\n") self.logger.info(OSUtils.getOSInfo()) self.shared = QtCore.QSharedMemory(guid, parent=self) diff --git a/Core/Meta.py b/Core/Meta.py index 74cee4d..5e5ced7 100644 --- a/Core/Meta.py +++ b/Core/Meta.py @@ -4,7 +4,7 @@ class Meta: APP_NAME = "TwitchLink" - VERSION = "2.0.1" + VERSION = "2.0.2" AUTHOR = "DevHotteok" diff --git a/Core/Qt/QtWidgets/QLabel.py b/Core/Qt/QtWidgets/QLabel.py index cf98639..388bdfa 100644 --- a/Core/Qt/QtWidgets/QLabel.py +++ b/Core/Qt/QtWidgets/QLabel.py @@ -65,13 +65,12 @@ def keepImageRatio(self, keepImageRatio): self._keepImageRatio = keepImageRatio def setText(self, text): + super().setText(str(text)) if isinstance(text, datetime): self._useAutoToolTip = False - super().setText(text.strftime("%Y-%m-%d %H:%M:%S")) - self.setToolTip(f"{self.text()} {text.tzname()} ({text.tzinfo.zone})") + self.setToolTip(text.details()) else: self._useAutoToolTip = True - super().setText(str(text)) def paintEvent(self, event): if self.pixmap() == None: diff --git a/Core/Ui.py b/Core/Ui.py index fe83b1a..2964e08 100644 --- a/Core/Ui.py +++ b/Core/Ui.py @@ -3,7 +3,7 @@ from Services.Utils.Utils import Utils from Services.Image.Presets import * from Services.Translator.Translator import Translator, T -from Services.Ad import AdManager +from Services import Ad from Database.Database import DB from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets, uic @@ -54,9 +54,9 @@ def ask(self, title, content, titleTranslate=True, contentTranslate=True, okText def setAds(self): adArea = self.findChildren(QtWidgets.QWidget, QtCore.QRegExp("^adArea_\d+$")) adGroup = self.findChildren(QtWidgets.QWidget, QtCore.QRegExp("^adGroup_\d+$")) - if AdManager.Config.SHOW: + if Ad.Config.SHOW: for widget in adArea: - Utils.setPlaceholder(widget, AdManager.Ad(minimumSize=widget.minimumSize(), responsive=True, parent=self)) + Utils.setPlaceholder(widget, Ad.AdWidget(adId=f"{self.__class__.__name__}.{widget.objectName()}", adSize=widget.minimumSize(), responsive=True, parent=self)) else: for widget in adArea + adGroup: widget.setParent(None) diff --git a/Core/Updater.py b/Core/Updater.py index dabd65c..b68757f 100644 --- a/Core/Updater.py +++ b/Core/Updater.py @@ -63,11 +63,21 @@ def updateStatusData(self, data): def updateNotifications(self, data): self.notifications = [ DocumentData( - **(notification | {"buttons": [ + contentId=notification.get("contentId", None), + contentVersion=notification.get("contentVersion", 0), + title=notification.get("title", ""), + content=notification.get("content", ""), + contentType=notification.get("contentType", ""), + modal=notification.get("modal", False), + blockExpiry=notification.get("blockExpiry", False), + buttons=[ DocumentButtonData( - **button + text=button.get("text", ""), + action=button.get("action", None), + role=button.get("role", "accept"), + default=button.get("default", False) ) for button in notification.get("buttons", []) - ]}) + ] ) for notification in data.get(Translator.getLanguage(), []) ] diff --git a/Database/Database.py b/Database/Database.py index d456225..dbd573d 100644 --- a/Database/Database.py +++ b/Database/Database.py @@ -16,7 +16,7 @@ import json -from datetime import datetime +from datetime import datetime, timedelta class Setup: @@ -163,6 +163,7 @@ def __init__(self): ImageConfig.DATA_TYPE: DownloadHistory.ThumbnailHistory() } self._windowGeometry = {} + self._blockedContent = {} def getDownloadHistory(self, contentType): return self._downloadHistory[contentType] @@ -176,6 +177,19 @@ def setWindowGeometry(self, windowName, windowGeometry): def getWindowGeometry(self, windowName): return self._windowGeometry[windowName] + def isContentBlocked(self, contentId, contentVersion): + if contentId in self._blockedContent: + oldContentVersion, blockExpiry = self._blockedContent[contentId] + if contentVersion != oldContentVersion or (blockExpiry != None and blockExpiry < datetime.now()): + del self._blockedContent[contentId] + return False + else: + return True + else: + return False + + def blockContent(self, contentId, contentVersion, blockExpiry=None): + self._blockedContent[contentId] = (contentVersion, None if blockExpiry == None else datetime.now() + timedelta(days=blockExpiry)) class Download: def __init__(self): diff --git a/Database/Updater.py b/Database/Updater.py index 1e0ffc9..123e6e4 100644 --- a/Database/Updater.py +++ b/Database/Updater.py @@ -39,7 +39,8 @@ def getUpdaters(cls, versionFrom): "1.1.0": None, "1.1.1": None, "2.0.0": cls.Update_2_0_0, - "2.0.1": None + "2.0.1": None, + "2.0.2": None } updaters = [] versionFound = False diff --git a/Download/DownloadHistory.py b/Download/DownloadHistory.py index 5759cf8..71d6008 100644 --- a/Download/DownloadHistory.py +++ b/Download/DownloadHistory.py @@ -78,7 +78,7 @@ class VideoHistory(FileHistory, AudioFormatHistory): def __init__(self): super(VideoHistory, self).__init__() - self.setUnmuteVideoEnabled(True) + self.setUnmuteVideoEnabled(False) self.setUpdateTrackEnabled(False) def setUnmuteVideoEnabled(self, unmuteVideo): diff --git a/Download/DownloadInfo.py b/Download/DownloadInfo.py index 923f3d1..083c394 100644 --- a/Download/DownloadInfo.py +++ b/Download/DownloadInfo.py @@ -29,6 +29,7 @@ def __init__(self, videoData, accessToken): def getFileNameTemplateVariables(self): if self.type.isStream(): + startedAt = self.stream.createdAt.asTimezone(DB.localization.getTimezone()) return { "type": T(self.type.toString()), "id": self.stream.id, @@ -37,12 +38,19 @@ def getFileNameTemplateVariables(self): "channel": self.stream.broadcaster.login, "channel_name": self.stream.broadcaster.displayName, "channel_formatted_name": self.stream.broadcaster.formattedName(), - "started_at": self.stream.createdAt.asTimezone(DB.localization.getTimezone()), - "date": self.stream.createdAt.date(DB.localization.getTimezone()), - "time": self.stream.createdAt.time(DB.localization.getTimezone()), + "started_at": startedAt, + "date": startedAt.date(), + "year": f"{startedAt.year:04}", + "month": f"{startedAt.month:02}", + "day": f"{startedAt.day:02}", + "time": startedAt.time(), + "hour": f"{startedAt.hour:02}", + "minute": f"{startedAt.minute:02}", + "second": f"{startedAt.second:02}", "resolution": self.resolution.resolutionName } elif self.type.isVideo(): + publishedAt = self.video.publishedAt.asTimezone(DB.localization.getTimezone()) return { "type": T(self.type.toString()), "id": self.video.id, @@ -52,13 +60,20 @@ def getFileNameTemplateVariables(self): "channel_name": self.video.owner.displayName, "channel_formatted_name": self.video.owner.formattedName(), "duration": self.video.lengthSeconds, - "published_at": self.video.publishedAt.asTimezone(DB.localization.getTimezone()), - "date": self.video.publishedAt.date(DB.localization.getTimezone()), - "time": self.video.publishedAt.time(DB.localization.getTimezone()), + "published_at": publishedAt, + "date": publishedAt.date(), + "year": f"{publishedAt.year:04}", + "month": f"{publishedAt.month:02}", + "day": f"{publishedAt.day:02}", + "time": publishedAt.time(), + "hour": f"{publishedAt.hour:02}", + "minute": f"{publishedAt.minute:02}", + "second": f"{publishedAt.second:02}", "views": self.video.viewCount, "resolution": self.resolution.resolutionName } else: + createdAt = self.clip.createdAt.asTimezone(DB.localization.getTimezone()) return { "type": T(self.type.toString()), "id": self.clip.id, @@ -72,9 +87,15 @@ def getFileNameTemplateVariables(self): "creator_name": self.clip.curator.displayName, "creator_formatted_name": self.clip.curator.formattedName(), "duration": self.clip.durationSeconds, - "created_at": self.clip.createdAt.asTimezone(DB.localization.getTimezone()), - "date": self.clip.createdAt.date(DB.localization.getTimezone()), - "time": self.clip.createdAt.time(DB.localization.getTimezone()), + "created_at": createdAt, + "date": createdAt.date(), + "year": f"{createdAt.year:04}", + "month": f"{createdAt.month:02}", + "day": f"{createdAt.day:02}", + "time": createdAt.time(), + "hour": f"{createdAt.hour:02}", + "minute": f"{createdAt.minute:02}", + "second": f"{createdAt.second:02}", "views": self.clip.viewCount, "resolution": self.resolution.resolutionName } diff --git a/Download/DownloadManager.py b/Download/DownloadManager.py index eda60f8..217749c 100644 --- a/Download/DownloadManager.py +++ b/Download/DownloadManager.py @@ -7,6 +7,7 @@ class _DownloadManager(QtCore.QObject): createdSignal = QtCore.pyqtSignal(object) destroyedSignal = QtCore.pyqtSignal(object) + startedSignal = QtCore.pyqtSignal(object) completedSignal = QtCore.pyqtSignal(object) runningCountChangedSignal = QtCore.pyqtSignal(int) @@ -23,6 +24,7 @@ def onStart(self, downloader): self.hideDownloaderProgress(complete=False) self.runningDownloaders.append(downloader) self.runningCountChangedSignal.emit(len(self.runningDownloaders)) + self.startedSignal.emit(downloader.getId()) def onFinish(self, downloader): self.runningDownloaders.remove(downloader) @@ -49,8 +51,8 @@ def hideDownloaderProgress(self, complete): def create(self, downloadInfo): downloader = TwitchDownloader(downloadInfo, parent=self) - downloader.needSetup.connect(self.onStart) - downloader.needCleanup.connect(self.onFinish) + downloader.started.connect(self.onStart) + downloader.finished.connect(self.onFinish) downloaderId = downloader.getId() self.downloaders[downloaderId] = downloader self.createdSignal.emit(downloaderId) @@ -114,5 +116,4 @@ def handleClipProgress(self, downloader): progress = downloader.progress App.taskbar.setValue(progress.byteSizeProgress) - DownloadManager = _DownloadManager() \ No newline at end of file diff --git a/Download/Downloader/Engine/Setup.py b/Download/Downloader/Engine/Setup.py index d7e933d..70fa64c 100644 --- a/Download/Downloader/Engine/Setup.py +++ b/Download/Downloader/Engine/Setup.py @@ -10,8 +10,8 @@ class EngineSetup(QtCore.QThread): - needSetup = QtCore.pyqtSignal(object) - needCleanup = QtCore.pyqtSignal(object) + started = QtCore.pyqtSignal(object) + finished = QtCore.pyqtSignal(object) statusUpdate = QtCore.pyqtSignal(Modules.Status) progressUpdate = QtCore.pyqtSignal(Modules.Progress) dataUpdate = QtCore.pyqtSignal(dict) @@ -24,6 +24,14 @@ def __init__(self, downloadInfo, parent=None): self.progress = Modules.Progress() self.actionLock = MutexLocker() self.setupLogger() + super().started.connect(self.emitStartedSignal) + super().finished.connect(self.emitFinishedSignal) + + def emitStartedSignal(self): + self.started.emit(self) + + def emitFinishedSignal(self): + self.finished.emit(self) def getId(self): return id(self) @@ -32,12 +40,11 @@ def setupLogger(self): name = f"{Config.APP_NAME}_Download_{self.getId()}" self.logger = Logger( name=name, - fileName=f"{name}.txt", + fileName=f"{name}.log", ) self.logger.debug(f"{Config.APP_NAME} {Config.VERSION}\n\n[Download Info]\n{ObjectLogger.generateObjectLog(self.setup.downloadInfo)}") def run(self): - self.needSetup.emit(self) self.logger.info("Download Started") try: self.download() @@ -64,7 +71,6 @@ def run(self): self.logger.info("Download Failed") else: self.logger.info("Download Completed") - self.needCleanup.emit(self) def download(self): pass diff --git a/Download/Downloader/Engine/Video/Video.py b/Download/Downloader/Engine/Video/Video.py index 2b8c84f..c5730df 100644 --- a/Download/Downloader/Engine/Video/Video.py +++ b/Download/Downloader/Engine/Video/Video.py @@ -52,7 +52,7 @@ def downloadSegments(self): url = self.setup.downloadInfo.getUrl().rsplit("/", 1)[0] processedFiles = [] with self.actionLock: - self.taskManager.taskCompleteSignal.connect(self.segmentDownloadResult) + self.taskManager.taskCompleteSignal.connect(self.segmentDownloadComplete) self.taskManager.ifPaused.connect(self.taskPaused) self.taskManager.start() while self.status.terminateState.isFalse(): @@ -108,11 +108,11 @@ def downloadSegments(self): except: break - def segmentDownloadResult(self, result): - if not result.success: - urls = "\n".join(result.task.urls) - self.logger.warning(f"Failed to download segment: {result.task.segment.fileName} [{result.error}]\n{urls}") - if isinstance(result.error, Exceptions.FileSystemError): + def segmentDownloadComplete(self, task): + if not task.result.success: + urls = "\n".join(task.urls) + self.logger.warning(f"Failed to download segment: {task.segment.fileName} [{task.result.error}]\n{urls}") + if isinstance(task.result.error, Exceptions.FileSystemError): self.cancel() self.status.raiseError(Exceptions.FileSystemError) return diff --git a/Download/Downloader/FFmpeg/FFmpeg.py b/Download/Downloader/FFmpeg/FFmpeg.py index a52df41..8a5c958 100644 --- a/Download/Downloader/FFmpeg/FFmpeg.py +++ b/Download/Downloader/FFmpeg/FFmpeg.py @@ -58,6 +58,7 @@ def start(self, target, saveAs, logLevel=LogLevel.INFO, priority=Priority.NORMAL startupinfo=startupinfo, creationflags=priority ) + self.process.notResponding = False def output(self, logger=None): return FFmpegOutputReader(self.process, logger).reader() @@ -75,10 +76,11 @@ def kill(self): def _killProcess(self): try: + self.process.communicate(input="q", timeout=Config.KILL_TIMEOUT) + except: + self.process.notResponding = True try: - self.process.communicate(input="q", timeout=Config.KILL_TIMEOUT) - except: self.process.kill() self.process.communicate() - except: - pass \ No newline at end of file + except: + pass \ No newline at end of file diff --git a/Download/Downloader/FFmpeg/OutputReader.py b/Download/Downloader/FFmpeg/OutputReader.py index cbd06c1..065e4ee 100644 --- a/Download/Downloader/FFmpeg/OutputReader.py +++ b/Download/Downloader/FFmpeg/OutputReader.py @@ -25,20 +25,29 @@ def reader(self): def _read(self): line = "" - for line in self.process.stdout: - progressData = self.getProgressData(line) - if progressData != None: - yield progressData + try: + for line in self.process.stdout: + progressData = self.getProgressData(line) + if progressData != None: + yield progressData + except: + pass self.checkError(self.process.wait(), line) def _readWithLogs(self): line = "" - for line in self.process.stdout: - self.logger.debug(line.strip("\n")) - progressData = self.getProgressData(line) - if progressData != None: - yield progressData + try: + for line in self.process.stdout: + self.logger.debug(line.strip("\n")) + progressData = self.getProgressData(line) + if progressData != None: + yield progressData + except Exception as e: + self.logger.error("Unable to read output properly.") + self.logger.exception(e) returnCode = self.process.wait() + if self.process.notResponding: + self.logger.info("Subprocess was unresponsive and forced to terminate.") self.logger.info(f"Subprocess ended with exit code {returnCode}.") self.checkError(returnCode, line) diff --git a/Services/Ad/AdManager.py b/Services/Ad/AdManager.py index 930be54..69ee71a 100644 --- a/Services/Ad/AdManager.py +++ b/Services/Ad/AdManager.py @@ -1,97 +1,46 @@ -from .Config import Config +from .AdView import AdView -from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets +from PyQt5 import QtCore -class Ad(QtWidgets.QWidget): - def __init__(self, minimumSize, responsive=False, parent=None): - super(Ad, self).__init__(parent=parent) - self.adView = AdView(parent=self) - self.setLayout(QtWidgets.QHBoxLayout(self)) - self.layout().addChildWidget(self.adView) - if responsive: - self.setMinimumSize(minimumSize) - else: - self.setFixedSize(minimumSize) - self.adView.getAd(minimumSize.width(), minimumSize.height()) +class CachedAd(QtCore.QObject): + noReferenceFound = QtCore.pyqtSignal(object) - def setContentsMargins(self, left, top, right, bottom): - self.adView.setContentsMargins(left, top, right, bottom) + def __init__(self, adId, adSize, parent=None): + super(CachedAd, self).__init__(parent=parent) + self.adId = adId + self.references = [] + self.adObject = AdView() + self.adObject.getAd(adSize.width(), adSize.height()) - def minimumSizeHint(self): - return self.minimumSize() + def addReference(self, widgetId): + if widgetId not in self.references: + self.references.append(widgetId) - def sizeHint(self): - return self.minimumSizeHint() + def removeReference(self, widgetId): + self.references.remove(widgetId) + if len(self.references) == 0: + self.noReferenceFound.emit(self.adId) - def resizeEvent(self, event): - self.adView.resize(self.size()) - super().resizeEvent(event) - -class AdView(QtWebEngineWidgets.QWebEngineView): +class _AdManager(QtCore.QObject): def __init__(self, parent=None): - super(AdView, self).__init__(parent=parent) - self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.setPage(AdPage(parent=self)) - self.currentAd = None - self.hide() - self.loadFinished.connect(self.showAd) - - def getAvailableAds(self, width, height): - availableAds = [] - for adSize in Config.SIZE_LIST: - if adSize[0] > width or adSize[1] > height: - continue - availableAds.append(adSize) - return availableAds - - def getAd(self, width, height): - availableAds = self.getAvailableAds(width, height) - if len(availableAds) == 0: - if self.currentAd != None: - self.currentAd = None - self.removeAd() - else: - if self.currentAd != availableAds[0]: - self.currentAd = availableAds[0] - self.loadAd(self.currentAd) + super(_AdManager, self).__init__(parent=parent) + self.cache = {} - def removeAd(self): - self.stop() - self.hide() + def setAd(self, adWidget): + if adWidget.adId not in self.cache: + cachedAd = CachedAd(adId=adWidget.adId, adSize=adWidget.adSize, parent=self) + cachedAd.noReferenceFound.connect(self._removeCache) + self.cache[adWidget.adId] = cachedAd + self.cache[adWidget.adId].addReference(id(adWidget)) + self.cache[adWidget.adId].adObject.setNewParent(adWidget) - def loadAd(self, size): - self.removeAd() - self.load(QtCore.QUrl(f"{Config.SERVER}?{Config.URL_QUERY.format(width=size[0], height=size[1])}")) + def removeAd(self, adWidget): + if adWidget.adId in self.cache: + self.cache[adWidget.adId].removeReference(id(adWidget)) - def showAd(self, success): - if success: - self.show() - - -class AdPage(QtWebEngineWidgets.QWebEnginePage): - def __init__(self, parent=None): - super(AdPage, self).__init__(parent=parent) - settings = self.settings() - settings.setAttribute(QtWebEngineWidgets.QWebEngineSettings.ShowScrollBars, False) - settings.setAttribute(QtWebEngineWidgets.QWebEngineSettings.ErrorPageEnabled, False) - self.setBackgroundColor(QtCore.Qt.transparent) - self.loadFinished.connect(self.setup) - - def setup(self, success): - if success: - self.runJavaScript("document.body.style.webkitUserSelect='none';document.body.style.webkitUserDrag='none';") - - def createWindow(self, type): - return PageClickHandler(parent=self) - - -class PageClickHandler(QtWebEngineWidgets.QWebEnginePage): - def __init__(self, parent=None): - super(PageClickHandler, self).__init__(parent=parent) - self.urlChanged.connect(self.urlChangeHandler) + def _removeCache(self, adId): + self.cache.pop(adId).setParent(None) - def urlChangeHandler(self, url): - QtGui.QDesktopServices.openUrl(url) - self.deleteLater() \ No newline at end of file +AdManager = _AdManager() \ No newline at end of file diff --git a/Services/Ad/AdView.py b/Services/Ad/AdView.py new file mode 100644 index 0000000..02bc884 --- /dev/null +++ b/Services/Ad/AdView.py @@ -0,0 +1,56 @@ +from .Config import Config + +from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets + + +class AdView(QtWebEngineWidgets.QWebEngineView): + def __init__(self, parent=None): + super(AdView, self).__init__(parent=parent) + self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) + self.setPage(AdPage(parent=self)) + + def setNewParent(self, widget): + if self.parent() != None: + self.parent().destroyed.disconnect(self.parentDestroyed) + widget.layout().addWidget(self) + self.parent().destroyed.connect(self.parentDestroyed) + + def parentDestroyed(self): + self.setParent(None) + + def getAd(self, width, height): + for adSize in Config.SIZE_LIST: + if adSize[0] > width or adSize[1] > height: + continue + self.loadAd(adSize) + return + + def loadAd(self, size): + self.load(QtCore.QUrl(f"{Config.SERVER}?{Config.URL_QUERY.format(width=size[0], height=size[1])}")) + + +class AdPage(QtWebEngineWidgets.QWebEnginePage): + def __init__(self, parent=None): + super(AdPage, self).__init__(parent=parent) + settings = self.settings() + settings.setAttribute(QtWebEngineWidgets.QWebEngineSettings.ShowScrollBars, False) + settings.setAttribute(QtWebEngineWidgets.QWebEngineSettings.ErrorPageEnabled, False) + self.setBackgroundColor(QtCore.Qt.transparent) + self.loadFinished.connect(self.setup) + + def setup(self, success): + if success: + self.runJavaScript("document.body.style.webkitUserSelect='none';document.body.style.webkitUserDrag='none';") + + def createWindow(self, type): + return PageClickHandler(parent=self) + + +class PageClickHandler(QtWebEngineWidgets.QWebEnginePage): + def __init__(self, parent=None): + super(PageClickHandler, self).__init__(parent=parent) + self.urlChanged.connect(self.urlChangeHandler) + + def urlChangeHandler(self, url): + QtGui.QDesktopServices.openUrl(url) + self.deleteLater() \ No newline at end of file diff --git a/Services/Ad/AdWidget.py b/Services/Ad/AdWidget.py new file mode 100644 index 0000000..92dba57 --- /dev/null +++ b/Services/Ad/AdWidget.py @@ -0,0 +1,26 @@ +from .AdManager import AdManager + +from PyQt5 import QtWidgets + + +class AdWidget(QtWidgets.QWidget): + def __init__(self, adId, adSize, responsive=False, parent=None): + super(AdWidget, self).__init__(parent=parent) + self.adId = adId + self.adSize = adSize + self.setLayout(QtWidgets.QHBoxLayout(self)) + self.layout().setContentsMargins(0, 0, 0, 0) + if responsive: + self.setMinimumSize(self.adSize) + else: + self.setFixedSize(self.adSize) + + def sizeHint(self): + return self.minimumSize() + + def showEvent(self, event): + AdManager.setAd(self) + super().showEvent(event) + + def __del__(self): + AdManager.removeAd(self) \ No newline at end of file diff --git a/Services/Ad/Config.py b/Services/Ad/Config.py index 95d939a..0e22770 100644 --- a/Services/Ad/Config.py +++ b/Services/Ad/Config.py @@ -2,4 +2,5 @@ class Config: SHOW = False SERVER = "" URL_QUERY = "" - SIZE_LIST = sorted([(728, 90), (300, 250), (320, 100), (320, 50)], key=lambda size: size[0] * size[1], reverse=True) \ No newline at end of file + SIZE_LIST = sorted([(728, 90), (300, 250), (320, 100), (320, 50)], key=lambda size: size[0] * size[1], reverse=True) + FREQUENCY = 10 \ No newline at end of file diff --git a/Services/Ad/__init__.py b/Services/Ad/__init__.py new file mode 100644 index 0000000..1c7762d --- /dev/null +++ b/Services/Ad/__init__.py @@ -0,0 +1,2 @@ +from .AdWidget import AdWidget +from .Config import Config \ No newline at end of file diff --git a/Services/Document.py b/Services/Document.py index 42a2305..fb4fbf5 100644 --- a/Services/Document.py +++ b/Services/Document.py @@ -16,12 +16,12 @@ def __init__(self, text="", action=None, role="accept", default=False): class DocumentData: - def __init__(self, contentId=None, date=None, title="", content="", contentType="text", modal=False, blockable=False, buttons=None): + def __init__(self, contentId=None, contentVersion=0, title="", content="", contentType="text", modal=False, blockExpiry=False, buttons=None): self.contentId = contentId - self.date = date + self.contentVersion = contentVersion self.title = title self.content = content self.contentType = contentType self.modal = modal - self.blockable = blockable + self.blockExpiry = blockExpiry self.buttons = buttons or [] \ No newline at end of file diff --git a/Services/Task/TaskManager.py b/Services/Task/TaskManager.py index 055fd5b..15b7e0f 100644 --- a/Services/Task/TaskManager.py +++ b/Services/Task/TaskManager.py @@ -1,5 +1,3 @@ -from .PrioritizedTask import TaskResult - from Services.Threading.MutexLocker import MutexLocker from Services.Threading.WaitCondition import WaitCondition @@ -34,7 +32,7 @@ def isPaused(self): class TaskManager(QtCore.QObject): - taskCompleteSignal = QtCore.pyqtSignal(TaskResult) + taskCompleteSignal = QtCore.pyqtSignal(object) def __init__(self, threadPool, parent=None): super(TaskManager, self).__init__(parent=parent) @@ -69,7 +67,7 @@ def _taskComplete(self, task): self._pausedCondition.makeTrue() elif len(self.tasks) == 0: self._doneCondition.makeTrue() - self.taskCompleteSignal.emit(task.result) + self.taskCompleteSignal.emit(task) @property def ifPaused(self): diff --git a/Services/Twitch/Gql/TwitchGqlAPI.py b/Services/Twitch/Gql/TwitchGqlAPI.py index 7f4bf5d..9eb2c7a 100644 --- a/Services/Twitch/Gql/TwitchGqlAPI.py +++ b/Services/Twitch/Gql/TwitchGqlAPI.py @@ -7,7 +7,7 @@ class Exceptions: class NetworkError(Exception): - def __init__(self, response): + def __init__(self, response=None): if response == None: self.status_code = None self.data = "Unable to connect to server" @@ -44,7 +44,7 @@ def api(self, operation, variables, headers=None): try: response = Network.session.post(Config.SERVER, headers=headers or {"Client-ID": Config.CLIENT_ID}, json=payload) except: - raise Exceptions.NetworkError(None) + raise Exceptions.NetworkError if response.status_code == 200: try: json = response.json() diff --git a/Services/Twitch/Gql/TwitchGqlModels.py b/Services/Twitch/Gql/TwitchGqlModels.py index 98ba82d..6474a08 100644 --- a/Services/Twitch/Gql/TwitchGqlModels.py +++ b/Services/Twitch/Gql/TwitchGqlModels.py @@ -1,6 +1,18 @@ import pytz -from datetime import datetime, timedelta +import datetime + + +class _datetime(datetime.datetime): + def __str__(self): + return self.strftime("%Y-%m-%d %H:%M:%S") + + def __repr__(self): + return self.__str__() + + def details(self): + return f"{self.__str__()} {self.tzname()} ({self.tzinfo.zone})" +datetime.datetime = _datetime class DataUtils: @@ -8,6 +20,7 @@ class DataUtils: def cleanString(string): return string.replace("\n", "").replace("\r", "") + class TimeUtils: class Datetime: DEFAULT_DATETIME = "0001-01-01T00:00:00Z" @@ -16,12 +29,12 @@ def __init__(self, string): if string == None: string = self.DEFAULT_DATETIME try: - self.datetime = datetime.strptime(string, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=pytz.utc) + self.datetime = pytz.utc.localize(datetime.datetime.strptime(string, "%Y-%m-%dT%H:%M:%S.%fZ")) except: try: - self.datetime = datetime.strptime(string, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.utc) + self.datetime = pytz.utc.localize(datetime.datetime.strptime(string, "%Y-%m-%dT%H:%M:%SZ")) except: - self.datetime = datetime.strptime(self.DEFAULT_DATETIME, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.utc) + self.datetime = pytz.utc.localize(datetime.datetime.strptime(self.DEFAULT_DATETIME, "%Y-%m-%dT%H:%M:%SZ")) def __str__(self): return self.datetime.__str__() @@ -46,7 +59,7 @@ def time(self, tzinfo=None): class Duration: def __init__(self, seconds): - self.timedelta = timedelta(seconds=seconds) + self.timedelta = datetime.timedelta(seconds=seconds) def totalSeconds(self): return int(self.timedelta.total_seconds()) @@ -74,9 +87,7 @@ def __init__(self, data): self.createdAt = TimeUtils.Datetime(data.get("createdAt")) def formattedName(self): - if self.id == None: - return "Unknown User" - if self.displayName.lower() == self.login: + if self.displayName.lower() == self.login.lower(): return self.displayName else: return f"{self.displayName}({self.login})" diff --git a/Ui/DocumentView.py b/Ui/DocumentView.py index 90a5333..eada827 100644 --- a/Ui/DocumentView.py +++ b/Ui/DocumentView.py @@ -31,14 +31,16 @@ class DocumentView(QtWidgets.QWidget, UiFile.documentView): def __init__(self, document, parent=None): super(DocumentView, self).__init__(parent=parent) self.contentId = document.contentId - self.date = document.date + self.contentVersion = document.contentVersion self.setTitle(document.title) self.setContent(document.content, document.contentType) self.setModal(document.modal) - self.setBlockable(document.blockable) + self.setBlockExpiry(False if self.contentId == None else document.blockExpiry) for button in document.buttons: self.addButton(button) self.buttonBox.accepted.connect(self.requestClose) + if self.isBlockable(): + self.buttonBox.accepted.connect(self.checkContentBlock) self.buttonBox.rejected.connect(self.requestClose) def setTitle(self, title): @@ -57,9 +59,13 @@ def setContent(self, content, contentType): def setModal(self, modal): self.modal = modal - def setBlockable(self, blockable): - self.blockable = blockable - if self.blockable: + def setBlockExpiry(self, blockExpiry): + self.blockExpiry = blockExpiry + if self.isBlockable(): + if self.blockExpiry == None: + self.checkBox.setText(T("#Do not show this again.")) + else: + self.checkBox.setText(T("#Do not show this again for {blockExpiry} days.", blockExpiry=blockExpiry)) self.checkBox.show() else: self.checkBox.hide() @@ -73,7 +79,7 @@ def isModal(self): return self.modal def isBlockable(self): - return self.blockable + return self.blockExpiry != False def accept(self): self.buttonBox.accepted.emit() @@ -82,4 +88,8 @@ def reject(self): self.buttonBox.rejected.emit() def requestClose(self): - self.closeRequested.emit(self) \ No newline at end of file + self.closeRequested.emit(self) + + def checkContentBlock(self): + if self.checkBox.isChecked(): + DB.temp.blockContent(self.contentId, self.contentVersion, self.blockExpiry) \ No newline at end of file diff --git a/Ui/DownloadMenu.py b/Ui/DownloadMenu.py index f4c4f9d..e987679 100644 --- a/Ui/DownloadMenu.py +++ b/Ui/DownloadMenu.py @@ -30,8 +30,8 @@ def loadOptions(self): elif self.downloadInfo.type.isVideo(): self.setupCropArea() h, m, s = Utils.toTime(self.downloadInfo.videoData.lengthSeconds.totalSeconds()) - self.startSpinH.setMaximum(h + 1) - self.endSpinH.setMaximum(h + 1) + self.fromSpinH.setMaximum(h + 1) + self.toSpinH.setMaximum(h + 1) self.reloadCropArea() self.unmuteVideoCheckBox.setChecked(self.downloadInfo.isUnmuteVideoEnabled()) self.unmuteVideoCheckBox.toggled.connect(self.downloadInfo.setUnmuteVideoEnabled) @@ -71,37 +71,37 @@ def setResolution(self, index): def setupCropArea(self): self.cropArea.setTitle(f"{T('crop')} / {T('#Total Length: {duration}', duration=self.downloadInfo.videoData.lengthSeconds)}") - self.startCheckBox.stateChanged.connect(self.reloadCropArea) - self.endCheckBox.stateChanged.connect(self.checkUpdateTrack) - self.startSpinH.valueChanged.connect(self.reloadStartRange) - self.startSpinM.valueChanged.connect(self.reloadStartRange) - self.startSpinS.valueChanged.connect(self.reloadStartRange) - self.endSpinH.valueChanged.connect(self.reloadEndRange) - self.endSpinM.valueChanged.connect(self.reloadEndRange) - self.endSpinS.valueChanged.connect(self.reloadEndRange) + self.cropFromStartRadioButton.toggled.connect(self.reloadCropArea) + self.cropToEndRadioButton.toggled.connect(self.checkUpdateTrack) + self.fromSpinH.valueChanged.connect(self.reloadStartRange) + self.fromSpinM.valueChanged.connect(self.reloadStartRange) + self.fromSpinS.valueChanged.connect(self.reloadStartRange) + self.toSpinH.valueChanged.connect(self.reloadEndRange) + self.toSpinM.valueChanged.connect(self.reloadEndRange) + self.toSpinS.valueChanged.connect(self.reloadEndRange) self.cropInfo.clicked.connect(self.showCropInfo) def reloadStartRange(self): - self.setStartSpin(*self.checkCropRange(*self.getStartSpin(), maximum=Utils.toSeconds(*self.getEndSpin()))) + self.setFromSpin(*self.checkCropRange(*self.getFromSpin(), maximum=Utils.toSeconds(*self.getToSpin()))) def reloadEndRange(self): - self.setEndSpin(*self.checkCropRange(*self.getEndSpin(), minimum=Utils.toSeconds(*self.getStartSpin()))) + self.setToSpin(*self.checkCropRange(*self.getToSpin(), minimum=Utils.toSeconds(*self.getFromSpin()))) - def getStartSpin(self): - return self.startSpinH.value(), self.startSpinM.value(), self.startSpinS.value() + def getFromSpin(self): + return self.fromSpinH.value(), self.fromSpinM.value(), self.fromSpinS.value() - def getEndSpin(self): - return self.endSpinH.value(), self.endSpinM.value(), self.endSpinS.value() + def getToSpin(self): + return self.toSpinH.value(), self.toSpinM.value(), self.toSpinS.value() - def setStartSpin(self, h, m, s): - self.startSpinH.setValueSilent(h) - self.startSpinM.setValueSilent(m) - self.startSpinS.setValueSilent(s) + def setFromSpin(self, h, m, s): + self.fromSpinH.setValueSilent(h) + self.fromSpinM.setValueSilent(m) + self.fromSpinS.setValueSilent(s) - def setEndSpin(self, h, m, s): - self.endSpinH.setValueSilent(h) - self.endSpinM.setValueSilent(m) - self.endSpinS.setValueSilent(s) + def setToSpin(self, h, m, s): + self.toSpinH.setValueSilent(h) + self.toSpinM.setValueSilent(m) + self.toSpinS.setValueSilent(s) def checkCropRange(self, h, m, s, maximum=None, minimum=None): videoTotalSeconds = self.downloadInfo.videoData.lengthSeconds.totalSeconds() @@ -120,20 +120,20 @@ def checkCropRange(self, h, m, s, maximum=None, minimum=None): return Utils.toTime(totalSeconds) def reloadCropArea(self): - self.startTimeBar.setEnabled(not self.startCheckBox.isChecked()) - self.endTimeBar.setEnabled(not self.endCheckBox.isChecked()) - if self.startCheckBox.isChecked(): - self.setStartSpin(0, 0, 0) - if self.endCheckBox.isChecked(): - self.setEndSpin(*Utils.toTime(self.downloadInfo.videoData.lengthSeconds.totalSeconds())) + self.fromTimeBar.setEnabled(not self.cropFromStartRadioButton.isChecked()) + self.toTimeBar.setEnabled(not self.cropToEndRadioButton.isChecked()) + if self.cropFromStartRadioButton.isChecked(): + self.setFromSpin(0, 0, 0) + if self.cropToEndRadioButton.isChecked(): + self.setToSpin(*Utils.toTime(self.downloadInfo.videoData.lengthSeconds.totalSeconds())) def checkUpdateTrack(self): self.reloadCropArea() - if self.updateTrackCheckBox.isChecked() and not self.endCheckBox.isChecked(): + if self.updateTrackCheckBox.isChecked() and not self.cropToEndRadioButton.isChecked(): if self.ask("warning", "#Update track mode is currently enabled.\nSetting the end of the crop range will not track updates.\nProceed?", defaultOk=True): self.updateTrackCheckBox.setCheckState(QtCore.Qt.Unchecked) else: - self.endCheckBox.setCheckState(QtCore.Qt.Checked) + self.cropToEndRadioButton.setCheckState(QtCore.Qt.Checked) def showCropInfo(self): self.info("video-crop", "#Video crop is based on the closest point in the crop range that can be processed.") @@ -158,9 +158,9 @@ def showPrioritizeInfo(self): def setUpdateTrack(self, updateTrack): self.downloadInfo.setUpdateTrackEnabled(updateTrack) - if self.updateTrackCheckBox.isChecked() and not self.endCheckBox.isChecked(): + if self.updateTrackCheckBox.isChecked() and not self.cropToEndRadioButton.isChecked(): if self.ask("warning", "#The end of the crop range is currently set.\nEnabling update track mode will ignore the end of the crop range and continue downloading.\nProceed?", defaultOk=True): - self.endCheckBox.setCheckState(QtCore.Qt.Checked) + self.cropToEndRadioButton.setCheckState(QtCore.Qt.Checked) else: self.updateTrackCheckBox.setCheckState(QtCore.Qt.Unchecked) @@ -203,12 +203,12 @@ def accept(self): super().accept(self.downloadInfo) def saveCropRange(self): - if self.startCheckBox.isChecked(): + if self.cropFromStartRadioButton.isChecked(): start = None else: - start = Utils.toSeconds(*self.getStartSpin()) - if self.endCheckBox.isChecked(): + start = Utils.toSeconds(*self.getFromSpin()) + if self.cropToEndRadioButton.isChecked(): end = None else: - end = Utils.toSeconds(*self.getEndSpin()) + end = Utils.toSeconds(*self.getToSpin()) self.downloadInfo.setCropRange(start, end) \ No newline at end of file diff --git a/Ui/DownloadPreview.py b/Ui/DownloadPreview.py index 1e04051..f1054f0 100644 --- a/Ui/DownloadPreview.py +++ b/Ui/DownloadPreview.py @@ -1,12 +1,10 @@ from Core.Ui import * from Services.Messages import Messages -from Services.Threading.MutexLocker import MutexLocker from Download.DownloadManager import DownloadManager class DownloaderControl: def __init__(self): - self.actionLock = MutexLocker() self._removeEnabled = False self._removeRegistered = False @@ -95,7 +93,6 @@ def connectDownloader(self): self.downloader.progressUpdate.connect(self.handleClipProgress) self.handleClipStatus(self.downloader.status) self.handleClipProgress(self.downloader.progress) - self.downloader.start() def tryRemoveDownloader(self): if self.control.isRemoveEnabled(): @@ -106,12 +103,11 @@ def tryRemoveDownloader(self): self.downloader.cancel() else: return - self.setEnabled(False) - with self.control.actionLock: - if self.control.isRemoveEnabled(): - self.removeDownloader() - else: - self.control.registerRemove() + if self.control.isRemoveEnabled(): + self.removeDownloader() + else: + self.setEnabled(False) + self.control.registerRemove() def removeDownloader(self): DownloadManager.remove(self.downloaderId) @@ -241,13 +237,11 @@ def handleDownloadResult(self): self.progressBar.setValue(100) self.pauseButton.hide() self.cancelButton.hide() - self.showResult() - with self.control.actionLock: - self.control.enableRemove() - if self.control.isRemoveRegistered(): - self.removeDownloader() - def showResult(self): + def processCompleteEvent(self): + self.control.enableRemove() + if self.control.isRemoveRegistered(): + self.removeDownloader() if self.downloader.status.terminateState.isTrue(): error = self.downloader.status.getError() if error != None: diff --git a/Ui/Downloads.py b/Ui/Downloads.py index 2a9507a..8f9524d 100644 --- a/Ui/Downloads.py +++ b/Ui/Downloads.py @@ -36,9 +36,15 @@ def downloaderDestroyed(self, downloaderId): self.previewWidgetView.takeItem(self.previewWidgetView.row(self.previewItems.pop(downloaderId))) self.showPreviewCount() + def downloadStarted(self, downloaderId): + self.processPreview(downloaderId) + def downloadCompleted(self, downloaderId): self.processPreview(downloaderId) + def processCompleteEvent(self, downloaderId): + self.previewItems[downloaderId].widget.processCompleteEvent() + def processPreview(self, downloaderId): self.setPreviewHidden(downloaderId, not self.filterPreview(downloaderId)) self.showPreviewCount() diff --git a/Ui/Home.py b/Ui/Home.py index 4661a19..d07f7a5 100644 --- a/Ui/Home.py +++ b/Ui/Home.py @@ -32,9 +32,9 @@ def startSearch(self, mode): if searchResult != False: if type(searchResult) == ExternalPlaylist.ExternalPlaylist: if searchResult.type.isStream(): - data = TwitchGqlModels.Stream({"title": "Unknown Stream", "broadcaster": {"login": "Unknown User"}}) + data = TwitchGqlModels.Stream({"title": "Unknown Stream", "game": {"name": "Unknown"}, "broadcaster": {"login": "Unknown User"}}) else: - data = TwitchGqlModels.Video({"title": "Unknown Video", "owner": {"login": "Unknown User"}, "lengthSeconds": searchResult.totalSeconds}) + data = TwitchGqlModels.Video({"title": "Unknown Video", "game": {"name": "Unknown"}, "owner": {"login": "Unknown User"}, "lengthSeconds": searchResult.totalSeconds}) downloadInfo = Ui.DownloadMenu(DownloadInfo(data, searchResult), viewOnly=True, parent=self).exec() if downloadInfo != False: DownloadManager.create(downloadInfo) diff --git a/Ui/MainWindow.py b/Ui/MainWindow.py index 9e9a390..e4926f6 100644 --- a/Ui/MainWindow.py +++ b/Ui/MainWindow.py @@ -97,7 +97,8 @@ def setup(self): ) if Updater.status.isOperational(): for notification in Updater.status.notifications: - self.document.showDocument(notification, icon=None if notification.modal else Icons.NOTICE_ICON) + if notification.blockExpiry == False or not DB.temp.isContentBlocked(notification.contentId, notification.contentVersion): + self.document.showDocument(notification, icon=None if notification.modal else Icons.NOTICE_ICON) if DB.setup.getTermsOfServiceAgreement() == None: self.openTermsOfService() else: diff --git a/Ui/Operators/DownloadsPage.py b/Ui/Operators/DownloadsPage.py index d30d3bc..d9b1017 100644 --- a/Ui/Operators/DownloadsPage.py +++ b/Ui/Operators/DownloadsPage.py @@ -23,7 +23,10 @@ def __init__(self, pageObject, parent=None): self.addTab(self.downloads, icon=Icons.FOLDER_ICON, closable=False) DownloadManager.createdSignal.connect(self.downloaderCreated) DownloadManager.destroyedSignal.connect(self.downloaderDestroyed) + DownloadManager.startedSignal.connect(self.downloadStarted) DownloadManager.completedSignal.connect(self.downloadCompleted) + DownloadManager.completedSignal.connect(self.processCompleteEvent, QtCore.Qt.QueuedConnection) + DownloadManager.completedSignal.connect(self.performDownloadCompleteAction, QtCore.Qt.QueuedConnection) DownloadManager.runningCountChangedSignal.connect(self.changePageText) def openDownloadTab(self, downloaderId): @@ -42,20 +45,27 @@ def downloaderCreated(self, downloaderId): self.downloads.downloaderCreated(downloaderId) if DB.general.isOpenProgressWindowEnabled(): self.openDownloadTab(downloaderId) + DownloadManager.get(downloaderId).start() def downloaderDestroyed(self, downloaderId): self.downloads.downloaderDestroyed(downloaderId) self.closeDownloadTab(downloaderId) + def downloadStarted(self, downloaderId): + self.downloads.downloadStarted(downloaderId) + def downloadCompleted(self, downloaderId): self.downloads.downloadCompleted(downloaderId) - if not DownloadManager.isDownloaderRunning(): - self.performDownloadCompleteAction() + + def processCompleteEvent(self, downloaderId): + self.downloads.processCompleteEvent(downloaderId) def changePageText(self, downloadersCount): self.pageObject.setPageName("" if downloadersCount == 0 else str(downloadersCount)) def performDownloadCompleteAction(self): + if DownloadManager.isDownloaderRunning(): + return if self.downloads.downloadCompleteAction.currentIndex() == DownloadCompleteAction.SHUTDOWN_APP: if TimedMessageBox( T("warning"), @@ -73,7 +83,6 @@ def performDownloadCompleteAction(self): time=Config.SYSTEM_SHUTDOWN_TIMEOUT, parent=self ) - dialog.setMinimumSize(self.size() / 2) dialog.exec() if not dialog.wasCanceled(): self.systemShutdownRequested.emit() \ No newline at end of file diff --git a/Ui/Operators/TermsOfService.py b/Ui/Operators/TermsOfService.py index a053852..c4541ab 100644 --- a/Ui/Operators/TermsOfService.py +++ b/Ui/Operators/TermsOfService.py @@ -11,7 +11,6 @@ def __init__(self, parent=None): super(TermsOfService, self).__init__(DocumentData(title=T("terms-of-service"), content=Utils.getDoc("TermsOfService.txt", DB.localization.getLanguage(), appName=Config.APP_NAME)), parent=parent) if DB.setup.getTermsOfServiceAgreement() == None: self.setModal(True) - self.checkBox.show() okButton = self.addButton( DocumentButtonData( text=T("ok"), @@ -33,14 +32,14 @@ def __init__(self, parent=None): self.buttonBox.accepted.connect(self.termsOfServiceAccepted) self.buttonBox.rejected.connect(self.appShutdownRequested) else: - self.checkBox.show() self.checkBox.setEnabled(False) self.checkBox.setChecked(True) - self.checkBox.setText(T("#Agreed at {time}", time=str(DB.setup.getTermsOfServiceAgreement()).split(".")[0])) + self.checkBox.setText(T("#Agreed at {time}", time=DB.setup.getTermsOfServiceAgreement().strftime("%Y-%m-%d %H:%M:%S"))) self.addButton( DocumentButtonData( text=T("ok"), role="accept", default=True ) - ) \ No newline at end of file + ) + self.checkBox.show() \ No newline at end of file diff --git a/Ui/PropertyView.py b/Ui/PropertyView.py index 027946b..b4bc739 100644 --- a/Ui/PropertyView.py +++ b/Ui/PropertyView.py @@ -28,15 +28,21 @@ def __init__(self, windowTitle, targetVideoWidget, formData, enableLabelTranslat def setFormData(self): for key, value in self.formData.items(): if not isinstance(key, QtCore.QObject): - if not isinstance(key, str): - key = str(key) - key = QtWidgets.QLabel(T(key) if self.enableLabelTranslation else key) - if self.targetVideoWidget != None: - key.setText(f"{key.text()}:") + if isinstance(key, str): + if self.enableLabelTranslation: + key = T(key) + if self.targetVideoWidget != None: + key = f"{key}:" + label = QtWidgets.QLabel() + label.setText(key) + key = label if not isinstance(value, QtCore.QObject): - if not isinstance(value, str): - value = str(value) - value = QtWidgets.QLabel(T(value) if self.enableFieldTranslation else value) + if isinstance(value, str): + if self.enableFieldTranslation: + value = T(value) + label = QtWidgets.QLabel() + label.setText(value) + value = label if self.targetVideoWidget != None: value.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Preferred) if self.enableLabelSelection and type(key) == QtWidgets.QLabel: diff --git a/Ui/Search.py b/Ui/Search.py index 10cdaa3..a6fd5bd 100644 --- a/Ui/Search.py +++ b/Ui/Search.py @@ -6,6 +6,13 @@ class Search(QtWidgets.QDialog, UiFile.search): + SEARCH_MESSAGE = { + SearchModes.CHANNEL: "#Checking channel info", + SearchModes.VIDEO: "#Checking video info", + SearchModes.CLIP: "#Checking clip info", + SearchModes.URL: "#Checking URL" + } + def __init__(self, mode, parent=None): super(Search, self).__init__(parent=parent) self.mode = mode @@ -51,20 +58,13 @@ def checkText(self): def accept(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) query = self.getCurrentQuery() - if self.mode.isVideo() and not query.isnumeric(): - mode = SearchModes(SearchModes.CLIP) - else: - mode = self.mode - self.searchProgress.setText(T({ - mode.CHANNEL: "#Checking channel info", - mode.VIDEO: "#Checking video info", - mode.CLIP: "#Checking clip info", - mode.URL: "#Checking URL" - }[mode.getMode()], ellipsis=True)) + if self.mode.isVideo() or self.mode.isClip(): + self.mode.setMode(SearchModes.VIDEO if query.isnumeric() else SearchModes.CLIP) + self.searchProgress.setText(T(self.SEARCH_MESSAGE[self.mode.getMode()], ellipsis=True)) self.queryArea.setCurrentIndex(2) self.searchThread.setup( target=Engine.Search.Query, - args=(mode, query), + args=(self.mode, query), kwargs={"searchExternalContent": DB.advanced.isExternalContentUrlEnabled()}, ) self.searchThread.start() diff --git a/Ui/SearchResult.py b/Ui/SearchResult.py index 91cfa3f..7f50c10 100644 --- a/Ui/SearchResult.py +++ b/Ui/SearchResult.py @@ -204,9 +204,17 @@ def addVideos(self, videos): videoDownloadWidget = Ui.VideoDownloadWidget(data, resizable=False, parent=self) videoDownloadWidget.accountPageShowRequested.connect(self.accountPageShowRequested.emit) self.addWidget(videoDownloadWidget) - if AdManager.Config.SHOW: - if self.videoArea.count() % 6 == 1: - self.addWidget(AdManager.Ad(minimumSize=videoDownloadWidget.sizeHint(), responsive=False, parent=self), fitContent = False) + if Ad.Config.SHOW: + if self.videoArea.count() % Ad.Config.FREQUENCY == 1: + self.addWidget( + Ad.AdWidget( + adId=f"videoArea.{self.videoArea.count() // Ad.Config.FREQUENCY}", + adSize=videoDownloadWidget.sizeHint(), + responsive=False, + parent=self + ), + fitContent=False + ) def addWidget(self, widget, fitContent=True): widget.setContentsMargins(10, 10, 10, 10) diff --git a/Ui/Settings.py b/Ui/Settings.py index 0f0c51f..c7b52b2 100644 --- a/Ui/Settings.py +++ b/Ui/Settings.py @@ -114,7 +114,13 @@ def getTimeInfo(self, timeType): return { f"{{{timeType}_at}}": f"{T(f'{timeType}-at')} (XXXX-XX-XX XX:XX:XX)", "{date}": f"{T(f'{timeType}-date')} (XXXX-XX-XX)", - "{time}": f"{T(f'{timeType}-time')} (XX:XX:XX)" + "{year}": f"{T(f'{timeType}-date')} - {T('year')}", + "{month}": f"{T(f'{timeType}-date')} - {T('month')}", + "{day}": f"{T(f'{timeType}-date')} - {T('day')}", + "{time}": f"{T(f'{timeType}-time')} (XX:XX:XX)", + "{hour}": f"{T(f'{timeType}-time')} - {T('hour')}", + "{minute}": f"{T(f'{timeType}-time')} - {T('minute')}", + "{second}": f"{T(f'{timeType}-time')} - {T('second')}" } def showStreamTemplateInfo(self): diff --git a/requirements.txt b/requirements.txt index f7ddca3..df70f70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -PyQt5==5.15.6 -PyQtWebEngine==5.15.5 +PyQt5==5.15.7 +PyQtWebEngine==5.15.6 pytz==2022.1 -requests==2.27.1 -selenium==4.1.5 -webdriver-manager==3.5.4 \ No newline at end of file +requests==2.28.0 +selenium==4.2.0 +webdriver-manager==3.7.0 \ No newline at end of file diff --git a/resources/translations/KeywordTranslations.json b/resources/translations/KeywordTranslations.json index 870c88b..8da2b86 100644 --- a/resources/translations/KeywordTranslations.json +++ b/resources/translations/KeywordTranslations.json @@ -387,6 +387,30 @@ "en": "Created Time", "ko": "생성시간" }, + "year": { + "en": "Year", + "ko": "년" + }, + "month": { + "en": "Month", + "ko": "월" + }, + "day": { + "en": "Day", + "ko": "일" + }, + "hour": { + "en": "Hour", + "ko": "시" + }, + "minute": { + "en": "Minute", + "ko": "분" + }, + "second": { + "en": "Second", + "ko": "초" + }, "views": { "en": "Views", "ko": "조회수" diff --git a/resources/translations/Translations.json b/resources/translations/Translations.json index 4b40401..d4046fb 100644 --- a/resources/translations/Translations.json +++ b/resources/translations/Translations.json @@ -23,6 +23,14 @@ "en": "A new version of {appName} has been released!", "ko": "{appName}의 새로운 버전이 출시되었습니다!" }, + "#Do not show this again.": { + "en": "Do not show this again.", + "ko": "다시 보지 않기" + }, + "#Do not show this again for {blockExpiry} days.": { + "en": "Do not show this again for {blockExpiry} days.", + "ko": "{blockExpiry}일 동안 다시 보지 않기" + }, "#There are one or more downloads in progress.\nAre you sure you want to stop/cancel and exit?": { "en": "There are one or more downloads in progress.\nAre you sure you want to stop/cancel and exit?", "ko": "하나 이상의 진행 중인 다운로드가 있습니다.\n중지/취소하고 종료하시겠습니까?" diff --git a/resources/ui/documentView.ui b/resources/ui/documentView.ui index e298f82..3ce8614 100644 --- a/resources/ui/documentView.ui +++ b/resources/ui/documentView.ui @@ -52,24 +52,6 @@ - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - diff --git a/resources/ui/download.ui b/resources/ui/download.ui index 9a2634e..3cf3402 100644 --- a/resources/ui/download.ui +++ b/resources/ui/download.ui @@ -881,7 +881,7 @@ border-radius: 10px; - + Qt::Horizontal diff --git a/resources/ui/downloadMenu.ui b/resources/ui/downloadMenu.ui index 5fb1878..feca470 100644 --- a/resources/ui/downloadMenu.ui +++ b/resources/ui/downloadMenu.ui @@ -191,9 +191,15 @@ Crop + + 20 + + + 20 + 0 @@ -206,15 +212,23 @@ 0 - - - - - 0 - 0 - + + + + From + + + + + + + To - + + + + + 0 @@ -228,114 +242,142 @@ 0 - - - - 0 - 0 - - - - - 40 - 0 - - - - -1 - - - - - - - - 12 - - + - : + End + + + true - + - + 0 0 - - - 40 - 0 - - - - -1 - - - - - - - - 12 - - - : + - - + + - + 0 0 - - - 40 - 0 - - - - -1 - + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 40 + 0 + + + + -1 + + + + + + + + 12 + + + + : + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + -1 + + + + + + + + 12 + + + + : + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + + + + -1 + + + + - - - - - 80 - 0 - - - - To End - - - true - - - - - - - - 0 - 0 - - - + + + 0 @@ -349,108 +391,136 @@ 0 - - - - 0 - 0 - - - - - 40 - 0 - - - - -1 - - - - - - - - 12 - - + - : + Start + + + true - + - + 0 0 - - - 40 - 0 - - - - -1 - - - - - - - - 12 - - - : + - - + + - + 0 0 - - - 40 - 0 - - - - - - - -1 - + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 40 + 0 + + + + -1 + + + + + + + + 12 + + + + : + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + -1 + + + + + + + + 12 + + + + : + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + -1 + + + + - - - - - 80 - 0 - - - - From Start - - - true - - - diff --git a/resources/ui/translators/ko/documentView.qm b/resources/ui/translators/ko/documentView.qm index fb8954c76cb48d4e00d472d25072119c57319428..690da26bc394cf220b39c15738204a9f6706eb1f 100644 GIT binary patch delta 37 pcmbQmIE`_FnzRF_Hd`J85M%;z5;Kqx*!1Mk9R`IxUnaVS0sxoj3Hbm3 delta 41 tcmbQnIE!(Dn!F3AHd`J85M%;z8Z(d(+4SVl9R`IfF^BG4jhN^d3INE@3{U_7 diff --git a/resources/ui/translators/ko/downloadMenu.qm b/resources/ui/translators/ko/downloadMenu.qm index 041d4998a16d495b1c3a55ca1e5117d8b65a9a6a..854edf72780b02d1818a26096f9a3e891fe43683 100644 GIT binary patch delta 238 zcmbQm_JVDKTxJIY1A`S10|D!L1_ma51_mD&1_nkM29}ixK*2l)mYcCaej)?Q8v`KS z#Spb(3Q!$4)68x1KstnJuD&&pZfBX9Hx;PfmF3#x7N9yCHcOR%K>7jO)K{!Pdi}(F z1=dWU!aW=3C@=~zZHVOns%BsVVrJL8l*wxtO(nSw-8nvyLE+?q69RBa7Pq4O+{ynK tCt9+cnQ`0)F3uWUl2}y24l=?eGchMWosk*H=GYL6rk^P!fATV>5&+qIHEjR@ delta 175 zcmaFCHj8b7TwnzQ1A`S1Gcd5MOaPL33@mR97#JAym}YL1XJB9wW16dP4WvC;X68)= z%Db{$o7@7Fmto!L;{v4LvstSA1JXy>roLhY(vv5)EAaBi6&?Yy9W#L%>o%@YV4VDh zQB{`X%#7ncdmAnqZ~)C=U;|<