From 485ec5fe2d4274703f43174b9024d3eb5c2d54ab Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sat, 29 Apr 2023 20:31:24 -0600 Subject: [PATCH 01/45] Add docker-compose file --- docker-compose.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e3783f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' + +services: + jellyplex-watched: + image: luigi311/jellyplex-watched:latest + container_name: jellyplex-watched + restart: always + environment: + - DRYRUN=True + - DEBUG=True + - DEBUG_LEVEL=info + - RUN_ONLY_ONCE=False + - SLEEP_DURATION=3600 + - LOGFILE=/tmp/log.log + - USER_MAPPING= + - LIBRARY_MAPPING={"TV Shows":"Shows"} + - BLACKLIST_LIBRARY= + - WHITELIST_LIBRARY= + - BLACKLIST_LIBRARY_TYPE= + - WHITELIST_LIBRARY_TYPE= + - BLACKLIST_USERS= + - WHITELIST_USERS= + - PLEX_BASEURL= + - PLEX_TOKEN= + - JELLYFIN_BASEURL= + - JELLYFIN_TOKEN= + - SSL_BYPASS=True + - SYNC_FROM_PLEX_TO_JELLYFIN=True + - SYNC_FROM_JELLYFIN_TO_PLEX=True + - SYNC_FROM_PLEX_TO_PLEX=True + - SYNC_FROM_JELLYFIN_TO_JELLYFIN=True From 03d1fd8019512b430a0b09b9e696f968a9f0a336 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 15 May 2023 10:44:30 -0600 Subject: [PATCH 02/45] Log both servers users instead of exiting immediately --- src/main.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main.py b/src/main.py index f308310..715e5d8 100644 --- a/src/main.py +++ b/src/main.py @@ -40,15 +40,23 @@ def setup_users( # Check if users is none or empty if output_server_1_users is None or len(output_server_1_users) == 0: - raise Exception( - f"No users found for server 1 {server_1[0]}, users found {users}, filtered users {users_filtered}, server 1 users {server_1[1].users}" + logger( + f"No users found for server 1 {server_1[0]}, users: {server_1_users}, overlapping users {users}, filtered users {users_filtered}, server 1 users {server_1[1].users}" ) if output_server_2_users is None or len(output_server_2_users) == 0: - raise Exception( - f"No users found for server 2 {server_2[0]}, users found {users} filtered users {users_filtered}, server 2 users {server_2[1].users}" + logger( + f"No users found for server 2 {server_2[0]}, users: {server_2_users}, overlapping users {users} filtered users {users_filtered}, server 2 users {server_2[1].users}" ) + if ( + output_server_1_users is None + or len(output_server_1_users) == 0 + or output_server_2_users is None + or len(output_server_2_users) == 0 + ): + raise Exception("No users found for one or both servers") + logger(f"Server 1 users: {output_server_1_users}", 1) logger(f"Server 2 users: {output_server_2_users}", 1) From 7de7b42fd2638d6491cad434948d52e61a74189c Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 15 May 2023 11:10:03 -0600 Subject: [PATCH 03/45] Users: Default to username and fall back to title --- src/users.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/users.py b/src/users.py index 6075be8..fc8962b 100644 --- a/src/users.py +++ b/src/users.py @@ -11,7 +11,12 @@ def generate_user_list(server): server_users = [] if server_type == "plex": - server_users = [x.title.lower() for x in server_connection.users] + for user in server_connection.users: + if user.username: + server_users.append(user.username.lower()) + else: + server_users.append(user.title.lower()) + elif server_type == "jellyfin": server_users = [key.lower() for key in server_connection.users.keys()] From aa177666a5b92e0ee0992d8572e6d944183f46cb Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 15 May 2023 11:17:28 -0600 Subject: [PATCH 04/45] Plex: Fix username/title selection --- src/plex.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plex.py b/src/plex.py index 889078d..0402016 100644 --- a/src/plex.py +++ b/src/plex.py @@ -126,7 +126,9 @@ def get_user_library_watched_show(show): def get_user_library_watched(user, user_plex, library): try: - user_name = user.title.lower() + user_name = user.username.lower() + if user_name == "": + user_name = user.title.lower() user_watched = {} user_watched[user_name] = {} @@ -509,10 +511,14 @@ class Plex: user_other = search_mapping(user_mapping, user) for index, value in enumerate(self.users): - if user.lower() == value.title.lower(): + username_title = value.username.lower() + if username_title == "": + username_title = value.title.lower() + + if user.lower() == username_title: user = self.users[index] break - elif user_other and user_other.lower() == value.title.lower(): + elif user_other and user_other.lower() == username_title: user = self.users[index] break From 7eba46b5cbf7abfb1d210f5b3aea0a814b3b5590 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 15 May 2023 14:57:46 -0600 Subject: [PATCH 05/45] plex: Fix username/title --- src/users.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/users.py b/src/users.py index fc8962b..e6e2309 100644 --- a/src/users.py +++ b/src/users.py @@ -71,9 +71,13 @@ def generate_server_users(server, users): if server[0] == "plex": server_users = [] for plex_user in server[1].users: + username_title = ( + plex_user.username if plex_user.username != "" else plex_user.title + ) + if ( - plex_user.title.lower() in users.keys() - or plex_user.title.lower() in users.values() + username_title.lower() in users.keys() + or username_title.lower() in users.values() ): server_users.append(plex_user) elif server[0] == "jellyfin": From ffc81dad69d8be791b24f63a9eb2b5d8651930e5 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 15 May 2023 15:12:25 -0600 Subject: [PATCH 06/45] CI: Add back in dev based on alpine --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63fec60..826a647 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,8 @@ 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 == format('refs/heads/{0}', github.event.repository.default_branch) }} + 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,suffix=-${{ matrix.variant }},enable={{ is_default_branch }} type=ref,event=branch,suffix=-${{ matrix.variant }} type=ref,event=pr,suffix=-${{ matrix.variant }} From 9f61c7338da7716999d9036d844e55a07dbbb831 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 17 May 2023 13:22:00 -0600 Subject: [PATCH 07/45] Plex: Cleanup username_title --- src/plex.py | 12 ++++++------ src/users.py | 9 ++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/plex.py b/src/plex.py index 0402016..0bcdf7b 100644 --- a/src/plex.py +++ b/src/plex.py @@ -126,9 +126,7 @@ def get_user_library_watched_show(show): def get_user_library_watched(user, user_plex, library): try: - user_name = user.username.lower() - if user_name == "": - user_name = user.title.lower() + user_name = user.username.lower() if user.username else user.title.lower() user_watched = {} user_watched[user_name] = {} @@ -511,9 +509,11 @@ class Plex: user_other = search_mapping(user_mapping, user) for index, value in enumerate(self.users): - username_title = value.username.lower() - if username_title == "": - username_title = value.title.lower() + username_title = ( + value.username.lower() + if value.username + else value.title.lower() + ) if user.lower() == username_title: user = self.users[index] diff --git a/src/users.py b/src/users.py index e6e2309..4f1e280 100644 --- a/src/users.py +++ b/src/users.py @@ -12,10 +12,9 @@ def generate_user_list(server): server_users = [] if server_type == "plex": for user in server_connection.users: - if user.username: - server_users.append(user.username.lower()) - else: - server_users.append(user.title.lower()) + server_users.append( + user.username.lower() if user.username else user.title.lower() + ) elif server_type == "jellyfin": server_users = [key.lower() for key in server_connection.users.keys()] @@ -72,7 +71,7 @@ def generate_server_users(server, users): server_users = [] for plex_user in server[1].users: username_title = ( - plex_user.username if plex_user.username != "" else plex_user.title + plex_user.username if plex_user.username else plex_user.title ) if ( From 8986c1037bf35d1fdf2a3bff844f1ea03d831c54 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 22 May 2023 01:22:34 -0600 Subject: [PATCH 08/45] Jellyfin: Fix errors with missing matches Signed-off-by: Luigi311 --- src/jellyfin.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index f17f445..11184b9 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -582,9 +582,8 @@ class Jellyfin: ] ): for video in videos: - if ( - movie_provider_id.lower() - in video[movie_provider_source.lower()] + if movie_provider_id.lower() in video.get( + movie_provider_source.lower(), [] ): movie_status = video["status"] break @@ -671,9 +670,8 @@ class Jellyfin: episode_videos = [] for show, seasons in videos.items(): show = {k: v for k, v in show} - if ( - show_provider_id.lower() - in show[show_provider_source.lower()] + if show_provider_id.lower() in show.get( + show_provider_source.lower(), [] ): for season in seasons.values(): for episode in season: @@ -752,7 +750,7 @@ class Jellyfin: if episode_status["completed"]: jellyfin_episode_id = jellyfin_episode["Id"] msg = ( - f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']}" + f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode['Name']}" + f" as watched for {user_name} in {library} for Jellyfin" ) if not dryrun: @@ -768,7 +766,7 @@ class Jellyfin: # TODO add support for partially watched episodes jellyfin_episode_id = jellyfin_episode["Id"] msg = ( - f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']}" + f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode['Name']}" + f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" ) if not dryrun: From 968cb2091d89fe79be309dfea9f1114e04a80967 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 06:45:31 +0000 Subject: [PATCH 09/45] Bump requests from 2.28.2 to 2.31.0 Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index de75d94..2f36597 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ PlexAPI==4.13.4 -requests==2.28.2 +requests==2.31.0 python-dotenv==1.0.0 aiohttp==3.8.4 From 1351bfc1cfa1817c25352aa37db75fc667ae2f87 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Tue, 23 May 2023 14:33:42 -0600 Subject: [PATCH 10/45] Jellyfin: Remove reassigning jellyfin_episode_id Signed-off-by: Luigi311 --- src/jellyfin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 11184b9..78f0484 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -747,8 +747,8 @@ class Jellyfin: break if episode_status: + jellyfin_episode_id = jellyfin_episode["Id"] if episode_status["completed"]: - jellyfin_episode_id = jellyfin_episode["Id"] msg = ( f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode['Name']}" + f" as watched for {user_name} in {library} for Jellyfin" @@ -764,7 +764,6 @@ class Jellyfin: logger(f"Dryrun {msg}", 0) else: # TODO add support for partially watched episodes - jellyfin_episode_id = jellyfin_episode["Id"] msg = ( f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode['Name']}" + f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" From aa5d97a0d529cb96d7c89ee7ef24622acfb09897 Mon Sep 17 00:00:00 2001 From: Lai Jiang Date: Mon, 29 May 2023 21:08:12 -0400 Subject: [PATCH 11/45] Fix a type --- src/black_white.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black_white.py b/src/black_white.py index e32ee08..5b44c03 100644 --- a/src/black_white.py +++ b/src/black_white.py @@ -15,7 +15,7 @@ def setup_black_white_lists( blacklist_library, blacklist_library_type, blacklist_users, - "White", + "Black", library_mapping, user_mapping, ) @@ -24,7 +24,7 @@ def setup_black_white_lists( whitelist_library, whitelist_library_type, whitelist_users, - "Black", + "White", library_mapping, user_mapping, ) From ff2e2deb20ffb9b80af8f61c628bfd5d2fd19ccc Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 28 Jun 2023 16:21:07 -0600 Subject: [PATCH 12/45] Jellyfin: Handle missing paths Signed-off-by: Luigi311 --- src/jellyfin.py | 97 ++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 78f0484..4f71157 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -49,8 +49,11 @@ def get_episode_guids(episode): # Create a dictionary for the episode with its provider IDs and media sources episode_dict = {k.lower(): v for k, v in episode["ProviderIds"].items()} episode_dict["title"] = episode["Name"] - episode_dict["locations"] = tuple( - [x["Path"].split("/")[-1] for x in episode["MediaSources"]] + + episode_dict["locations"] = ( + tuple([x["Path"].split("/")[-1] for x in ["MediaSources"] if "Path" in x]) + if "MediaSources" in episode + else tuple() ) episode_dict["status"] = { @@ -238,7 +241,11 @@ class Jellyfin: k.lower(): v for k, v in show["ProviderIds"].items() } show_guids["title"] = show["Name"] - show_guids["locations"] = tuple([show["Path"].split("/")[-1]]) + show_guids["locations"] = ( + tuple([show["Path"].split("/")[-1]]) + if "Path" in show + else tuple() + ) show_guids = frozenset(show_guids.items()) show_identifiers = { "show_guids": show_guids, @@ -550,24 +557,27 @@ class Jellyfin: if "MediaSources" in jellyfin_video: for movie_location in jellyfin_video["MediaSources"]: - if ( - contains_nested( - movie_location["Path"].split("/")[-1], - videos_movies_ids["locations"], - ) - is not None - ): - for video in videos: - if ( - contains_nested( - movie_location["Path"].split("/")[-1], - video["locations"], - ) - is not None - ): - movie_status = video["status"] - break - break + if "Path" in movie_location: + if ( + contains_nested( + movie_location["Path"].split("/")[-1], + videos_movies_ids["locations"], + ) + is not None + ): + for video in videos: + if ( + contains_nested( + movie_location["Path"].split("/")[ + -1 + ], + video["locations"], + ) + is not None + ): + movie_status = video["status"] + break + break if not movie_status: for ( @@ -697,26 +707,31 @@ class Jellyfin: for episode_location in jellyfin_episode[ "MediaSources" ]: - if ( - contains_nested( - episode_location["Path"].split("/")[-1], - videos_episodes_ids["locations"], - ) - is not None - ): - for episode in episode_videos: - if ( - contains_nested( - episode_location["Path"].split( - "/" - )[-1], - episode["locations"], - ) - is not None - ): - episode_status = episode["status"] - break - break + if "Path" in episode_location: + if ( + contains_nested( + episode_location["Path"].split("/")[ + -1 + ], + videos_episodes_ids["locations"], + ) + is not None + ): + for episode in episode_videos: + if ( + contains_nested( + episode_location[ + "Path" + ].split("/")[-1], + episode["locations"], + ) + is not None + ): + episode_status = episode[ + "status" + ] + break + break if not episode_status: for ( From 29f55104bc22f01f9ffc626172e20517d2a26421 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 28 Jun 2023 16:52:23 -0600 Subject: [PATCH 13/45] Jellyfin: Check for provider_source in episode Signed-off-by: Luigi311 --- src/jellyfin.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 4f71157..32078bc 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -750,15 +750,19 @@ class Jellyfin: ): for episode in episode_videos: if ( - episode_provider_id.lower() - in episode[ - episode_provider_source.lower() - ] + episode_provider_source.lower() + in episode ): - episode_status = episode[ - "status" - ] - break + if ( + episode_provider_id.lower() + in episode[ + episode_provider_source.lower() + ] + ): + episode_status = episode[ + "status" + ] + break break if episode_status: From 81e967864dfbdb201b759c40cb43df4f5c901c7e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 28 Jun 2023 16:55:56 -0600 Subject: [PATCH 14/45] Disable fast fail Signed-off-by: Luigi311 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 826a647..4dcee0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: runs-on: ubuntu-latest needs: pytest strategy: + fail-fast: false matrix: include: - dockerfile: Dockerfile.alpine From 19f77c89e7f6c937a26adae9cc26c98afa640d01 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Tue, 18 Jul 2023 16:27:13 -0600 Subject: [PATCH 15/45] Jellyfin: Fix locations logic Signed-off-by: Luigi311 --- src/jellyfin.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 32078bc..03beeba 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -35,6 +35,8 @@ def get_movie_guids(movie): movie_guids["locations"] = tuple( [x["Path"].split("/")[-1] for x in movie["MediaSources"]] ) + else: + movie_guids["locations"] = tuple() movie_guids["status"] = { "completed": movie["UserData"]["Played"], @@ -50,11 +52,11 @@ def get_episode_guids(episode): episode_dict = {k.lower(): v for k, v in episode["ProviderIds"].items()} episode_dict["title"] = episode["Name"] - episode_dict["locations"] = ( - tuple([x["Path"].split("/")[-1] for x in ["MediaSources"] if "Path" in x]) - if "MediaSources" in episode - else tuple() - ) + episode_dict["locations"] = tuple() + if "MediaSources" in episode: + for x in episode["MediaSources"]: + if "Path" in x: + episode_dict["locations"] += (x["Path"].split("/")[-1],) episode_dict["status"] = { "completed": episode["UserData"]["Played"], From cacbca5a076a149502f8b067bc57db9862049c4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:25:18 +0000 Subject: [PATCH 16/45] Bump aiohttp from 3.8.4 to 3.8.5 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.4 to 3.8.5. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/v3.8.5/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.4...v3.8.5) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2f36597..65308eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ PlexAPI==4.13.4 requests==2.31.0 python-dotenv==1.0.0 -aiohttp==3.8.4 +aiohttp==3.8.5 From 8fab4304a49c70e1ae80c6d7db8bcb0bd7b1a6e8 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 16 Aug 2023 19:00:17 -0600 Subject: [PATCH 17/45] Jellyfin: Remove isPlayed, Use get for name Signed-off-by: Luigi311 --- src/jellyfin.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 03beeba..e4bd4b5 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -14,12 +14,12 @@ from src.watched import ( def get_movie_guids(movie): if "ProviderIds" in movie: logger( - f"Jellyfin: {movie['Name']} {movie['ProviderIds']} {movie['MediaSources']}", + f"Jellyfin: {movie.get('Name')} {movie['ProviderIds']} {movie['MediaSources']}", 3, ) else: logger( - f"Jellyfin: {movie['Name']} {movie['MediaSources']['Path']}", + f"Jellyfin: {movie.get('Name')} {movie['MediaSources']['Path']}", 3, ) @@ -177,7 +177,7 @@ class Jellyfin: for movie in watched["Items"]: if "MediaSources" in movie and movie["MediaSources"] != {}: logger( - f"Jellyfin: Adding {movie['Name']} to {user_name} watched list", + f"Jellyfin: Adding {movie.get('Name')} to {user_name} watched list", 3, ) @@ -198,7 +198,7 @@ class Jellyfin: continue logger( - f"Jellyfin: Adding {movie['Name']} to {user_name} watched list", + f"Jellyfin: Adding {movie.get('Name')} to {user_name} watched list", 3, ) @@ -236,7 +236,7 @@ class Jellyfin: seasons_tasks = [] for show in watched_shows_filtered: logger( - f"Jellyfin: Adding {show['Name']} to {user_name} watched list", + f"Jellyfin: Adding {show.get('Name')} to {user_name} watched list", 3, ) show_guids = { @@ -604,7 +604,7 @@ class Jellyfin: if movie_status: jellyfin_video_id = jellyfin_video["Id"] if movie_status["completed"]: - msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin" + msg = f"{jellyfin_video.get('Name')} as watched for {user_name} in {library} for Jellyfin" if not dryrun: logger(f"Marking {msg}", 0) await self.query( @@ -616,7 +616,7 @@ class Jellyfin: logger(f"Dryrun {msg}", 0) else: # TODO add support for partially watched movies - msg = f"{jellyfin_video['Name']} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" + msg = f"{jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" if not dryrun: pass # logger(f"Marked {msg}", 0) @@ -625,7 +625,7 @@ class Jellyfin: # logger(f"Dryrun {msg}", 0) else: logger( - f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", + f"Jellyfin: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}", 1, ) @@ -634,7 +634,7 @@ class Jellyfin: jellyfin_search = await self.query( f"/Users/{user_id}/Items" + f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}" - + "&isPlayed=false&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series", + + "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series", "get", session, ) @@ -691,7 +691,7 @@ class Jellyfin: if show_found: logger( - f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", + f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show.get('Name')}", 1, ) jellyfin_show_id = jellyfin_show["Id"] @@ -771,7 +771,7 @@ 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['Name']}" + 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" ) if not dryrun: @@ -786,7 +786,7 @@ 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['Name']}" + 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: @@ -797,12 +797,12 @@ class Jellyfin: # logger(f"Dryrun {msg}", 0) else: logger( - f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", + f"Jellyfin: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}", 3, ) else: logger( - f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", + f"Jellyfin: Skipping show {jellyfin_show.get('Name')} as it is not in mark list for {user_name}", 3, ) From ac7f389563e136134949a827a6fdb87deb5e2d6e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 25 Sep 2023 01:59:16 -0600 Subject: [PATCH 18/45] Timeout issues (#103) * Add timeout support for jellyfin requests Signed-off-by: Luigi311 --- .env.sample | 3 +++ README.md | 3 +++ src/jellyfin.py | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.env.sample b/.env.sample index 2f231ea..af6e63e 100644 --- a/.env.sample +++ b/.env.sample @@ -18,6 +18,9 @@ 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" } diff --git a/README.md b/README.md index 35e1c76..899425d 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ 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" } diff --git a/src/jellyfin.py b/src/jellyfin.py index e4bd4b5..2dbd7f3 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -1,5 +1,6 @@ -import asyncio, aiohttp, traceback +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.library import ( @@ -10,6 +11,8 @@ from src.watched import ( combine_watched_dicts, ) +load_dotenv(override=True) + def get_movie_guids(movie): if "ProviderIds" in movie: @@ -70,6 +73,12 @@ class Jellyfin: def __init__(self, baseurl, token): self.baseurl = baseurl self.token = token + self.timeout = aiohttp.ClientTimeout( + total = int(os.getenv("REQUEST_TIMEOUT", 300)), + connect=None, + sock_connect=None, + sock_read=None, + ) if not self.baseurl: raise Exception("Jellyfin baseurl not set") @@ -130,7 +139,7 @@ class Jellyfin: users = {} query_string = "/Users" - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: response = await self.query(query_string, "get", session) # If response is not empty @@ -156,7 +165,7 @@ class Jellyfin: 0, ) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: # Movies if library_type == "Movie": user_watched[user_name][library_title] = [] @@ -404,7 +413,7 @@ class Jellyfin: tasks_watched = [] tasks_libraries = [] - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: libraries = await self.query(f"/Users/{user_id}/Views", "get", session) for library in libraries["Items"]: library_id = library["Id"] @@ -545,7 +554,7 @@ class Jellyfin: f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1, ) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: if videos_movies_ids: jellyfin_search = await self.query( f"/Users/{user_id}/Items" @@ -829,7 +838,7 @@ class Jellyfin: ): try: tasks = [] - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: for user, libraries in watched_list.items(): logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1) user_other = None From fb657d41dbe606a5cdd2173f9bdd6cf1ca1bcc9a Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Thu, 28 Sep 2023 09:47:34 -0600 Subject: [PATCH 19/45] Update apis Signed-off-by: Luigi311 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2f36597..027232b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -PlexAPI==4.13.4 +PlexAPI==4.15.2 requests==2.31.0 python-dotenv==1.0.0 -aiohttp==3.8.4 +aiohttp==3.8.5 From 3a0e60c772006e4aa1351907848d3e3bf906694b Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Thu, 28 Sep 2023 10:00:07 -0600 Subject: [PATCH 20/45] Add max_threads Signed-off-by: Luigi311 --- .env.sample | 6 ++++++ src/functions.py | 5 ++--- src/plex.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index 2f231ea..2953a0e 100644 --- a/.env.sample +++ b/.env.sample @@ -18,6 +18,12 @@ 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" } diff --git a/src/functions.py b/src/functions.py index 2e7529b..49dc67e 100644 --- a/src/functions.py +++ b/src/functions.py @@ -63,12 +63,11 @@ def search_mapping(dictionary: dict, key_value: str): return None -def future_thread_executor(args: list, workers: int = -1): +def future_thread_executor(args: list, threads: int = 32): futures_list = [] results = [] - if workers == -1: - workers = min(32, os.cpu_count() * 2) + workers = min(int(os.getenv("MAX_THREADS", 32)), os.cpu_count() * 2, threads) with ThreadPoolExecutor(max_workers=workers) as executor: for arg in args: diff --git a/src/plex.py b/src/plex.py index 0bcdf7b..715daae 100644 --- a/src/plex.py +++ b/src/plex.py @@ -174,7 +174,7 @@ def get_user_library_watched(user, user_plex, library): args.append([get_user_library_watched_show, show]) for show_guids, episode_guids in future_thread_executor( - args, workers=min(os.cpu_count(), 4) + args, threads=4 ): if show_guids and episode_guids: # append show, season, episode From 2a59f38faf36b3367c56b540b65af2ef46e759c7 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Thu, 28 Sep 2023 10:45:02 -0600 Subject: [PATCH 21/45] Add docker compose to types Signed-off-by: Luigi311 --- .github/ISSUE_TEMPLATE/bug_report.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 298ced2..08d0947 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,7 +24,8 @@ A clear and concise description of what you expected to happen. If applicable, add logs to help explain your problem ideally with DEBUG set to true, be sure to remove sensitive information **Type:** -- [ ] Docker +- [ ] Docker Compose +- [ ] Docker - [ ] Native **Additional context** From bf5d87507904ef90c9085a57ed38f07906851e5e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Thu, 28 Sep 2023 19:41:53 -0600 Subject: [PATCH 22/45] Print server info Signed-off-by: Luigi311 --- src/black_white.py | 40 +--------------------------------------- src/jellyfin.py | 23 +++++++++++++++++++++-- src/main.py | 3 +++ src/plex.py | 9 +++++---- 4 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/black_white.py b/src/black_white.py index 5b44c03..494818a 100644 --- a/src/black_white.py +++ b/src/black_white.py @@ -38,6 +38,7 @@ def setup_black_white_lists( whitelist_users, ) + def setup_x_lists( xlist_library, xlist_library_type, @@ -89,42 +90,3 @@ def setup_x_lists( logger(f"{xlist_type}list Users: {xlist_users}", 1) return xlist_library, xlist_library_type, xlist_users - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/jellyfin.py b/src/jellyfin.py index 2dbd7f3..50555f9 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -74,7 +74,7 @@ class Jellyfin: self.baseurl = baseurl self.token = token self.timeout = aiohttp.ClientTimeout( - total = int(os.getenv("REQUEST_TIMEOUT", 300)), + total=int(os.getenv("REQUEST_TIMEOUT", 300)), connect=None, sock_connect=None, sock_read=None, @@ -88,8 +88,12 @@ class Jellyfin: self.users = asyncio.run(self.get_users()) - async def query(self, query, query_type, session, identifiers=None): + async def query(self, query, query_type, session=None, identifiers=None): try: + if not session: + async with aiohttp.ClientSession(timeout=self.timeout) as session: + return await self.query(query, query_type, session, identifiers) + results = None headers = {"Accept": "application/json", "X-Emby-Token": self.token} authorization = ( @@ -134,6 +138,21 @@ class Jellyfin: logger(f"Jellyfin: Query {query_type} {query}\nResults {results}\n{e}", 2) raise Exception(e) + def info(self) -> str: + try: + query_string = "/System/Info/Public" + + response = asyncio.run(self.query(query_string, "get")) + + if response: + return f"{response['ServerName']}: {response['Version']}" + else: + return None + + except Exception as e: + logger(f"Jellyfin: Get server name failed {e}", 2) + raise Exception(e) + async def get_users(self): try: users = {} diff --git a/src/main.py b/src/main.py index 715e5d8..562dc46 100644 --- a/src/main.py +++ b/src/main.py @@ -304,6 +304,9 @@ def main_loop(): # Start server_2 at the next server in the list for server_2 in servers[servers.index(server_1) + 1 :]: + logger(f"Server 1: {server_1[0].capitalize()}: {server_1[1].info()}", 0) + logger(f"Server 2: {server_2[0].capitalize()}: {server_2[1].info()}", 0) + # Create users list logger("Creating users list", 1) server_1_users, server_2_users = setup_users( diff --git a/src/plex.py b/src/plex.py index 715daae..df80adf 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,4 +1,4 @@ -import re, requests, os, traceback +import re, requests, traceback from urllib3.poolmanager import PoolManager from math import floor @@ -173,9 +173,7 @@ def get_user_library_watched(user, user_plex, library): for show in library_videos.search(inProgress=True): args.append([get_user_library_watched_show, show]) - for show_guids, episode_guids in future_thread_executor( - args, threads=4 - ): + for show_guids, episode_guids in future_thread_executor(args, threads=4): if show_guids and episode_guids: # append show, season, episode if show_guids not in user_watched[user_name][library.title]: @@ -414,6 +412,9 @@ class Plex: logger(f"Plex: Failed to login, Error: {e}", 2) raise Exception(e) + def info(self) -> str: + return f"{self.plex.friendlyName}: {self.plex.version}" + def get_users(self): try: users = self.plex.myPlexAccount().users() 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 23/45] 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 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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 33/45] 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 34/45] 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 35/45] 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 36/45] 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 37/45] 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 From f91005f0bad31c735ea41e6f00699fccefda59d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 01:04:37 +0000 Subject: [PATCH 38/45] Bump aiohttp from 3.8.6 to 3.9.0 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.6 to 3.9.0. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.6...v3.9.0) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 332dbb6..de8801d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ PlexAPI==4.15.5 requests==2.31.0 python-dotenv==1.0.0 -aiohttp==3.8.6 +aiohttp==3.9.0 From dcdbe44648cdbf3fff575cfd840f8ae6f9b54ae5 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 6 Dec 2023 12:04:32 -0700 Subject: [PATCH 39/45] Action: Limit ghcr push to luigi311 Signed-off-by: Luigi311 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5c5bdb..5b9be46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,8 @@ jobs: with: images: | ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }} - # Do not push to ghcr.io on PRs due to permission issues - ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }} + # Do not push to ghcr.io on PRs due to permission issues, only push if the owner is luigi311 so it doesnt fail on forks + ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' && github.repository_owner == 'luigi311'}} tags: | 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' }} From ca5403f97b4704bf707cafc70775209f6f9d30d7 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 6 Dec 2023 12:54:33 -0700 Subject: [PATCH 40/45] Action: Add test Spins up jellyfin and plex containers to test against --- .github/workflows/ci.yml | 43 +++++++++++++++++++- src/jellyfin.py | 10 +++-- src/main.py | 45 +++++++++++++-------- test/ci.env | 86 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 test/ci.env diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5c5bdb..c0d61e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,50 @@ jobs: - name: "Run tests" run: pytest -vvv + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: "Install dependencies" + run: | + pip install -r requirements.txt + sudo apt update && sudo apt install -y docker-compose + + - name: "Checkout JellyPlex-Watched-CI" + uses: actions/checkout@v2 + with: + repository: luigi311/JellyPlex-Watched-CI + path: JellyPlex-Watched-CI + + - name: "Start containers" + run: | + export PGID=$(id -g) + export PUID=$(id -u) + + sudo chown -R $PUID:$PGID JellyPlex-Watched-CI + + docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml up -d + docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml up -d + # Wait for containers to start + sleep 15 + docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml logs + docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml logs + + - name: "Run tests" + run: | + # Move test/.env to root + mv test/ci.env .env + # Run script + python main.py + + cat mark.log + docker: runs-on: ubuntu-latest - needs: pytest + needs: + - pytest + - test env: DEFAULT_VARIANT: alpine strategy: diff --git a/src/jellyfin.py b/src/jellyfin.py index faaa144..41ddbc9 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 - + authorization = ( "MediaBrowser , " 'Client="other", ' @@ -108,8 +108,12 @@ class Jellyfin: 'DeviceId="script", ' 'Version="0.0.0"' ) - headers = {"Accept": "application/json", "X-Emby-Token": self.token, "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 diff --git a/src/main.py b/src/main.py index 562dc46..05b5c96 100644 --- a/src/main.py +++ b/src/main.py @@ -83,17 +83,21 @@ def generate_server_connections(): ) for i, url in enumerate(plex_baseurl): + server = Plex( + baseurl=url.strip(), + token=plex_token[i].strip(), + username=None, + password=None, + servername=None, + ssl_bypass=ssl_bypass, + ) + + logger(f"Plex Server {i} info: {server.info()}", 3) + servers.append( ( "plex", - Plex( - baseurl=url.strip(), - token=plex_token[i].strip(), - username=None, - password=None, - servername=None, - ssl_bypass=ssl_bypass, - ), + server, ) ) @@ -110,17 +114,20 @@ def generate_server_connections(): ) for i, username in enumerate(plex_username): + server = Plex( + baseurl=None, + token=None, + username=username.strip(), + password=plex_password[i].strip(), + servername=plex_servername[i].strip(), + ssl_bypass=ssl_bypass, + ) + + logger(f"Plex Server {i} info: {server.info()}", 3) servers.append( ( "plex", - Plex( - baseurl=None, - token=None, - username=username.strip(), - password=plex_password[i].strip(), - servername=plex_servername[i].strip(), - ssl_bypass=ssl_bypass, - ), + server, ) ) @@ -140,10 +147,14 @@ def generate_server_connections(): baseurl = baseurl.strip() if baseurl[-1] == "/": baseurl = baseurl[:-1] + + server = Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip()) + + logger(f"Jellyfin Server {i} info: {server.info()}", 3) servers.append( ( "jellyfin", - Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip()), + server, ) ) diff --git a/test/ci.env b/test/ci.env new file mode 100644 index 0000000..ba9bd28 --- /dev/null +++ b/test/ci.env @@ -0,0 +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 = "False" + +## Additional logging information +DEBUG = "True" + +## Debugging level, "info" is default, "debug" is more verbose +DEBUG_LEVEL = "debug" + +## If set to true then the script will only run once and then exit +RUN_ONLY_ONCE = "True" + +## How often to run the script in seconds +SLEEP_DURATION = 10 + +## Log file where all output will be written to +LOG_FILE = "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 = 2 + +## Map usernames between servers in the event that they are different, order does not matter +## Comma seperated for multiple options +USER_MAPPING = {"JellyUser":"jellyplex_watched"} + +## Map libraries between servers in the even that they are different, order does not matter +## Comma seperated for multiple options +LIBRARY_MAPPING = { "Shows": "TV Shows" } + + +## 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 seperated for multiple options +#BLACKLIST_LIBRARY = "" +#WHITELIST_LIBRARY = "Movies" +#BLACKLIST_LIBRARY_TYPE = "Series" +#WHITELIST_LIBRARY_TYPE = "Movies, movie" +#BLACKLIST_USERS = "" +WHITELIST_USERS = "jellyplex_watched" + + + +# 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 seperated list for multiple servers +PLEX_BASEURL = "https://localhost:32400" + +## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ +## Comma seperated list for multiple servers +PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y" + +## If not using plex token then use username and password of the server admin along with the servername +## Comma seperated 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 = "True" + +## 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 seperated list for multiple servers +JELLYFIN_BASEURL = "http://localhost:8096" + +## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key +## Comma seperated list for multiple servers +JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c" From 994d529f5919518834ce2c651df913573e37369b Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 6 Dec 2023 12:54:33 -0700 Subject: [PATCH 41/45] Action: Add test Spins up jellyfin and plex containers to test against --- .github/workflows/ci.yml | 43 +++++++++++++++++++- src/jellyfin.py | 10 +++-- src/main.py | 45 +++++++++++++-------- test/ci.env | 86 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 test/ci.env diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b9be46..dad43c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,50 @@ jobs: - name: "Run tests" run: pytest -vvv + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: "Install dependencies" + run: | + pip install -r requirements.txt + sudo apt update && sudo apt install -y docker-compose + + - name: "Checkout JellyPlex-Watched-CI" + uses: actions/checkout@v2 + with: + repository: luigi311/JellyPlex-Watched-CI + path: JellyPlex-Watched-CI + + - name: "Start containers" + run: | + export PGID=$(id -g) + export PUID=$(id -u) + + sudo chown -R $PUID:$PGID JellyPlex-Watched-CI + + docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml up -d + docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml up -d + # Wait for containers to start + sleep 15 + docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml logs + docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml logs + + - name: "Run tests" + run: | + # Move test/.env to root + mv test/ci.env .env + # Run script + python main.py + + cat mark.log + docker: runs-on: ubuntu-latest - needs: pytest + needs: + - pytest + - test env: DEFAULT_VARIANT: alpine strategy: diff --git a/src/jellyfin.py b/src/jellyfin.py index faaa144..41ddbc9 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 - + authorization = ( "MediaBrowser , " 'Client="other", ' @@ -108,8 +108,12 @@ class Jellyfin: 'DeviceId="script", ' 'Version="0.0.0"' ) - headers = {"Accept": "application/json", "X-Emby-Token": self.token, "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 diff --git a/src/main.py b/src/main.py index 562dc46..05b5c96 100644 --- a/src/main.py +++ b/src/main.py @@ -83,17 +83,21 @@ def generate_server_connections(): ) for i, url in enumerate(plex_baseurl): + server = Plex( + baseurl=url.strip(), + token=plex_token[i].strip(), + username=None, + password=None, + servername=None, + ssl_bypass=ssl_bypass, + ) + + logger(f"Plex Server {i} info: {server.info()}", 3) + servers.append( ( "plex", - Plex( - baseurl=url.strip(), - token=plex_token[i].strip(), - username=None, - password=None, - servername=None, - ssl_bypass=ssl_bypass, - ), + server, ) ) @@ -110,17 +114,20 @@ def generate_server_connections(): ) for i, username in enumerate(plex_username): + server = Plex( + baseurl=None, + token=None, + username=username.strip(), + password=plex_password[i].strip(), + servername=plex_servername[i].strip(), + ssl_bypass=ssl_bypass, + ) + + logger(f"Plex Server {i} info: {server.info()}", 3) servers.append( ( "plex", - Plex( - baseurl=None, - token=None, - username=username.strip(), - password=plex_password[i].strip(), - servername=plex_servername[i].strip(), - ssl_bypass=ssl_bypass, - ), + server, ) ) @@ -140,10 +147,14 @@ def generate_server_connections(): baseurl = baseurl.strip() if baseurl[-1] == "/": baseurl = baseurl[:-1] + + server = Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip()) + + logger(f"Jellyfin Server {i} info: {server.info()}", 3) servers.append( ( "jellyfin", - Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip()), + server, ) ) diff --git a/test/ci.env b/test/ci.env new file mode 100644 index 0000000..ba9bd28 --- /dev/null +++ b/test/ci.env @@ -0,0 +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 = "False" + +## Additional logging information +DEBUG = "True" + +## Debugging level, "info" is default, "debug" is more verbose +DEBUG_LEVEL = "debug" + +## If set to true then the script will only run once and then exit +RUN_ONLY_ONCE = "True" + +## How often to run the script in seconds +SLEEP_DURATION = 10 + +## Log file where all output will be written to +LOG_FILE = "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 = 2 + +## Map usernames between servers in the event that they are different, order does not matter +## Comma seperated for multiple options +USER_MAPPING = {"JellyUser":"jellyplex_watched"} + +## Map libraries between servers in the even that they are different, order does not matter +## Comma seperated for multiple options +LIBRARY_MAPPING = { "Shows": "TV Shows" } + + +## 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 seperated for multiple options +#BLACKLIST_LIBRARY = "" +#WHITELIST_LIBRARY = "Movies" +#BLACKLIST_LIBRARY_TYPE = "Series" +#WHITELIST_LIBRARY_TYPE = "Movies, movie" +#BLACKLIST_USERS = "" +WHITELIST_USERS = "jellyplex_watched" + + + +# 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 seperated list for multiple servers +PLEX_BASEURL = "https://localhost:32400" + +## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ +## Comma seperated list for multiple servers +PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y" + +## If not using plex token then use username and password of the server admin along with the servername +## Comma seperated 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 = "True" + +## 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 seperated list for multiple servers +JELLYFIN_BASEURL = "http://localhost:8096" + +## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key +## Comma seperated list for multiple servers +JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c" From b46d4a71661123a3e7d52de04171ad1b76155bd6 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Fri, 8 Dec 2023 11:57:30 -0700 Subject: [PATCH 42/45] Show average time on exit Signed-off-by: Luigi311 --- src/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.py b/src/main.py index 05b5c96..fa99a63 100644 --- a/src/main.py +++ b/src/main.py @@ -422,5 +422,7 @@ def main(): sleep(sleep_duration) except KeyboardInterrupt: + if len(times) > 0: + logger(f"Average time: {sum(times) / len(times)}", 0) logger("Exiting", log_type=0) os._exit(0) From 0190788658723bd09cecf0747fe5e157cc2fbf07 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Fri, 8 Dec 2023 14:16:43 -0700 Subject: [PATCH 43/45] Plex: Use updateTimeline instead of updateProgress Not all accounts have access to updateProgress, so use updateTimeline instead Signed-off-by: Luigi311 --- src/plex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plex.py b/src/plex.py index 87b2b3a..01049c2 100644 --- a/src/plex.py +++ b/src/plex.py @@ -308,7 +308,7 @@ def update_user_watched(user, user_plex, library, videos, dryrun): 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(msg, 5) - movies_search.updateProgress(video_status["time"]) + movies_search.updateTimeline(video_status["time"]) else: logger(msg, 6) @@ -353,7 +353,7 @@ def update_user_watched(user, user_plex, library, videos, dryrun): 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(msg, 5) - episode_search.updateProgress(video_status["time"]) + episode_search.updateTimeline(video_status["time"]) else: logger(msg, 6) From 2d00d8cb3ed9769a9d6a8c14d5e1c35fe3040b09 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sat, 9 Dec 2023 18:37:23 -0700 Subject: [PATCH 44/45] README: Add troubleshooting/Issues Signed-off-by: Luigi311 --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 8b6cfdd..476f933 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,15 @@ Full list of configuration options can be found in the [.env.sample](.env.sample docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest ``` +## Troubleshooting/Issues + +* Jellyfin + * Attempt to decode JSON with unexpected mimetype, make sure you enable remote access or add your docker subnet to lan networks in jellyfin settings + +* Configuration + * Do not use quotes around variables in docker compose + + ## Contributing I am open to receiving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches. From 117932e272fc8a1f0b2de5010b5716bdba805571 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sun, 10 Dec 2023 10:41:59 -0700 Subject: [PATCH 45/45] Use season number instead of season name Using season name is not reliable as it can vary between servers and can be overridden by the user. Signed-off-by: Luigi311 --- src/jellyfin.py | 11 ++++++----- src/plex.py | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/jellyfin.py b/src/jellyfin.py index 41ddbc9..9ab4942 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -331,8 +331,9 @@ class Jellyfin: if len(seasons["Items"]) > 0: for season in seasons["Items"]: season_identifiers = dict(seasons["Identifiers"]) - season_identifiers["season_id"] = season["Id"] - season_identifiers["season_name"] = season["Name"] + season_identifiers["season_index"] = season[ + "IndexNumber" + ] watched_task = asyncio.ensure_future( self.query( f"/Shows/{season_identifiers['show_id']}/Episodes" @@ -390,18 +391,18 @@ class Jellyfin: ] = {} if ( - season_dict["Identifiers"]["season_name"] + season_dict["Identifiers"]["season_index"] not in user_watched[user_name][library_title][ season_dict["Identifiers"]["show_guids"] ] ): user_watched[user_name][library_title][ season_dict["Identifiers"]["show_guids"] - ][season_dict["Identifiers"]["season_name"]] = [] + ][season_dict["Identifiers"]["season_index"]] = [] user_watched[user_name][library_title][ season_dict["Identifiers"]["show_guids"] - ][season_dict["Identifiers"]["season_name"]] = season_dict[ + ][season_dict["Identifiers"]["season_index"]] = season_dict[ "Episodes" ] logger( diff --git a/src/plex.py b/src/plex.py index 01049c2..ba4fb41 100644 --- a/src/plex.py +++ b/src/plex.py @@ -105,17 +105,17 @@ def get_user_library_watched_show(show): for episode in show.episodes(): if episode in watched: - if episode.parentTitle not in episode_guids: - episode_guids[episode.parentTitle] = [] + if episode.parentIndex not in episode_guids: + episode_guids[episode.parentIndex] = [] - episode_guids[episode.parentTitle].append( + episode_guids[episode.parentIndex].append( get_episode_guids(episode, show, completed=True) ) elif episode.viewOffset > 0: - if episode.parentTitle not in episode_guids: - episode_guids[episode.parentTitle] = [] + if episode.parentIndex not in episode_guids: + episode_guids[episode.parentIndex] = [] - episode_guids[episode.parentTitle].append( + episode_guids[episode.parentIndex].append( get_episode_guids(episode, show, completed=False) )