From 23f2d287d6d1fb20bee079169f4ab67552eefbed Mon Sep 17 00:00:00 2001 From: neofright <68615872+neofright@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:14:46 +0000 Subject: [PATCH 01/15] Typo in .env.sample --- .env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 2953a0e..b5bc55e 100644 --- a/.env.sample +++ b/.env.sample @@ -28,7 +28,7 @@ MAX_THREADS = 32 ## Comma separated for multiple options #USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" } -## Map libraries between servers in the even that they are different, order does not matter +## 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" } From b378dff0dc180f7f9b031eac2aefae772e511b49 Mon Sep 17 00:00:00 2001 From: neofright <68615872+neofright@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:21:07 +0000 Subject: [PATCH 02/15] Improve README.md - Inprogress is not a word in English, but two separate words. - Many words are unnecessarily captialised as they are not names or at the beginning of the sentences. - Prefer 'usernames' to 'usersnames' --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 899425d..5fbb898 100644 --- a/README.md +++ b/README.md @@ -12,33 +12,33 @@ Keep in sync all your users watched history between jellyfin and plex servers lo ### Plex -* \[x] Match via Filenames +* \[x] Match via filenames * \[x] Match via provider ids -* \[x] Map usersnames +* \[x] Map usernames * \[x] Use single login -* \[x] One Way/Multi Way sync -* \[x] Sync Watched -* \[x] Sync Inprogress +* \[x] One way/multi way sync +* \[x] Sync watched +* \[x] Sync in progress ### Jellyfin -* \[x] Match via Filenames +* \[x] Match via filenames * \[x] Match via provider ids -* \[x] Map usersnames +* \[x] Map usernames * \[x] Use single login -* \[x] One Way/Multi Way sync -* \[x] Sync Watched -* \[ ] Sync Inprogress +* \[x] One way/multi way sync +* \[x] Sync watched +* \[ ] Sync in progress ### Emby -* \[ ] Match via Filenames +* \[ ] Match via filenames * \[ ] Match via provider ids -* \[ ] Map usersnames +* \[ ] Map usernames * \[ ] Use single login -* \[ ] One Way/Multi Way sync -* \[ ] Sync Watched -* \[ ] Sync Inprogress +* \[ ] One way/multi Way sync +* \[ ] Sync watched +* \[ ] Sync in progress ## Configuration From 375c6b23a5bbc7bf21fafce9921d3c0fcdb1b939 Mon Sep 17 00:00:00 2001 From: neofright <68615872+neofright@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:21:41 +0000 Subject: [PATCH 03/15] Update README.md Remove another unnecessary captialisation. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fbb898..e0f3f9a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo * \[ ] Match via provider ids * \[ ] Map usernames * \[ ] Use single login -* \[ ] One way/multi Way sync +* \[ ] One way/multi way sync * \[ ] Sync watched * \[ ] Sync in progress From 032243de0a5dcfc62b31aa7c4c1ee8c1b59fa525 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 00:16:44 -0700 Subject: [PATCH 04/15] Pin to 3.11 due to 3.12 issues --- Dockerfile.alpine | 2 +- Dockerfile.slim | 2 +- requirements.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 5b1a049..9437333 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM python:3-alpine +FROM python:3.11-alpine ENV DRYRUN 'True' ENV DEBUG 'True' diff --git a/Dockerfile.slim b/Dockerfile.slim index f667fe7..7cc3c0d 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -1,4 +1,4 @@ -FROM python:3-slim +FROM python:3.11-slim ENV DRYRUN 'True' ENV DEBUG 'True' diff --git a/requirements.txt b/requirements.txt index 027232b..332dbb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -PlexAPI==4.15.2 +PlexAPI==4.15.5 requests==2.31.0 python-dotenv==1.0.0 -aiohttp==3.8.5 +aiohttp==3.8.6 From 6ccb68aeb3f22c931d6a6bc5e7124226c8528282 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 01:09:11 -0700 Subject: [PATCH 05/15] Add example baseurl/token to docker-compose --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e3783f1..9a061c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,10 +20,10 @@ services: - WHITELIST_LIBRARY_TYPE= - BLACKLIST_USERS= - WHITELIST_USERS= - - PLEX_BASEURL= - - PLEX_TOKEN= - - JELLYFIN_BASEURL= - - JELLYFIN_TOKEN= + - PLEX_BASEURL=https://localhost:32400 + - PLEX_TOKEN=plex_token + - JELLYFIN_BASEURL=http://localhost:8096 + - JELLYFIN_TOKEN=jelly_token - SSL_BYPASS=True - SYNC_FROM_PLEX_TO_JELLYFIN=True - SYNC_FROM_JELLYFIN_TO_PLEX=True From 2c48e89435d873052b9b32a84280030202fc7a5e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 01:12:08 -0700 Subject: [PATCH 06/15] Add mark list support --- .env.sample | 169 ++++++++++++++++++++++++----------------------- src/functions.py | 19 ++++++ src/jellyfin.py | 36 +++++++++- src/plex.py | 25 +++++++ 4 files changed, 165 insertions(+), 84 deletions(-) diff --git a/.env.sample b/.env.sample index b5bc55e..b90d0e1 100644 --- a/.env.sample +++ b/.env.sample @@ -1,83 +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 - -## 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" +# 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}", From 9ff3bdf302989e380d5db0f33d385fe8acc1ca1f Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 01:48:07 -0700 Subject: [PATCH 07/15] Add MARK/DRYRUN logger levels --- src/functions.py | 4 ++++ src/jellyfin.py | 27 ++++++++++++++------------- src/plex.py | 24 ++++++++++++------------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/functions.py b/src/functions.py index 588d5e1..94169b2 100644 --- a/src/functions.py +++ b/src/functions.py @@ -23,6 +23,10 @@ def logger(message: str, log_type=0): output = f"[DEBUG]: {output}" elif log_type == 4: output = f"[WARNING]: {output}" + elif log_type == 5: + output = f"[MARK]: {output}" + elif log_type == 6: + output = f"[DRYRUN]: {output}" else: output = None diff --git a/src/jellyfin.py b/src/jellyfin.py index c3f26a9..7d750f9 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -637,16 +637,16 @@ class Jellyfin: if movie_status: jellyfin_video_id = jellyfin_video["Id"] if movie_status["completed"]: - msg = f"{jellyfin_video.get('Name')} as watched for {user_name} in {library} for Jellyfin" + msg = f"Jellyfin: {jellyfin_video.get('Name')} as watched for {user_name} in {library}" if not dryrun: - logger(f"Marking {msg}", 0) + logger(msg, 5) await self.query( f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post", session, ) else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) log_marked( user_name, @@ -655,20 +655,21 @@ class Jellyfin: ) 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" + msg = f"Jellyfin: {jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library}" + """ if not dryrun: pass - # logger(f"Marked {msg}", 0) + # logger(msg, 5) else: pass - # logger(f"Dryrun {msg}", 0) + # logger(msg, 6) 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}", @@ -817,18 +818,18 @@ class Jellyfin: jellyfin_episode_id = jellyfin_episode["Id"] if episode_status["completed"]: msg = ( - f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}" - + f" as watched for {user_name} in {library} for Jellyfin" + f"Jellyfin: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}" + + f" as watched for {user_name} in {library}" ) if not dryrun: - logger(f"Marked {msg}", 0) + logger(msg, 5) await self.query( f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post", session, ) else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) log_marked( user_name, @@ -839,8 +840,8 @@ class Jellyfin: 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" + f"Jellyfin: {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}" ) """ if not dryrun: diff --git a/src/plex.py b/src/plex.py index c9cc9ae..87b2b3a 100644 --- a/src/plex.py +++ b/src/plex.py @@ -296,21 +296,21 @@ def update_user_watched(user, user_plex, library, videos, dryrun): ) if video_status: if video_status["completed"]: - msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex" + msg = f"Plex: {movies_search.title} as watched for {user.title} in {library}" if not dryrun: - logger(f"Marked {msg}", 0) + logger(msg, 5) movies_search.markWatched() else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) 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" + msg = f"Plex: {movies_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library}" if not dryrun: - logger(f"Marked {msg}", 0) + logger(msg, 5) movies_search.updateProgress(video_status["time"]) else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) log_marked( user.title, @@ -336,12 +336,12 @@ def update_user_watched(user, user_plex, library, videos, dryrun): ) if video_status: if video_status["completed"]: - msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex" + msg = f"Plex: {show_search.title} {episode_search.title} as watched for {user.title} in {library}" if not dryrun: - logger(f"Marked {msg}", 0) + logger(msg, 5) episode_search.markWatched() else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) log_marked( user.title, @@ -350,12 +350,12 @@ def update_user_watched(user, user_plex, library, videos, dryrun): 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" + msg = f"Plex: {show_search.title} {episode_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library}" if not dryrun: - logger(f"Marked {msg}", 0) + logger(msg, 5) episode_search.updateProgress(video_status["time"]) else: - logger(f"Dryrun {msg}", 0) + logger(msg, 6) log_marked( user.title, From 89a2768fc9ce4ab025968cfeff9427529526f44b Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 01:59:18 -0700 Subject: [PATCH 08/15] Jellyfin: Remove headers append --- src/jellyfin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 7d750f9..faaa144 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -100,7 +100,7 @@ class Jellyfin: return await self.query(query, query_type, session, identifiers) results = None - headers = {"Accept": "application/json", "X-Emby-Token": self.token} + authorization = ( "MediaBrowser , " 'Client="other", ' @@ -108,8 +108,8 @@ class Jellyfin: 'DeviceId="script", ' 'Version="0.0.0"' ) - headers["X-Emby-Authorization"] = authorization - + headers = {"Accept": "application/json", "X-Emby-Token": self.token, "X-Emby-Authorization": authorization} + if query_type == "get": async with session.get( self.baseurl + query, headers=headers From 7e9c6bb33866a0158f7ad121c9f3dba58c5e2396 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 02:05:47 -0700 Subject: [PATCH 09/15] Add unraid to type --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 08d0947..853ff22 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,6 +26,7 @@ If applicable, add logs to help explain your problem ideally with DEBUG set to t **Type:** - [ ] Docker Compose - [ ] Docker +- [ ] Unraid - [ ] Native **Additional context** From 6afe123947abf8b3bd90b489d844956c9a270528 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 02:28:40 -0700 Subject: [PATCH 10/15] Docker: Add RUN_ONLY_ONCE and MARKFILE --- Dockerfile.alpine | 2 ++ Dockerfile.slim | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 9437333..22cc341 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -3,8 +3,10 @@ FROM python:3.11-alpine ENV DRYRUN 'True' ENV DEBUG 'True' ENV DEBUG_LEVEL 'INFO' +ENV RUN_ONLY_ONCE 'False' ENV SLEEP_DURATION '3600' ENV LOGFILE 'log.log' +ENV MARKFILE 'mark.log' ENV USER_MAPPING '' ENV LIBRARY_MAPPING '' diff --git a/Dockerfile.slim b/Dockerfile.slim index 7cc3c0d..ee524fe 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -3,8 +3,10 @@ FROM python:3.11-slim ENV DRYRUN 'True' ENV DEBUG 'True' ENV DEBUG_LEVEL 'INFO' +ENV RUN_ONLY_ONCE 'False' ENV SLEEP_DURATION '3600' ENV LOGFILE 'log.log' +ENV MARKFILE 'mark.log' ENV USER_MAPPING '' ENV LIBRARY_MAPPING '' @@ -33,6 +35,7 @@ ENV WHITELIST_USERS '' WORKDIR /app COPY ./requirements.txt ./ + RUN pip install --no-cache-dir -r requirements.txt COPY . . From a3fc53059c20b3e4822d74cc4ab09c39d03c08d0 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 02:30:11 -0700 Subject: [PATCH 11/15] MARKFILE match LOGFILE --- src/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index 94169b2..b7ae693 100644 --- a/src/functions.py +++ b/src/functions.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv load_dotenv(override=True) logfile = os.getenv("LOGFILE", "log.log") -markfile = os.getenv("MARK_FILE", "mark.log") +markfile = os.getenv("MARKFILE", "mark.log") def logger(message: str, log_type=0): From f6b2186824b260d655957e8095c5b46214691aa1 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 02:49:14 -0700 Subject: [PATCH 12/15] Docker-compose: Add markfile. Add user mapping ex --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9a061c8..c3f0703 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,8 @@ services: - RUN_ONLY_ONCE=False - SLEEP_DURATION=3600 - LOGFILE=/tmp/log.log - - USER_MAPPING= + - MARKFILE=/tmp/mark.log + - USER_MAPPING={"user1":"user2"} - LIBRARY_MAPPING={"TV Shows":"Shows"} - BLACKLIST_LIBRARY= - WHITELIST_LIBRARY= From d607c9c821b07cd38535aebc90cf49269e3c3e14 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 03:36:10 -0700 Subject: [PATCH 13/15] Use non root for containers --- Dockerfile.alpine | 12 ++++++++++-- Dockerfile.slim | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 22cc341..03f432a 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -32,12 +32,20 @@ ENV WHITELIST_LIBRARY_TYPE '' ENV BLACKLIST_USERS '' ENV WHITELIST_USERS '' + +RUN addgroup --system jellyplex_user && \ + adduser --system --no-create-home jellyplex_user --ingroup jellyplex_user && \ + mkdir -p /app && \ + chown -R jellyplex_user:jellyplex_user /app + WORKDIR /app -COPY ./requirements.txt ./ +COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY . . +COPY --chown=jellyplex_user:jellyplex_user . . + +USER jellyplex_user CMD ["python", "-u", "main.py"] diff --git a/Dockerfile.slim b/Dockerfile.slim index ee524fe..59c9a02 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -32,12 +32,20 @@ ENV WHITELIST_LIBRARY_TYPE '' ENV BLACKLIST_USERS '' ENV WHITELIST_USERS '' + +RUN addgroup --system jellyplex_user && \ + adduser --system --no-create-home jellyplex_user --ingroup jellyplex_user && \ + mkdir -p /app && \ + chown -R jellyplex_user:jellyplex_user /app + WORKDIR /app -COPY ./requirements.txt ./ +COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY . . +COPY --chown=jellyplex_user:jellyplex_user . . + +USER jellyplex_user CMD ["python", "-u", "main.py"] From e1ef6615cc147ce6f7ebd4710c548ba0ee5dbebf Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 03:39:29 -0700 Subject: [PATCH 14/15] README: Change configuration to point to .env.sample --- README.md | 84 +------------------------------------------------------ 1 file changed, 1 insertion(+), 83 deletions(-) diff --git a/README.md b/README.md index e0f3f9a..8b6cfdd 100644 --- a/README.md +++ b/README.md @@ -42,89 +42,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo ## Configuration -```bash -# 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 even 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" -``` +Full list of configuration options can be found in the [.env.sample](.env.sample) ## Installation From 2a65c4b5cae0ee1f7dd0b702725b754d15e70d2c Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Nov 2023 03:48:05 -0700 Subject: [PATCH 15/15] Action: Add default variant --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dcee0f..a5c5bdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: docker: runs-on: ubuntu-latest needs: pytest + env: + DEFAULT_VARIANT: alpine strategy: fail-fast: false matrix: @@ -45,14 +47,19 @@ jobs: # Do not push to ghcr.io on PRs due to permission issues ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }} tags: | - type=raw,value=latest,enable=${{ matrix.variant == 'alpine' && github.ref_name == github.event.repository.default_branch }} - type=raw,value=dev,enable=${{ matrix.variant == 'alpine' && github.ref_name == 'dev' }} + type=raw,value=latest,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == github.event.repository.default_branch }} + type=raw,value=dev,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == 'dev' }} type=raw,value=latest,suffix=-${{ matrix.variant }},enable={{ is_default_branch }} type=ref,event=branch,suffix=-${{ matrix.variant }} + type=ref,event=branch,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=ref,event=pr,suffix=-${{ matrix.variant }} + type=ref,event=pr,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=semver,pattern={{ version }},suffix=-${{ matrix.variant }} + type=semver,pattern={{ version }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=semver,pattern={{ major }}.{{ minor }},suffix=-${{ matrix.variant }} + type=semver,pattern={{ major }}.{{ minor }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=sha,suffix=-${{ matrix.variant }} + type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} - name: Set up QEMU uses: docker/setup-qemu-action@v2