diff --git a/syncplay/client.py b/syncplay/client.py index 54f12950..ababa59b 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -73,6 +73,9 @@ def __init__(self, playerClass, ui, config): constants.FOLDER_SEARCH_TIMEOUT = config['folderSearchTimeout'] constants.FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL = config['folderSearchDoubleCheckInterval'] constants.FOLDER_SEARCH_WARNING_THRESHOLD = config['folderSearchWarningThreshold'] + constants.WATCHED_SUBFOLDER = config['watchedSubfolder'] + constants.WATCHED_AUTOMOVE = config['watchedAutoMove'] if len(constants.WATCHED_SUBFOLDER) > 0 else False + constants.WATCHED_AUTOCREATESUBFOLDERS = config['watchedSubfolderAutocreate'] if len(constants.WATCHED_SUBFOLDER) > 0 else False self.controlpasswords = {} self.lastControlPasswordAttempt = None @@ -81,7 +84,9 @@ def __init__(self, playerClass, ui, config): self.serverFeatures = {} self.lastRewindTime = None + self._pendingWatchedMoves = [] self.lastUpdatedFileTime = None + self._lastWatchedMoveAttempt = 0.0 #Secs self.lastAdvanceTime = None self.fileOpenBeforeChangingPlaylistIndex = None self.waitingToLoadNewfile = False @@ -248,6 +253,16 @@ def updatePlayerStatus(self, paused, position): ): pauseChange = self._toggleReady(pauseChange, paused) + if self._pendingWatchedMoves and (time.time() - (self._lastWatchedMoveAttempt or 0.0)) >= constants.WATCHED_CHECKQUEUE_INTERVAL: + self._lastWatchedMoveAttempt = time.time() + try: + self._tryMovePendingWatchedFiles() + except Exception: + pass + self.currentlyPlayingFilename = self.userlist.currentUser.file["name"] if self.userlist.currentUser.file else None + playingCurrentIndex = not self.playlist._notPlayingCurrentIndex() + if playingCurrentIndex: + self.playlist.recordPlayedNearEOF(paused, position) if self._lastGlobalUpdate: self._lastPlayerUpdate = time.time() if (pauseChange or seeked) and self._protocol: @@ -263,6 +278,13 @@ def prepareToChangeToNewPlaylistItemAndRewind(self): self.fileOpenBeforeChangingPlaylistIndex = self.userlist.currentUser.file["path"] if self.userlist.currentUser.file else None self.waitingToLoadNewfile = True self.waitingToLoadNewfileSince = time.time() + position = self.getStoredPlayerPosition() + currentLength = self.userlist.currentUser.file["duration"] if self.userlist.currentUser.file else 0 + if ( + currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH and + abs(position - currentLength) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD + ): + self.markWatchedFilePendingMove() def prepareToAdvancePlaylist(self): if self.playlist.canSwitchToNextPlaylistIndex(): @@ -516,6 +538,97 @@ def getGlobalPaused(self): return True return self._globalPaused + def markWatchedFilePendingMove(self, oldFilePath=None): + self.playlist.lastNearEOFName = None + self.playlist.lastNearEOFPath = None + + try: + if oldFilePath: + currentFilePath = oldFilePath + else: + currentFile = self.userlist.currentUser.file if self.userlist and self.userlist.currentUser else None + currentFilePath = currentFile.get("path") if currentFile else None + if currentFilePath and not utils.isURL(currentFilePath) and currentFilePath not in self._pendingWatchedMoves: + self._pendingWatchedMoves.append(currentFilePath) + self.ui.showDebugMessage("Marked for watched move: {}".format(currentFilePath)) + except Exception as e: + self.ui.showDebugMessage("Could not mark watched file: {}".format(e)) + + def userInitiatedMarkWatched(self, fileSourcePath): + try: + directory = os.path.dirname(fileSourcePath) + filename = os.path.basename(fileSourcePath) + watchedDirectory = utils.getWatchedSubfolder(directory) + utils.createWatchedSubdirIfNeeded(watchedDirectory) + if not os.path.isdir(watchedDirectory): + self.ui.showErrorMessage("'{}' subfolder not found for this file.".format(constants.WATCHED_SUBFOLDER)) # TODO: Move to Language + return + watchedDirectoryFilepath = os.path.join(watchedDirectory, filename) + watchedDirectoryName = os.path.basename(os.path.dirname(watchedDirectoryFilepath)) + if os.path.isfile(watchedDirectoryFilepath): + self.ui.showErrorMessage(getMessage("cannot-move-file-due-to-name-conflict-error").format(watchedDirectoryName)) # TODO: Move to Language + return + utils.moveFile(fileSourcePath, watchedDirectoryFilepath) + self.fileSwitch.updateInfo() + self.ui.showMessage(getMessage("moved-file-to-subfolder-notification").format(fileSourcePath, watchedDirectoryName)) + except Exception as e: + self.ui.showErrorMessage("Could not mark as watched: {}".format(e)) # TODO: Move to language + + def userInitiatedMarkUnwatched(self, fileSourcePath): + try: + watchedDirectoryPath = os.path.dirname(fileSourcePath) + filename = os.path.basename(fileSourcePath) + if not utils.isWatchedSubfolder(watchedDirectoryPath): + self.ui.showErrorMessage("This file is not in a '{}' subfolder.".format(constants.WATCHED_SUBFOLDER)) + return + unwatchedDirectoryPath = utils.getUnwatchedParentfolder(watchedDirectoryPath) + unwatchedDirectoryPathName = os.path.basename(unwatchedDirectoryPath) + unwatchedFilePath = os.path.join(unwatchedDirectoryPath, filename) + if os.path.isfile(unwatchedFilePath): + self.ui.showErrorMessage("A file with the same name already exists in the parent folder.") + return + utils.moveFile(fileSourcePath, unwatchedFilePath) + self.fileSwitch.updateInfo() + self.ui.showMessage(getMessage("moved-file-to-subfolder-notification").format(fileSourcePath, unwatchedDirectoryPathName)) + except Exception as e: + self.ui.showErrorMessage("Could not mark as unwatched: {}".format(e)) # TODO: Move to Language + + def _tryMovePendingWatchedFiles(self): + if not constants.WATCHED_AUTOMOVE: + self._pendingWatchedMoves = [] + return + + if not self._pendingWatchedMoves: + return + + for pendingWatchedMove in list(self._pendingWatchedMoves): + try: + if not os.path.exists(pendingWatchedMove): + self._pendingWatchedMoves.remove(pendingWatchedMove) + continue + if not utils.canMarkAsWatched(pendingWatchedMove): + self._pendingWatchedMoves.remove(pendingWatchedMove) + continue + originalDir = os.path.dirname(pendingWatchedMove) + watchedDir = utils.getWatchedSubfolder(originalDir) + utils.createWatchedSubdirIfNeeded(watchedDir) + if not os.path.isdir(watchedDir): + self._pendingWatchedMoves.remove(pendingWatchedMove) + continue + destFilepath = os.path.join(watchedDir, os.path.basename(pendingWatchedMove)) + if os.path.exists(destFilepath): + self.ui.showErrorMessage(getMessage("cannot-move-file-due-to-name-conflict-error").format(pendingWatchedMove, constants.WATCHED_SUBFOLDER)) + self._pendingWatchedMoves.remove(pendingWatchedMove) + continue + utils.moveFile(pendingWatchedMove, destFilepath) + self.fileSwitch.updateInfo() + try: + self.ui.showMessage(getMessage("moved-file-to-subfolder-notification").format(pendingWatchedMove, constants.WATCHED_SUBFOLDER)) + self._pendingWatchedMoves.remove(pendingWatchedMove) + except Exception: + pass + except Exception as e: + self.ui.showDebugMessage("Deferring watched move for '{}': {}".format(pendingWatchedMove, e)) def eofReportedByPlayer(self): if self.playlist.notJustChangedPlaylist() and self.userlist.currentUser.file: self.ui.showDebugMessage("Fixing file duration to allow for playlist advancement") @@ -542,6 +655,7 @@ def updateFile(self, filename, duration, path): self.userlist.currentUser.setFile(filename, duration, size, path) self.sendFile() self.playlist.changeToPlaylistIndexFromFilename(filename) + self.playlist.doubleCheckForWatchedPreviousFile() def setTrustedDomains(self, newTrustedDomains): from syncplay.ui.ConfigurationGetter import ConfigurationGetter @@ -624,6 +738,7 @@ def openFile(self, filePath, resetPosition=False, fromUser=False): self.establishRewindDoubleCheck() self.lastRewindTime = time.time() self.autoplayCheck() + self.playlist.doubleCheckForWatchedPreviousFile() def fileSwitchFoundFiles(self): self.ui.fileSwitchFoundFiles() @@ -631,9 +746,11 @@ def fileSwitchFoundFiles(self): def setPlaylistIndex(self, index): self._protocol.setPlaylistIndex(index) + self.playlist.doubleCheckForWatchedPreviousFile() def changeToPlaylistIndex(self, *args, **kwargs): self.playlist.changeToPlaylistIndex(*args, **kwargs) + self.playlist.doubleCheckForWatchedPreviousFile() def loopSingleFiles(self): return self._config["loopSingleFiles"] or self.isPlayingMusic() @@ -914,6 +1031,16 @@ def stop(self, promptForAction=False): self.destroyProtocol() if self._player: self._player.drop() + + if self._pendingWatchedMoves: + for _ in range(constants.WATCHED_PLAYERWAIT_MAXRETRIES): + try: + self._tryMovePendingWatchedFiles() + if not self._pendingWatchedMoves: + break + except Exception: + pass + time.sleep(constants.WATCHED_PLAYERWAIT_INTERVAL) if self.ui: self.ui.drop() reactor.callLater(0.1, reactor.stop) @@ -1766,6 +1893,10 @@ def __init__(self, client): self.addedChangeListCallback = False self.switchToNewPlaylistItem = False self._lastPlaylistIndexChange = time.time() + self.lastNearEOFName = None + self.lastNearEOFPath = None + self.lastNearEOFFirstTime = 0.0 + self.lastNearEOFLastTime = 0.0 def needsSharedPlaylistsEnabled(f): # @NoSelf @wraps(f) @@ -1887,6 +2018,7 @@ def changeToPlaylistIndex(self, index, username=None, resetPosition=False): self._client._protocol.sendMessage({"State": state}) self._playerPaused = True self._client.autoplayCheck() + self.doubleCheckForWatchedPreviousFile() elif index is not None: filename = self._playlist[index] self._ui.setPlaylistIndexFilename(filename) @@ -2037,6 +2169,7 @@ def changePlaylist(self, files, username=None, resetIndex=False): else: self._ui.setPlaylist(self._playlist) self._ui.showMessage(getMessage("playlist-contents-changed-notification").format(username)) + self.doubleCheckForWatchedPreviousFile() def addToPlaylist(self, file): self.changePlaylist([*self._playlist, file]) @@ -2091,7 +2224,50 @@ def advancePlaylistCheck(self): abs(position - currentLength) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD and self.notJustChangedPlaylist() ): - self.loadNextFileInPlaylist() + self._client.markWatchedFilePendingMove() + self.loadNextFileInPlaylist() + + @needsSharedPlaylistsEnabled + def recordPlayedNearEOF(self, paused, position): + if not self._client.userlist.currentUser.file: + return + if position == None: + return + currentLength = self._client.userlist.currentUser.file["duration"] if self._client.userlist.currentUser.file else 0 + isPlaying = paused is False + remainingTime = currentLength - position + + if ( + isPlaying and + remainingTime < constants.PLAYLIST_NEAR_EOF_WINDOW and + currentLength > (constants.PLAYLIST_NEAR_EOF_WINDOW * 2) and # The EOF window must represent at least the last half of the video to avoid misdetection when playing start of a short file + currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH and + self.notJustChangedPlaylist() + ): + now_monotime = time.monotonic() + if self.lastNearEOFName != self._client.userlist.currentUser.file['name']: + self.lastNearEOFName = self._client.userlist.currentUser.file['name'] + self.lastNearEOFPath = self._client.userlist.currentUser.file['path'] + self.lastNearEOFFirstTime = now_monotime + self.lastNearEOFLastTime = now_monotime + + @needsSharedPlaylistsEnabled + def doubleCheckForWatchedPreviousFile(self): + if not self.lastNearEOFName or not self.lastNearEOFPath: + return False + + if self._playingSpecificFilename(self.lastNearEOFName): + return False + + now_monotime = time.monotonic() + dwell = max(0.0, self.lastNearEOFLastTime - self.lastNearEOFFirstTime) + if dwell < constants.PLAYLIST_NEAR_EOF_MIN_DWELL: + return False + + age = now_monotime - self.lastNearEOFLastTime + if age > constants.PLAYLIST_NEAR_EOF_LATCH_TTL: + return False + self._client.markWatchedFilePendingMove(self.lastNearEOFPath) def notJustChangedPlaylist(self): secondsSinceLastChange = time.time() - self._lastPlaylistIndexChange @@ -2132,6 +2308,12 @@ def _notPlayingCurrentIndex(self): self._ui.showDebugMessage("Not playing current index - Filename mismatch or no file") return True + def _playingSpecificFilename(self, filenameToCompare): + if self._client.userlist.currentUser.file: + return self._client.userlist.currentUser.file['name'] == filenameToCompare + else: + return False + def _thereIsNextPlaylistIndex(self): if self._playlistIndex is None: return False @@ -2210,6 +2392,7 @@ def checkForFileSwitchUpdate(self): if self.directorySearchError: self._client.ui.showErrorMessage(self.directorySearchError) self.directorySearchError = None + self._client.playlist.doubleCheckForWatchedPreviousFile() def updateInfo(self): if not self.currentlyUpdating and self.mediaDirectories: @@ -2278,13 +2461,14 @@ def findFilepath(self, filename, highPriority=False): return if self._client.userlist.currentUser.file and utils.sameFilename(filename, self._client.userlist.currentUser.file['name']): - return self._client.userlist.currentUser.file['path'] + return utils.getCorrectedPathForFile(self._client.userlist.currentUser.file['path']) if self.mediaFilesCache is not None: for directory in self.mediaFilesCache: files = self.mediaFilesCache[directory] if len(files) > 0 and filename in files: filepath = os.path.join(directory, filename) + filepath = utils.getCorrectedPathForFile(filepath) if os.path.isfile(filepath): return filepath @@ -2292,6 +2476,7 @@ def findFilepath(self, filename, highPriority=False): directoryList = self.mediaDirectories for directory in directoryList: filepath = os.path.join(directory, filename) + filepath = utils.getCorrectedPathForFile(filepath) if os.path.isfile(filepath): return filepath @@ -2313,7 +2498,13 @@ def getDirectoryOfFilenameInCache(self, filename): for directory in self.mediaFilesCache: files = self.mediaFilesCache[directory] if filename in files: - return directory + filepath = os.path.join(directory, filename) + if os.path.isfile(filepath): + return directory + watched_directory = os.path.join(directory, constants.WATCHED_SUBFOLDER) + watched_filepath = os.path.join(directory, constants.WATCHED_SUBFOLDER, filename) + if os.path.isfile(watched_filepath): + return watched_directory return None def isDirectoryInList(self, directoryToFind, folderList): diff --git a/syncplay/constants.py b/syncplay/constants.py index 32937f72..73cfdebf 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -45,6 +45,9 @@ def getValueForOS(constantDict): ['syncplay.pl:8999 (France)', 'syncplay.pl:8999']] PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH = 10 # Seconds PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD = 5 # Seconds (only triggered if file is paused, e.g. due to EOF) +PLAYLIST_NEAR_EOF_WINDOW = 270 # seconds +PLAYLIST_NEAR_EOF_MIN_DWELL = 120 # seconds +PLAYLIST_NEAR_EOF_LATCH_TTL = 60 # seconds EXECUTABLE_COMBOBOX_MINIMUM_LENGTH = 30 # Minimum number of characters that the combobox will make visible # Overriden by config @@ -110,6 +113,11 @@ def getValueForOS(constantDict): FOLDER_SEARCH_WARNING_THRESHOLD = 2.0 # Secs - how long until a warning saying how many files have been scanned FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL = 30.0 # Secs - Frequency of updating cache +# Changable values for watched features (you usually don't need to change these) +WATCHED_CHECKQUEUE_INTERVAL = 1.0 # Secs +WATCHED_PLAYERWAIT_INTERVAL = 0.1 # Secs +WATCHED_PLAYERWAIT_MAXRETRIES = 80 + # Usually there's no need to adjust these DOUBLE_CHECK_REWIND = True LAST_PAUSED_DIFF_THRESHOLD = 2 @@ -332,6 +340,10 @@ def getValueForOS(constantDict): VLC_EOF_DURATION_THRESHOLD = 2.0 +WATCHED_SUBFOLDER = "Watched" +WATCHED_AUTOMOVE = False +WATCHED_AUTOCREATESUBFOLDERS = False + PRIVACY_HIDDENFILENAME = "**Hidden filename**" INVERTED_STATE_MARKER = "*" ERROR_MESSAGE_MARKER = "*" diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index cf00bc10..1150bb6c 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -554,4 +554,20 @@ "playlist-empty-error": "Wiedergabeliste is aktuell leer.", "playlist-invalid-index-error": "Ungültiger Wiedergabelisten-Index", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Ordner", + "syncplay-watchedfiles-title": "Gesehene Dateien", + "syncplay-watchedautomove-label": "Gesehene Dateien automatisch in Unterordner verschieben (falls vorhanden)", + "syncplay-watchedmovesubfolder-label": "Unterordner für gesehene Dateien", + "syncplay-watchedsubfolderautocreate-label": "Unterordner für gesehene Dateien bei Bedarf automatisch erstellen", + "mark-as-watched-menu-label": "Als gesehen markieren", + "mark-as-unwatched-menu-label": "Als ungesehen markieren", + "watchedautomove-tooltip": "Verschiebt die Datei automatisch in den Unterordner für gesehene Dateien, wenn das Ende der Datei erreicht ist. Dies funktioniert nur, wenn der übergeordnete Ordner ein Medienordner ist.", + "watchedsubfolder-tooltip": "Unterordner (relativ zum Ordner der Datei), in den gesehene Dateien verschoben werden (der Unterordner muss existieren, damit die Datei verschoben werden kann). Dies funktioniert nur, wenn der übergeordnete Ordner ein Medienordner ist.", + "watchedsubfolderautocreate-tooltip": "Erstellt den Unterordner für gesehene Dateien beim Verschieben automatisch, falls er noch nicht existiert. Dies funktioniert nur, wenn der übergeordnete Ordner ein Medienordner ist.", + "cannot-move-file-due-to-name-conflict-error": "Konnte '{}' nicht in den Unterordner '{}' verschieben, da bereits eine Datei mit diesem Namen existiert.", # Path, subfolder + "moved-file-to-subfolder-notification": "Verschoben: '{}' in den Unterordner '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index f568cbf9..a9c9adab 100644 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -122,6 +122,8 @@ "media-player-latency-warning": "Warning: The media player took {} seconds to respond. If you experience syncing issues then close applications to free up system resources, and if that doesn't work then try a different media player.", # Seconds to respond "mpv-unresponsive-error": "mpv has not responded for {} seconds so appears to have malfunctioned. Please restart Syncplay.", # Seconds to respond + "moved-file-to-subfolder-notification": "Moved '{}' to '{}' subfolder.", # Path, subfolder + # Client prompts "enter-to-exit-prompt": "Press enter to exit\n", @@ -161,7 +163,7 @@ "feature-sharedPlaylists": "shared playlists", # used for not-supported-by-server-error "feature-chat": "chat", # used for not-supported-by-server-error - "feature-readiness": "readiness", # used for not-supported-by-server-error + "feature-readiness": "readiness", # used for not-supported-by-server-error" "feature-managedRooms": "managed rooms", # used for not-supported-by-server-error "feature-setOthersReadiness": "readiness override", # used for not-supported-by-server-error @@ -173,8 +175,8 @@ "invalid-offset-value": "Invalid offset value", "switch-file-not-found-error": "Could not switch to file '{0}'. Syncplay looks in specified media directories.", # File not found - "folder-search-timeout-error": "The search for media in media directories was aborted as it took too long to search through '{}' after having processed the first {:,} files. This will occur if you select a folder with too many sub-folders in your list of media folders to search through or if there are too many files to process. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder. If the folder is actually fine then you can re-enable it by selecting File->Set Media Directories and pressing 'OK'.", # Folder, Files processed. Note: {:,} is {} but with added commas seprators. - "folder-search-timeout-warning": "Warning: It has taken {} seconds to scan {:,} files in the folder '{}'. This will occur if you select a folder with too many sub-folders in your list of media folders to search through or if there are too many files to process.", # Folder, Files processed. Note: {:,} is {} but with added commas seprators. + "folder-search-timeout-error": "The search for media in media directories was aborted as it took too long to search through '{}' after having processed the first {:,} files. This will occur if you select a folder with too many subfolders in your list of media folders to search through or if there are too many files to process. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate subfolder. If the folder is actually fine then you can re-enable it by selecting File->Set Media Directories and pressing 'OK'.", # Folder, Files processed. Note: {:,} is {} but with added commas seprators. + "folder-search-timeout-warning": "Warning: It has taken {} seconds to scan {:,} files in the folder '{}'. This will occur if you select a folder with too many subfolders in your list of media folders to search through or if there are too many files to process.", # Folder, Files processed. Note: {:,} is {} but with added commas seprators. "folder-search-first-file-timeout-error": "The search for media in '{}' was aborted as it took too long to access the directory. This could happen if it is a network drive or if you configure your drive to spin down after a period of inactivity. For automatic file switching to work again please go to File->Set Media Directories and either remove the directory or resolve the issue (e.g. by changing power saving settings).", # Folder "added-file-not-in-media-directory-error": "You loaded a file in '{}' which is not a known media directory. You can add this as a media directory by selecting File->Set Media Directories in the menu bar.", # Folder "no-media-directories-error": "No media directories have been set. For shared playlist and file switching features to work properly please select File->Set Media Directories and specify where Syncplay should look to find media files.", @@ -182,6 +184,8 @@ "failed-to-load-server-list-error": "Failed to load public server list. Please visit https://www.syncplay.pl/ in your browser.", + "cannot-move-file-due-to-name-conflict-error": "Could not move '{}' to '{}' subfolder because a file with that name already exists.", # Path, folder + # Client arguments "argument-description": 'Solution to synchronize playback of multiple media player instances over the network.', "argument-epilog": 'If no options supplied _config values will be used', @@ -252,12 +256,19 @@ "automatic-language": "Default ({})", # Default language "showdurationnotification-label": "Warn about media duration mismatches", "basics-label": "Basics", + "folders-label": "Folders", # Directories "readiness-label": "Play/Pause", "misc-label": "Misc", "core-behaviour-title": "Core room behaviour", "syncplay-internals-title": "Syncplay internals", "syncplay-mediasearchdirectories-title": "Directories to search for media", "syncplay-mediasearchdirectories-label": "Directories to search for media (one path per line)", + "syncplay-watchedfiles-title": "Watched files", + "syncplay-watchedautomove-label": "Automatically move watched files to subfolder (if subfolder exists)", + "syncplay-watchedmovesubfolder-label": "Subfolder for watched files", + "syncplay-watchedsubfolderautocreate-label": "Automatically create watched subfolder when needed", + "mark-as-watched-menu-label": "Mark as watched", + "mark-as-unwatched-menu-label": "Mark as unwatched", "sync-label": "Sync", "sync-otherslagging-title": "If others are lagging behind...", "sync-youlaggging-title": "If you are lagging behind...", @@ -411,7 +422,7 @@ "executable-path-tooltip": "Location of your chosen supported media player (mpv, mpv.net, VLC, MPC-HC/BE, mplayer2 or IINA).", "media-path-tooltip": "Location of video or stream to be opened. Necessary for mplayer2.", "player-arguments-tooltip": "Additional command line arguments / switches to pass on to this media player.", - "mediasearcdirectories-arguments-tooltip": "Directories where Syncplay will search for media files, e.g. when you are using the click to switch feature. Syncplay will look recursively through sub-folders.", + "mediasearcdirectories-arguments-tooltip": "Directories where Syncplay will search for media files, e.g. when you are using the click to switch feature. Syncplay will look recursively through subfolders.", "more-tooltip": "Display less frequently used settings.", "filename-privacy-tooltip": "Privacy mode for sending currently playing filename to server.", @@ -472,6 +483,10 @@ "switch-to-file-tooltip": "Double click to switch to {}", # Filename "sendmessage-tooltip": "Send message to room", + "watchedautomove-tooltip": "Automatically move file into watched subfolder when end of file is reached. This only works if the parent directory is a media directory.", + "watchedsubfolder-tooltip": "Subfolder for watched files to be moved into relative to file directory (subfolder needs to exist for file to be moved). This only works if the parent directory is a media directory.", + "watchedsubfolderautocreate-tooltip": "Automatically create watched subfolder when moving file to subfolder if it does not already exist. This only works if the parent directory is a media directory.", + # In-userlist notes (GUI) "differentsize-note": "Different size!", "differentsizeandduration-note": "Different size and duration!", diff --git a/syncplay/messages_eo.py b/syncplay/messages_eo.py index 280d7b53..96ff78aa 100644 --- a/syncplay/messages_eo.py +++ b/syncplay/messages_eo.py @@ -558,4 +558,20 @@ "playlist-empty-error": "Ludlisto nun estas malplena.", "playlist-invalid-index-error": "Nevalida indico de ludlisto", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Dosierujoj", + "syncplay-watchedfiles-title": "Spektitaj dosieroj", + "syncplay-watchedautomove-label": "Aŭtomate movi spektitajn dosierojn al subdosierujo (se la subdosierujo ekzistas)", + "syncplay-watchedmovesubfolder-label": "Subdosierujo por spektitaj dosieroj", + "syncplay-watchedsubfolderautocreate-label": "Aŭtomate krei subdosierujon por spektitaj dosieroj laŭbezone", + "mark-as-watched-menu-label": "Marki kiel spektita", + "mark-as-unwatched-menu-label": "Marki kiel nespektita", + "watchedautomove-tooltip": "Aŭtomate movas la dosieron al la subdosierujo por spektitaj dosieroj kiam la fino de la dosiero estas atingita. Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "watchedsubfolder-tooltip": "Subdosierujo por movi spektitajn dosierojn, rilate al la dosierujo de la dosiero (la subdosierujo devas ekzisti por ke la dosiero estu movita). Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "watchedsubfolderautocreate-tooltip": "Aŭtomate kreas la subdosierujon por spektitaj dosieroj dum la movo, se ĝi ankoraŭ ne ekzistas. Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "cannot-move-file-due-to-name-conflict-error": "Ne eblis movi '{}' al la subdosierujo '{}' ĉar dosiero kun tiu nomo jam ekzistas.", # Path, subfolder + "moved-file-to-subfolder-notification": "Movis '{}' al la subdosierujo '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index ae3108dd..ad3b8d34 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -552,4 +552,19 @@ "playlist-empty-error": "Playlist is currently empty.", # TO DO: Translate "playlist-invalid-index-error": "Invalid playlist index", # TO DO: Translate + + # Watchlist terms + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Dosierujoj", + "syncplay-watchedfiles-title": "Spektitaj dosieroj", + "syncplay-watchedautomove-label": "Aŭtomate movi spektitajn dosierojn al subdosierujo (se la subdosierujo ekzistas)", + "syncplay-watchedmovesubfolder-label": "Subdosierujo por spektitaj dosieroj", + "syncplay-watchedsubfolderautocreate-label": "Aŭtomate krei subdosierujon por spektitaj dosieroj laŭbezone", + "mark-as-watched-menu-label": "Marki kiel spektita", + "mark-as-unwatched-menu-label": "Marki kiel nespektita", + "watchedautomove-tooltip": "Aŭtomate movas la dosieron al la subdosierujo por spektitaj dosieroj kiam la fino de la dosiero estas atingita. Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "watchedsubfolder-tooltip": "Subdosierujo por movi spektitajn dosierojn, rilate al la dosierujo de la dosiero (la subdosierujo devas ekzisti por ke la dosiero estu movita). Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "watchedsubfolderautocreate-tooltip": "Aŭtomate kreas la subdosierujon por spektitaj dosieroj dum la movo, se ĝi ankoraŭ ne ekzistas. Ĉi tio funkcias nur se la patra dosierujo estas aŭdvidaĵa dosierujo.", + "Could not move '{}' to '{}' subfolder because a file with that name already exists": "Ne eblis movi '{}' al la subdosierujo '{}' ĉar dosiero kun tiu nomo jam ekzistas", + "moved-file-to-subfolder-notification": "Movis '{}' al la subdosierujo '{}'." } diff --git a/syncplay/messages_fi.py b/syncplay/messages_fi.py index 2696c563..aad01e3b 100644 --- a/syncplay/messages_fi.py +++ b/syncplay/messages_fi.py @@ -551,4 +551,21 @@ "playlist-empty-error": "Toistoluettelo on tällä hetkellä tyhjä.", "playlist-invalid-index-error": "Epäkelpo toistoluettelohakemisto", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Kansiot", + "syncplay-watchedfiles-title": "Katsotut tiedostot", + "syncplay-watchedautomove-label": "Siirrä katsotut tiedostot automaattisesti alikansioon (jos alikansio on olemassa)", + "syncplay-watchedmovesubfolder-label": "Alikansio katsotuille tiedostoille", + "syncplay-watchedsubfolderautocreate-label": "Luo katsottujen tiedostojen alikansio automaattisesti tarvittaessa", + "mark-as-watched-menu-label": "Merkitse katsotuksi", + "mark-as-unwatched-menu-label": "Merkitse katsomattomaksi", + "watchedautomove-tooltip": "Siirtää tiedoston automaattisesti katsottujen tiedostojen alikansioon, kun tiedoston loppu saavutetaan. Toimii vain, jos yläkansio on mediakansio.", + "watchedsubfolder-tooltip": "Alikansio, johon katsotut tiedostot siirretään suhteessa tiedoston kansioon (alikansion on oltava olemassa, jotta tiedosto voidaan siirtää). Toimii vain, jos yläkansio on mediakansio.", + "watchedsubfolderautocreate-tooltip": "Luo katsottujen tiedostojen alikansion automaattisesti siirron yhteydessä, jos sitä ei vielä ole. Toimii vain, jos yläkansio on mediakansio.", + "cannot-move-file-due-to-name-conflict-error": "Tiedostoa '{}' ei voitu siirtää alikansioon '{}', koska samanniminen tiedosto on jo olemassa.", # Path, subfolder + "moved-file-to-subfolder-notification": "Siirrettiin '{}' alikansioon '{}'.", # Path, subfolder + + } \ No newline at end of file diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py index 8cd133a8..14bdc450 100644 --- a/syncplay/messages_fr.py +++ b/syncplay/messages_fr.py @@ -555,4 +555,20 @@ "playlist-empty-error": "La liste de lecture est actuellement vide.", "playlist-invalid-index-error": "Index de liste de lecture non valide", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Dossiers", + "syncplay-watchedfiles-title": "Fichiers vus", + "syncplay-watchedautomove-label": "Déplacer automatiquement les fichiers vus vers un sous-dossier (si le sous-dossier existe)", + "syncplay-watchedmovesubfolder-label": "Sous-dossier pour les fichiers vus", + "syncplay-watchedsubfolderautocreate-label": "Créer automatiquement le sous-dossier des fichiers vus si nécessaire", + "mark-as-watched-menu-label": "Marquer comme vu", + "mark-as-unwatched-menu-label": "Marquer comme non vu", + "watchedautomove-tooltip": "Déplace automatiquement le fichier vers le sous-dossier des fichiers vus lorsque la fin du fichier est atteinte. Fonctionne uniquement si le dossier parent est un dossier multimédia.", + "watchedsubfolder-tooltip": "Sous-dossier (par rapport au dossier du fichier) vers lequel déplacer les fichiers vus (le sous-dossier doit exister pour que le fichier soit déplacé). Fonctionne uniquement si le dossier parent est un dossier multimédia.", + "watchedsubfolderautocreate-tooltip": "Crée automatiquement le sous-dossier des fichiers vus lors du déplacement s’il n’existe pas encore. Fonctionne uniquement si le dossier parent est un dossier multimédia.", + "cannot-move-file-due-to-name-conflict-error": "Impossible de déplacer '{}' vers le sous-dossier '{}' car un fichier portant ce nom existe déjà.", # Path, subfolder + "moved-file-to-subfolder-notification": "Déplacé '{}' vers le sous-dossier '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index c941472a..abe26463 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -554,4 +554,20 @@ "playlist-empty-error": "Playlist is currently empty.", # TO DO: Translate "playlist-invalid-index-error": "Invalid playlist index", # TO DO: Translate + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Cartelle", + "syncplay-watchedfiles-title": "File guardati", + "syncplay-watchedautomove-label": "Sposta automaticamente i file guardati nella sottocartella (se esiste)", + "syncplay-watchedmovesubfolder-label": "Sottocartella per i file guardati", + "syncplay-watchedsubfolderautocreate-label": "Crea automaticamente la sottocartella dei file guardati quando necessario", + "mark-as-watched-menu-label": "Segna come guardato", + "mark-as-unwatched-menu-label": "Segna come non guardato", + "watchedautomove-tooltip": "Sposta automaticamente il file nella sottocartella dei file guardati quando si raggiunge la fine del file. Funziona solo se la cartella principale è una cartella multimediale.", + "watchedsubfolder-tooltip": "Sottocartella (relativa alla cartella del file) in cui spostare i file guardati (la sottocartella deve esistere per poter spostare il file). Funziona solo se la cartella principale è una cartella multimediale.", + "watchedsubfolderautocreate-tooltip": "Crea automaticamente la sottocartella dei file guardati durante lo spostamento se non esiste ancora. Funziona solo se la cartella principale è una cartella multimediale.", + "cannot-move-file-due-to-name-conflict-error": "Impossibile spostare '{}' nella sottocartella '{}' perché esiste già un file con lo stesso nome.", # Path, subfolder + "moved-file-to-subfolder-notification": "Spostato '{}' nella sottocartella '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_ko.py b/syncplay/messages_ko.py index 6e563ba3..5aa22db4 100644 --- a/syncplay/messages_ko.py +++ b/syncplay/messages_ko.py @@ -550,4 +550,20 @@ "playlist-empty-error": "현재 재생목록이 비어 있습니다.", "playlist-invalid-index-error": "잘못된 재생목록 색인", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "폴더", + "syncplay-watchedfiles-title": "시청한 파일", + "syncplay-watchedautomove-label": "시청한 파일을 하위 폴더로 자동 이동(하위 폴더가 있으면)", + "syncplay-watchedmovesubfolder-label": "시청한 파일용 하위 폴더", + "syncplay-watchedsubfolderautocreate-label": "필요 시 시청한 파일 하위 폴더 자동 생성", + "mark-as-watched-menu-label": "시청함으로 표시", + "mark-as-unwatched-menu-label": "미시청으로 표시", + "watchedautomove-tooltip": "파일 끝에 도달하면 파일을 시청한 파일 하위 폴더로 자동 이동합니다. 이는 상위 폴더가 미디어 폴더인 경우에만 작동합니다.", + "watchedsubfolder-tooltip": "파일의 폴더 기준 상대 경로의 하위 폴더로 시청한 파일을 이동합니다(파일을 이동하려면 해당 하위 폴더가 존재해야 합니다). 이는 상위 폴더가 미디어 폴더인 경우에만 작동합니다.", + "watchedsubfolderautocreate-tooltip": "파일을 하위 폴더로 이동할 때 해당 하위 폴더가 없으면 시청한 파일 하위 폴더를 자동으로 생성합니다. 이는 상위 폴더가 미디어 폴더인 경우에만 작동합니다.", + "cannot-move-file-due-to-name-conflict-error": "같은 이름의 파일이 이미 존재하므로 '{}'을(를) '{}' 하위 폴더로 이동할 수 없습니다.", # Path, subfolder + "moved-file-to-subfolder-notification": "'{}'을(를) '{}' 하위 폴더로 이동했습니다.", # Path, subfolder + } diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 682720e6..da74c68b 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -554,4 +554,20 @@ "playlist-empty-error": "A playlist está atualemnte vazia.", "playlist-invalid-index-error": "Índice inválido na playlist.", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Pastas", + "syncplay-watchedfiles-title": "Arquivos assistidos", + "syncplay-watchedautomove-label": "Mover automaticamente os arquivos assistidos para a subpasta (se a subpasta existir)", + "syncplay-watchedmovesubfolder-label": "Subpasta para arquivos assistidos", + "syncplay-watchedsubfolderautocreate-label": "Criar automaticamente a subpasta de assistidos quando necessário", + "mark-as-watched-menu-label": "Marcar como assistido", + "mark-as-unwatched-menu-label": "Marcar como não assistido", + "watchedautomove-tooltip": "Move automaticamente o arquivo para a subpasta de assistidos quando o final do arquivo é alcançado. Funciona apenas se a pasta pai for uma pasta de mídia.", + "watchedsubfolder-tooltip": "Subpasta (relativa à pasta do arquivo) para onde mover os arquivos assistidos (a subpasta precisa existir para que o arquivo seja movido). Funciona apenas se a pasta pai for uma pasta de mídia.", + "watchedsubfolderautocreate-tooltip": "Cria automaticamente a subpasta de assistidos ao mover o arquivo, se ela ainda não existir. Funciona apenas se a pasta pai for uma pasta de mídia.", + "cannot-move-file-due-to-name-conflict-error": "Não foi possível mover '{}' para a subpasta '{}' porque já existe um arquivo com esse nome.", # Path, subfolder + "moved-file-to-subfolder-notification": "Movido '{}' para a subpasta '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index 3b8bf552..cfb6fcb9 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -554,4 +554,20 @@ "playlist-empty-error": "Playlist is currently empty.", # TO DO: Translate "playlist-invalid-index-error": "Invalid playlist index", # TO DO: Translate + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Pastas", + "syncplay-watchedfiles-title": "Ficheiros vistos", + "syncplay-watchedautomove-label": "Mover automaticamente os ficheiros vistos para a subpasta (se a subpasta existir)", + "syncplay-watchedmovesubfolder-label": "Subpasta para ficheiros vistos", + "syncplay-watchedsubfolderautocreate-label": "Criar automaticamente a subpasta de ficheiros vistos quando necessário", + "mark-as-watched-menu-label": "Marcar como visto", + "mark-as-unwatched-menu-label": "Marcar como não visto", + "watchedautomove-tooltip": "Move automaticamente o ficheiro para a subpasta de ficheiros vistos quando o fim do ficheiro é alcançado. Funciona apenas se a pasta principal for uma pasta multimédia.", + "watchedsubfolder-tooltip": "Subpasta (relativa à pasta do ficheiro) para onde mover os ficheiros vistos (a subpasta tem de existir para que o ficheiro seja movido). Funciona apenas se a pasta principal for uma pasta multimédia.", + "watchedsubfolderautocreate-tooltip": "Cria automaticamente a subpasta de ficheiros vistos ao mover o ficheiro, se ainda não existir. Funciona apenas se a pasta principal for uma pasta multimédia.", + "cannot-move-file-due-to-name-conflict-error": "Não foi possível mover '{}' para a subpasta '{}' porque já existe um ficheiro com esse nome.", # Path, subfolder + "moved-file-to-subfolder-notification": "Movido '{}' para a subpasta '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 0562482a..925b8809 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -551,4 +551,20 @@ "playlist-empty-error": "Список воспроизведения пуст.", "playlist-invalid-index-error": "Неверный индекс в списке воспроизведения", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Папки", + "syncplay-watchedfiles-title": "Просмотренные файлы", + "syncplay-watchedautomove-label": "Автоматически перемещать просмотренные файлы в подпапку (если подпапка существует)", + "syncplay-watchedmovesubfolder-label": "Подпапка для просмотренных файлов", + "syncplay-watchedsubfolderautocreate-label": "Автоматически создавать подпапку для просмотренных файлов при необходимости", + "mark-as-watched-menu-label": "Отметить как просмотрено", + "mark-as-unwatched-menu-label": "Отметить как не просмотрено", + "watchedautomove-tooltip": "Автоматически перемещает файл в подпапку для просмотренных файлов при достижении конца файла. Работает только, если родительская папка является медиа-папкой.", + "watchedsubfolder-tooltip": "Подпапка (относительно папки файла), в которую перемещаются просмотренные файлы (подпапка должна существовать, чтобы файл был перемещён). Работает только, если родительская папка является медиа-папкой.", + "watchedsubfolderautocreate-tooltip": "Автоматически создаёт подпапку для просмотренных файлов при перемещении, если она ещё не существует. Работает только, если родительская папка является медиа-папкой.", + "cannot-move-file-due-to-name-conflict-error": "Не удалось переместить '{}' в подпапку '{}', потому что файл с таким именем уже существует.", # Path, subfolder + "moved-file-to-subfolder-notification": "Перемещено '{}' в подпапку '{}'.", # Path, subfolder + } diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index 021fb9b0..fe075067 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -555,4 +555,20 @@ "playlist-empty-error": "Oynatma listesi şu anda boş.", "playlist-invalid-index-error": "Geçersiz oynatma listesi dizini", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "Klasörler", + "syncplay-watchedfiles-title": "İzlenen dosyalar", + "syncplay-watchedautomove-label": "İzlenen dosyaları otomatik olarak alt klasöre taşı (alt klasör varsa)", + "syncplay-watchedmovesubfolder-label": "İzlenen dosyalar için alt klasör", + "syncplay-watchedsubfolderautocreate-label": "Gerektiğinde izlenenler alt klasörünü otomatik oluştur", + "mark-as-watched-menu-label": "İzlenmiş olarak işaretle", + "mark-as-unwatched-menu-label": "İzlenmemiş olarak işaretle", + "watchedautomove-tooltip": "Dosyanın sonuna ulaşıldığında dosyayı otomatik olarak izlenenler alt klasörüne taşır. Bu yalnızca üst klasör bir medya klasörü ise çalışır.", + "watchedsubfolder-tooltip": "İzlenen dosyaların, dosyanın klasörüne göre göreli olarak taşınacağı alt klasör (dosyanın taşınabilmesi için alt klasörün var olması gerekir). Bu yalnızca üst klasör bir medya klasörü ise çalışır.", + "watchedsubfolderautocreate-tooltip": "Dosya alt klasöre taşınırken, yoksa izlenenler alt klasörünü otomatik olarak oluşturur. Bu yalnızca üst klasör bir medya klasörü ise çalışır.", + "cannot-move-file-due-to-name-conflict-error": "'{}' '{}' alt klasörüne taşınamadı çünkü bu ada sahip bir dosya zaten var.", # Path, subfolder + "moved-file-to-subfolder-notification": "'{}' '{}' alt klasörüne taşındı.", # Path, subfolder + } diff --git a/syncplay/messages_zh_CN.py b/syncplay/messages_zh_CN.py index 07510f55..dd7edd8f 100644 --- a/syncplay/messages_zh_CN.py +++ b/syncplay/messages_zh_CN.py @@ -555,4 +555,20 @@ "playlist-empty-error": "播放列表目前是空的。", "playlist-invalid-index-error": "无效的播放列表索引", + + # Watched file functionality + # TODO: Please double-check these translations and remove this message if they are fine + "folders-label": "文件夹", + "syncplay-watchedfiles-title": "已观看的文件", + "syncplay-watchedautomove-label": "自动将已观看的文件移动到子文件夹(若子文件夹存在)", + "syncplay-watchedmovesubfolder-label": "用于已观看文件的子文件夹", + "syncplay-watchedsubfolderautocreate-label": "需要时自动创建已观看文件的子文件夹", + "mark-as-watched-menu-label": "标记为已观看", + "mark-as-unwatched-menu-label": "标记为未观看", + "watchedautomove-tooltip": "当到达文件末尾时,自动将文件移动到已观看子文件夹。仅当父文件夹是媒体文件夹时才有效。", + "watchedsubfolder-tooltip": "用于将已观看文件移动到(相对于文件所在文件夹的)子文件夹(必须存在该子文件夹才能移动文件)。仅当父文件夹是媒体文件夹时才有效。", + "watchedsubfolderautocreate-tooltip": "在将文件移动到子文件夹时,如尚不存在,则自动创建已观看子文件夹。仅当父文件夹是媒体文件夹时才有效。", + "cannot-move-file-due-to-name-conflict-error": "无法将 '{}' 移动到 '{}' 子文件夹,因为已存在同名文件。", # Path, subfolder + "moved-file-to-subfolder-notification": "已将 '{}' 移动到 '{}' 子文件夹。", # Path, subfolder + } diff --git a/syncplay/resources/no_eye.png b/syncplay/resources/no_eye.png new file mode 100644 index 00000000..7a44af82 Binary files /dev/null and b/syncplay/resources/no_eye.png differ diff --git a/syncplay/resources/yes_eye.png b/syncplay/resources/yes_eye.png new file mode 100644 index 00000000..5e92a5bc Binary files /dev/null and b/syncplay/resources/yes_eye.png differ diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 0df41867..f1c41c92 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -67,6 +67,9 @@ def __init__(self): "checkForUpdatesAutomatically": None, "lastCheckedForUpdates": "", "resetConfig": False, + "watchedSubfolder": "Watched", + "watchedSubfolderAutocreate": False, + "watchedAutoMove": False, "showOSD": True, "showOSDWarnings": True, "showSlowdownOSD": True, @@ -138,6 +141,8 @@ def __init__(self): "showSameRoomOSD", "showNonControllerOSD", "showDurationNotification", + "watchedSubfolderAutocreate", + "watchedAutoMove", "sharedPlaylistEnabled", "loopAtEndOfPlaylist", "loopSingleFiles", @@ -203,6 +208,7 @@ def __init__(self): "filesizePrivacyMode", "unpauseAction", "pauseOnLeave", "readyAtStart", "autoplayMinUsers", "autoplayInitialState", "mediaSearchDirectories", + "watchedSubfolder", "watchedSubfolderAutocreate", "watchedAutoMove", "sharedPlaylistEnabled", "loopAtEndOfPlaylist", "loopSingleFiles", "autoplayRequireSameFilenames", diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 43ef0007..df4d6fb1 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -793,6 +793,60 @@ def addBasicTab(self): self.basicOptionsFrame.setLayout(self.basicOptionsLayout) self.stackedLayout.addWidget(self.basicOptionsFrame) + def addFolderTab(self): + self.folderFrame = QtWidgets.QFrame() + self.folderLayout = QtWidgets.QVBoxLayout() + self.folderLayout.setAlignment(Qt.AlignTop) + self.folderFrame.setLayout(self.folderLayout) + + ## Media path directories + + self.mediasearchSettingsGroup = QtWidgets.QGroupBox(getMessage("syncplay-mediasearchdirectories-title")) + self.mediasearchSettingsLayout = QtWidgets.QVBoxLayout() + self.mediasearchSettingsGroup.setLayout(self.mediasearchSettingsLayout) + + self.mediasearchTextEdit = QPlainTextEdit(utils.getListAsMultilineString(self.mediaSearchDirectories)) + self.mediasearchTextEdit.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "mediasearcdirectories-arguments") + self.mediasearchTextEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) + self.mediasearchSettingsLayout.addWidget(self.mediasearchTextEdit) + self.mediasearchSettingsGroup.setMaximumHeight(self.mediasearchSettingsGroup.minimumSizeHint().height()) + + # Move folder + + self.watchedVideosSettingsGroup = QtWidgets.QGroupBox(getMessage("syncplay-watchedfiles-title")) + self.watchedVideosSettingsLayout = QtWidgets.QVBoxLayout() + self.watchedVideosSettingsGroup.setLayout(self.watchedVideosSettingsLayout) + + self.moveWatchedVideoFolderWidget = QtWidgets.QWidget() + self.moveWatchedVideoFolderLayout = QtWidgets.QHBoxLayout() + self.moveWatchedVideoFolderWidget.setLayout(self.moveWatchedVideoFolderLayout) + self.moveWatchedVideoFolderLayout.setContentsMargins(0, 0, 0, 0) + + self.watchedSubfolderLabel = QtWidgets.QLabel(getMessage("syncplay-watchedmovesubfolder-label")) + + self.watchedSubfolderEdit = QtWidgets.QLineEdit("") + self.watchedSubfolderEdit.setObjectName("watchedSubfolder") + + self.moveWatchedVideoFolderLayout.addWidget(self.watchedSubfolderLabel) + self.moveWatchedVideoFolderLayout.addWidget(self.watchedSubfolderEdit) + + self.watchedVideosSettingsLayout.addWidget(self.moveWatchedVideoFolderWidget) + + self.autoMoveWatchedCheck = QtWidgets.QCheckBox(getMessage("syncplay-watchedautomove-label")) + self.autoMoveWatchedCheck.setObjectName("watchedAutoMove") + self.watchedVideosSettingsLayout.addWidget(self.autoMoveWatchedCheck) + + self.autoCreateSubfolderCheck = QtWidgets.QCheckBox(getMessage("syncplay-watchedsubfolderautocreate-label")) + self.autoCreateSubfolderCheck.setObjectName("watchedSubfolderAutocreate") + self.watchedVideosSettingsLayout.addWidget(self.autoCreateSubfolderCheck) + + self.watchedVideosSettingsGroup.setMaximumHeight(self.watchedVideosSettingsGroup.minimumSizeHint().height()) + + # Bring it all together + self.folderLayout.addWidget(self.mediasearchSettingsGroup) + self.folderLayout.addWidget(self.watchedVideosSettingsGroup) + self.stackedLayout.addWidget(self.folderFrame) + def addReadinessTab(self): self.readyFrame = QtWidgets.QFrame() self.readyLayout = QtWidgets.QVBoxLayout() @@ -912,21 +966,8 @@ def addMiscTab(self): self.autosaveJoinsToListCheckbox.setObjectName("autosaveJoinsToList") self.internalSettingsLayout.addWidget(self.autosaveJoinsToListCheckbox) - ## Media path directories - - self.mediasearchSettingsGroup = QtWidgets.QGroupBox(getMessage("syncplay-mediasearchdirectories-title")) - self.mediasearchSettingsLayout = QtWidgets.QVBoxLayout() - self.mediasearchSettingsGroup.setLayout(self.mediasearchSettingsLayout) - - self.mediasearchTextEdit = QPlainTextEdit(utils.getListAsMultilineString(self.mediaSearchDirectories)) - self.mediasearchTextEdit.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "mediasearcdirectories-arguments") - self.mediasearchTextEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) - self.mediasearchSettingsLayout.addWidget(self.mediasearchTextEdit) - self.mediasearchSettingsGroup.setMaximumHeight(self.mediasearchSettingsGroup.minimumSizeHint().height()) - self.miscLayout.addWidget(self.coreSettingsGroup) self.miscLayout.addWidget(self.internalSettingsGroup) - self.miscLayout.addWidget(self.mediasearchSettingsGroup) self.miscLayout.setAlignment(Qt.AlignTop) self.stackedLayout.addWidget(self.miscFrame) @@ -1286,6 +1327,7 @@ def tabList(self): self.tabListFrame = QtWidgets.QFrame() self.tabListWidget = QtWidgets.QListWidget() self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + "house.png"), getMessage("basics-label"))) + self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + "folder_film.png"), getMessage("folders-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + "control_pause_blue.png"), getMessage("readiness-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + "film_link.png"), getMessage("sync-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + "user_comment.png"), getMessage("chat-label"))) @@ -1437,6 +1479,7 @@ def __init__(self, config, playerpaths, error, defaultConfig): self.storedPassword = self.config['password'] self.addBasicTab() + self.addFolderTab() self.addReadinessTab() self.addSyncTab() self.addChatTab() diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a647feba..1865377e 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -18,6 +18,7 @@ from syncplay.utils import resourcespath from syncplay.utils import isLinux, isWindows, isMacOS from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize, isURL +from syncplay.utils import isWatchedFile, getCorrectedPathForFile, canMarkAsWatched from syncplay.vendor import Qt from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, __binding_version__, __qt_version__, IsPySide, IsPySide2, IsPySide6 from syncplay.vendor.Qt.QtCore import Qt, QSettings, QSize, QPoint, QUrl, QLine, QDateTime @@ -349,15 +350,25 @@ def updatePlaylistIndexIcon(self): if fileIsUntrusted: if isDarkMode: self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DARK_UNTRUSTEDITEM_COLOR))) + self.item(item).setBackground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Base))) else: self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_UNTRUSTEDITEM_COLOR))) + self.item(item).setBackground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Base))) elif fileIsAvailable: - self.item(item).setForeground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Text))) + directory = self.selfWindow._syncplayClient.fileSwitch.getDirectoryOfFilenameInCache(itemFilename) + if (directory and os.path.basename(os.path.normpath(directory)) == constants.WATCHED_SUBFOLDER) or isWindows() and (directory.lower() and os.path.basename(os.path.normpath(directory).lower()) == constants.WATCHED_SUBFOLDER.lower()): + self.item(item).setBackground(QtGui.QBrush(QtGui.QColor("grey"))) + self.item(item).setForeground(QtGui.QBrush(QtGui.QColor("black"))) + else: + self.item(item).setForeground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Text))) + self.item(item).setBackground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Base))) else: if isDarkMode: self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DARK_DIFFERENTITEM_COLOR))) + self.item(item).setBackground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Base))) else: self.item(item).setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + self.item(item).setBackground(QtGui.QBrush(self.selfWindow.palette().color(QtGui.QPalette.Base))) self.selfWindow._syncplayClient.fileSwitch.setFilenameWatchlist(self.selfWindow.newWatchlist) self.forceUpdate() @@ -729,8 +740,25 @@ def shuffleRemainingPlaylist(self): def shuffleEntirePlaylist(self): self._syncplayClient.playlist.shuffleEntirePlaylist() + def _markFileWatchedViaContext(self, filePath: str) -> None: + self._syncplayClient.userInitiatedMarkWatched(filePath) + self.playlist.updatePlaylistIndexIcon() + + def _markFileUnwatchedViaContext(self, filePath: str) -> None: + self._syncplayClient.userInitiatedMarkUnwatched(filePath) + self.playlist.updatePlaylistIndexIcon() + @needsClient def openPlaylistMenu(self, position): + def addSeenUnseenItems(pathFound, menu): + filename = os.path.basename(pathFound) + if len(constants.WATCHED_SUBFOLDER) > 0: + pathFound = getCorrectedPathForFile(pathFound) + if isWatchedFile(pathFound): + menu.addAction(QtGui.QPixmap(resourcespath + "no_eye.png"), getMessage("mark-as-unwatched-menu-label"), lambda p=pathFound: self._markFileUnwatchedViaContext(p)) # TODO: Move to language + elif canMarkAsWatched(pathFound): + menu.addAction(QtGui.QPixmap(resourcespath + "yes_eye.png"), getMessage("mark-as-watched-menu-label"), lambda p=pathFound: self._markFileWatchedViaContext(p)) # TODO: Move to language + indexes = self.playlist.selectedIndexes() if len(indexes) > 0: item = self.playlist.selectedIndexes()[0] @@ -750,6 +778,7 @@ def openPlaylistMenu(self, position): menu.addAction(QtGui.QPixmap(resourcespath + "folder_film.png"), getMessage('open-containing-folder'), lambda: utils.open_system_file_browser(pathFound)) + addSeenUnseenItems(pathFound, menu) if self._syncplayClient.isUntrustedTrustableURI(firstFile): domain = utils.getDomainFromURL(firstFile) if domain: diff --git a/syncplay/utils.py b/syncplay/utils.py index e51c6456..9967ca5b 100755 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -10,6 +10,7 @@ import string import subprocess import sys +import shutil import tempfile import time import traceback @@ -23,7 +24,6 @@ folderSearchEnabled = True - def isWindows(): return sys.platform.startswith(constants.OS_WINDOWS) @@ -102,6 +102,8 @@ def f_retry(*args, **kwargs): return f_retry # true decorator return deco_retry +if isWindows(): + import win32file def parseTime(timeStr): if ":" not in timeStr: @@ -461,7 +463,7 @@ def getDomainFromURL(URL): def open_system_file_browser(path): if isURL(path): return - path = os.path.dirname(path) + path = getCorrectedDirectoryForFile(path) if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": @@ -503,6 +505,90 @@ def getListOfPublicServers(): else: raise IOError(getMessage("failed-to-load-server-list-error")) +def isWatchedFile(filePath): + if len(constants.WATCHED_SUBFOLDER) == 0: + return False + directoryPath = getCorrectedDirectoryForFile(filePath) + return isWatchedSubfolder(directoryPath) and os.path.exists(filePath) + +def canMarkAsWatched(filePath): + if len(constants.WATCHED_SUBFOLDER) == 0: + return False + directory = getCorrectedDirectoryForFile(filePath) + if isWatchedSubfolder(directory): + return False + watchedDirectory = getWatchedSubfolder(directory) + return bool((watchedDirectory and os.path.isdir(watchedDirectory)) or constants.WATCHED_AUTOCREATESUBFOLDERS) + +def getUnwatchedParentfolder(watchedDirectoryPath): + if not watchedDirectoryPath: + return None + if not os.path.isdir(watchedDirectoryPath): + return None + unwatchedDirectory = os.path.abspath(os.path.join(watchedDirectoryPath, os.pardir)) + return(unwatchedDirectory) + +def getCorrectedDirectoryForFile(filePath): + if not filePath: + return + directory = os.path.dirname(filePath) + if os.path.exists(filePath): + return directory + else: + filename = os.path.basename(filePath) + seenDirectory = getWatchedSubfolder(directory) + unwatchedParentfolder = getUnwatchedParentfolder(directory) + seenPath = os.path.join(seenDirectory, filename) if seenDirectory else None + unseenPath = os.path.join(unwatchedParentfolder, filename) if unwatchedParentfolder else None + if seenPath and os.path.exists(seenPath): + directory = os.path.dirname(seenPath) + elif unseenPath and os.path.exists(unseenPath): + directory = os.path.dirname(unseenPath) + return directory +def getCorrectedPathForFile(filePath): + if len(constants.WATCHED_SUBFOLDER) == 0: + return filePath + + if os.path.isfile(filePath): + return filePath + else: + correctedDirectory = getCorrectedDirectoryForFile(filePath) + filename = os.path.basename(filePath) + correctedPath = os.path.join(correctedDirectory, filename) + if os.path.isfile(correctedPath): + return correctedPath + else: + return filePath + +def isWatchedSubfolder(directoryPath): + if not directoryPath: + return False + normDirectory = os.path.normcase(os.path.basename(os.path.normpath(directoryPath))) + if len(constants.WATCHED_SUBFOLDER) == 0: + return False + return normDirectory == os.path.normcase(constants.WATCHED_SUBFOLDER) + +def getWatchedSubfolder(parentDirectoryPath): + if not parentDirectoryPath: + return None + if not os.path.isdir(parentDirectoryPath): + return None + watchedSubfolder = os.path.join(parentDirectoryPath, constants.WATCHED_SUBFOLDER) + return(watchedSubfolder) + +def createWatchedSubdirIfNeeded(subfolderPath): + if not subfolderPath: + return + if not constants.WATCHED_AUTOCREATESUBFOLDERS: + return + if not os.path.isdir(subfolderPath): + os.makedirs(subfolderPath) + +def moveFile(sourcePath, destinatonPath): + if isWindows(): + win32file.MoveFile(sourcePath, destinatonPath) + else: + shutil.move(sourcePath, destinatonPath) class RoomPasswordProvider(object): CONTROLLED_ROOM_REGEX = re.compile(r"^\+(.*):(\w{12})$")