From df13cef760e1107e7ef8c6565e411e2e04a206a3 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 01:12:08 -0700 Subject: [PATCH] Add mark list support --- .env.sample | 166 ++++++++++++++++++++++++----------------------- src/functions.py | 19 ++++++ src/jellyfin.py | 36 +++++++++- src/plex.py | 25 +++++++ 4 files changed, 165 insertions(+), 81 deletions(-) diff --git a/.env.sample b/.env.sample index 4a7bdb0..b90d0e1 100644 --- a/.env.sample +++ b/.env.sample @@ -1,80 +1,86 @@ -# Global Settings - -## Do not mark any shows/movies as played and instead just output to log if they would of been marked. -DRYRUN = "True" - -## Additional logging information -DEBUG = "False" - -## Debugging level, "info" is default, "debug" is more verbose -DEBUG_LEVEL = "info" - -## If set to true then the script will only run once and then exit -RUN_ONLY_ONCE = "False" - -## How often to run the script in seconds -SLEEP_DURATION = "3600" - -## Log file where all output will be written to -LOGFILE = "log.log" - -## Timeout for requests for jellyfin -REQUEST_TIMEOUT = 300 - -## Map usernames between servers in the event that they are different, order does not matter -## Comma separated for multiple options -#USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" } - -## Map libraries between servers in the event that they are different, order does not matter -## Comma separated for multiple options -#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" } - -## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded. -## Comma separated for multiple options -#BLACKLIST_LIBRARY = "" -#WHITELIST_LIBRARY = "" -#BLACKLIST_LIBRARY_TYPE = "" -#WHITELIST_LIBRARY_TYPE = "" -#BLACKLIST_USERS = "" -WHITELIST_USERS = "testuser1,testuser2" - - - -# Plex - -## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers -## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly -## Comma separated list for multiple servers -PLEX_BASEURL = "http://localhost:32400, https://nas:32400" - -## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ -## Comma separated list for multiple servers -PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2" - -## If not using plex token then use username and password of the server admin along with the servername -## Comma separated for multiple options -#PLEX_USERNAME = "PlexUser, PlexUser2" -#PLEX_PASSWORD = "SuperSecret, SuperSecret2" -#PLEX_SERVERNAME = "Plex Server1, Plex Server2" - -## Skip hostname validation for ssl certificates. -## Set to True if running into ssl certificate errors -SSL_BYPASS = "False" - -## control the direction of syncing. e.g. SYNC_FROM_PLEX_TO_JELLYFIN set to true will cause the updates from plex -## to be updated in jellyfin. SYNC_FROM_PLEX_TO_PLEX set to true will sync updates between multiple plex servers -SYNC_FROM_PLEX_TO_JELLYFIN = "True" -SYNC_FROM_JELLYFIN_TO_PLEX = "True" -SYNC_FROM_PLEX_TO_PLEX = "True" -SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True" - - -# Jellyfin - -## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly -## Comma separated list for multiple servers -JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096" - -## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key -## Comma separated list for multiple servers -JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2" +# Global Settings + +## Do not mark any shows/movies as played and instead just output to log if they would of been marked. +DRYRUN = "True" + +## Additional logging information +DEBUG = "False" + +## Debugging level, "info" is default, "debug" is more verbose +DEBUG_LEVEL = "info" + +## If set to true then the script will only run once and then exit +RUN_ONLY_ONCE = "False" + +## How often to run the script in seconds +SLEEP_DURATION = "3600" + +## Log file where all output will be written to +LOGFILE = "log.log" + +## Mark file where all shows/movies that have been marked as played will be written to +MARK_FILE = "mark.log" + +## Timeout for requests for jellyfin +REQUEST_TIMEOUT = 300 + +## Max threads for processing +MAX_THREADS = 32 + +## Map usernames between servers in the event that they are different, order does not matter +## Comma separated for multiple options +#USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" } + +## Map libraries between servers in the event that they are different, order does not matter +## Comma separated for multiple options +#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" } + +## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded. +## Comma separated for multiple options +#BLACKLIST_LIBRARY = "" +#WHITELIST_LIBRARY = "" +#BLACKLIST_LIBRARY_TYPE = "" +#WHITELIST_LIBRARY_TYPE = "" +#BLACKLIST_USERS = "" +WHITELIST_USERS = "testuser1,testuser2" + + + +# Plex + +## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers +## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly +## Comma separated list for multiple servers +PLEX_BASEURL = "http://localhost:32400, https://nas:32400" + +## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ +## Comma separated list for multiple servers +PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2" + +## If not using plex token then use username and password of the server admin along with the servername +## Comma separated for multiple options +#PLEX_USERNAME = "PlexUser, PlexUser2" +#PLEX_PASSWORD = "SuperSecret, SuperSecret2" +#PLEX_SERVERNAME = "Plex Server1, Plex Server2" + +## Skip hostname validation for ssl certificates. +## Set to True if running into ssl certificate errors +SSL_BYPASS = "False" + +## control the direction of syncing. e.g. SYNC_FROM_PLEX_TO_JELLYFIN set to true will cause the updates from plex +## to be updated in jellyfin. SYNC_FROM_PLEX_TO_PLEX set to true will sync updates between multiple plex servers +SYNC_FROM_PLEX_TO_JELLYFIN = "True" +SYNC_FROM_JELLYFIN_TO_PLEX = "True" +SYNC_FROM_PLEX_TO_PLEX = "True" +SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True" + + +# Jellyfin + +## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly +## Comma separated list for multiple servers +JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096" + +## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key +## Comma separated list for multiple servers +JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2" diff --git a/src/functions.py b/src/functions.py index 49dc67e..588d5e1 100644 --- a/src/functions.py +++ b/src/functions.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv load_dotenv(override=True) logfile = os.getenv("LOGFILE", "log.log") +markfile = os.getenv("MARK_FILE", "mark.log") def logger(message: str, log_type=0): @@ -31,6 +32,24 @@ def logger(message: str, log_type=0): file.write(output + "\n") +def log_marked( + username: str, library: str, movie_show: str, episode: str = None, duration=None +): + if markfile is None: + return + + output = f"{username}/{library}/{movie_show}" + + if episode: + output += f"/{episode}" + + if duration: + output += f"/{duration}" + + file = open(f"{markfile}", "a", encoding="utf-8") + file.write(output + "\n") + + # Reimplementation of distutils.util.strtobool due to it being deprecated # Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668 def str_to_bool(value: any) -> bool: diff --git a/src/jellyfin.py b/src/jellyfin.py index 50555f9..c3f26a9 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -2,7 +2,12 @@ import asyncio, aiohttp, traceback, os from math import floor from dotenv import load_dotenv -from src.functions import logger, search_mapping, contains_nested +from src.functions import ( + logger, + search_mapping, + contains_nested, + log_marked, +) from src.library import ( check_skip_logic, generate_library_guids_dict, @@ -642,6 +647,12 @@ class Jellyfin: ) else: logger(f"Dryrun {msg}", 0) + + log_marked( + user_name, + library, + jellyfin_video.get("Name"), + ) else: # TODO add support for partially watched movies msg = f"{jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" @@ -651,6 +662,13 @@ class Jellyfin: else: pass # logger(f"Dryrun {msg}", 0) + + log_marked( + user_name, + library, + jellyfin_video.get("Name"), + duration=floor(movie_status["time"] / 60_000), + ) else: logger( f"Jellyfin: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}", @@ -811,18 +829,34 @@ class Jellyfin: ) else: logger(f"Dryrun {msg}", 0) + + log_marked( + user_name, + library, + jellyfin_episode.get("SeriesName"), + jellyfin_episode.get("Name"), + ) else: # TODO add support for partially watched episodes msg = ( f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}" + f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" ) + """ if not dryrun: pass # logger(f"Marked {msg}", 0) else: pass # logger(f"Dryrun {msg}", 0) + + log_marked( + user_name, + library, + jellyfin_episode.get("SeriesName"), + jellyfin_episode.get('Name'), + duration=floor(episode_status["time"] / 60_000), + )""" else: logger( f"Jellyfin: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}", diff --git a/src/plex.py b/src/plex.py index df80adf..c9cc9ae 100644 --- a/src/plex.py +++ b/src/plex.py @@ -10,6 +10,7 @@ from src.functions import ( search_mapping, future_thread_executor, contains_nested, + log_marked, ) from src.library import ( check_skip_logic, @@ -301,6 +302,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun): movies_search.markWatched() else: logger(f"Dryrun {msg}", 0) + + log_marked(user.title, library, movies_search.title, None, None) elif video_status["time"] > 60_000: msg = f"{movies_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library} for Plex" if not dryrun: @@ -308,6 +311,13 @@ def update_user_watched(user, user_plex, library, videos, dryrun): movies_search.updateProgress(video_status["time"]) else: logger(f"Dryrun {msg}", 0) + + log_marked( + user.title, + library, + movies_search.title, + duration=video_status["time"], + ) else: logger( f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", @@ -332,6 +342,13 @@ def update_user_watched(user, user_plex, library, videos, dryrun): episode_search.markWatched() else: logger(f"Dryrun {msg}", 0) + + log_marked( + user.title, + library, + show_search.title, + episode_search.title, + ) else: msg = f"{show_search.title} {episode_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library} for Plex" if not dryrun: @@ -339,6 +356,14 @@ def update_user_watched(user, user_plex, library, videos, dryrun): episode_search.updateProgress(video_status["time"]) else: logger(f"Dryrun {msg}", 0) + + log_marked( + user.title, + library, + show_search.title, + episode_search.title, + video_status["time"], + ) else: logger( f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}",