38 Commits

Author SHA1 Message Date
Luigi311
654e7f20e1 Merge pull request #33 from luigi311/dev
Lots of fixes and simplification
2022-12-23 23:13:22 -07:00
Luigi311
1eb92cf7c1 black formatting 2022-12-23 23:11:38 -07:00
Luigi311
111e284cc8 Cleanup 2022-12-23 23:10:51 -07:00
Luigi311
1a4e3f4ec4 Move setup_black_white_list to functions. Fix trailing slash on jellyfin baseurl 2022-12-23 23:02:53 -07:00
Luigi311
4066228e57 Add more debug logging. Do not enable debug by default 2022-12-19 14:07:56 -07:00
Luigi311
59c6d278e3 Add more logging to debug 2022-12-19 13:57:20 -07:00
Luigi311
39b33f3d43 Fix missing logging when using debug level 2022-12-19 13:22:42 -07:00
Luigi311
e8faf52b2b Do not mark shows/movies that do not exist 2022-12-19 01:35:16 -07:00
Luigi311
370e9bac63 change get user watched name to avoid mistakes 2022-12-18 22:39:03 -07:00
Luigi311
d0746cec5a Fix server 2 always running async runner. Speedup plex get watched 2022-12-18 22:27:42 -07:00
Luigi311
251937431b Move cleanup_watched to functions and simplify 2022-12-18 01:50:45 -07:00
Luigi311
50faf061af Remove dockerfile defaults 2022-11-21 18:21:29 -07:00
Luigi311
9ffbc49ad3 Merge pull request #30 from luigi311/dev
Add ssl_bypass to skip hostname validation.
2022-11-21 17:39:00 -07:00
Luigi311
644dc8e3af Merge pull request #29 from lgtm-migrator/codeql
Add CodeQL workflow for GitHub code scanning
2022-11-21 17:38:45 -07:00
Luigi311
47bc4e94dc Fix dockerfile 2022-11-21 17:31:47 -07:00
LGTM Migrator
f17d39fe17 Add CodeQL workflow for GitHub code scanning 2022-11-10 14:41:07 +00:00
Luigi311
966dcacf8d Add ssl_bypass to skip hostname validation. 2022-09-25 14:16:01 -06:00
Luigi311
9afc00443c Merge pull request #27 from luigi311/dev
Cleanup issues
2022-08-18 00:46:00 -06:00
Luigi311
3ec177ea64 rename test_main 2022-08-18 00:17:32 -06:00
Luigi311
b360c9fd0b Remove unnecessary deepcopy 2022-08-18 00:15:42 -06:00
Luigi311
1ed791b1ed Fix jellyfin 2022-08-17 23:49:05 -06:00
Luigi311
f19b1a3063 Cleanup length and functions instead of methods 2022-08-17 23:34:45 -06:00
Luigi311
190a72bd3c Cleanup 2022-08-17 22:53:27 -06:00
Luigi311
c848106ce7 Black cleanup 2022-08-17 22:31:23 -06:00
Luigi311
dd319271bd Cleanup 2022-08-17 22:09:11 -06:00
Luigi311
16879cc728 Merge pull request #26 from luigi311/dev
Use async for jellyfin
2022-08-17 21:49:34 -06:00
Luigi311
942ec3533f Cleanup log file on runs 2022-08-17 21:43:51 -06:00
Luigi311
9f6edfc91a Merge branch 'main' into dev 2022-08-17 21:40:25 -06:00
Luigi311
827ace2e97 cleanup 2022-08-17 21:20:28 -06:00
Luigi311
f6b57a1b4d Update README.md 2022-07-10 01:38:42 -06:00
Luigi311
88a7526721 Use async for jellyfin (#23)
* Use async

* Massive jellyfin watched speedup

Co-authored-by: Luigi311 <luigi311.lg@gmail.com>
2022-07-10 01:30:12 -06:00
luigi311
1efb4d8543 Fix debug 2022-07-06 17:22:35 -06:00
Luigi311
7571e9a343 Merge pull request #22 from luigi311/dev
Fix errors on certain edge cases
2022-07-05 21:23:14 -06:00
Luigi311
7640e9ee03 fix typo 2022-07-05 19:26:58 -06:00
Luigi311
50ed3d6400 Fix user_name in plex 2022-07-05 19:26:22 -06:00
Luigi311
c9a373851f Remove indexnumber from logging 2022-07-05 19:16:25 -06:00
Luigi311
a3f3db8f4e Use generate_library_guids_dict instead of library type 2022-07-05 18:09:08 -06:00
Luigi311
de619de923 Add more logging, fix username in jellyfin mark. 2022-07-05 16:35:22 -06:00
18 changed files with 3549 additions and 2510 deletions

View File

@@ -24,7 +24,8 @@ PLEX_TOKEN = "SuperSecretToken"
#PLEX_USERNAME = "" #PLEX_USERNAME = ""
#PLEX_PASSWORD = "" #PLEX_PASSWORD = ""
#PLEX_SERVERNAME = "Plex Server" #PLEX_SERVERNAME = "Plex Server"
## Skip hostname validation for ssl certificates.
SSL_BYPASS = "False"
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers

41
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: "23 20 * * 6"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ python ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

View File

@@ -6,16 +6,16 @@ ENV DEBUG_LEVEL 'INFO'
ENV SLEEP_DURATION '3600' ENV SLEEP_DURATION '3600'
ENV LOGFILE 'log.log' ENV LOGFILE 'log.log'
ENV USER_MAPPING '{ "User Test": "User Test2" }' ENV USER_MAPPING ''
ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }' ENV LIBRARY_MAPPING ''
ENV PLEX_BASEURL 'http://localhost:32400' ENV PLEX_BASEURL ''
ENV PLEX_TOKEN '' ENV PLEX_TOKEN ''
ENV PLEX_USERNAME '' ENV PLEX_USERNAME ''
ENV PLEX_PASSWORD '' ENV PLEX_PASSWORD ''
ENV PLEX_SERVERNAME '' ENV PLEX_SERVERNAME ''
ENV JELLYFIN_BASEURL 'http://localhost:8096' ENV JELLYFIN_BASEURL ''
ENV JELLYFIN_TOKEN '' ENV JELLYFIN_TOKEN ''
ENV BLACKLIST_LIBRARY '' ENV BLACKLIST_LIBRARY ''
@@ -27,6 +27,12 @@ ENV WHITELIST_USERS ''
WORKDIR /app WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY ./requirements.txt ./ COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

View File

@@ -56,7 +56,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
#### With .env #### With .env
- Create a .env file similar to .env.sample and set the MNEMONIC variable to your seed phrase - Create a .env file similar to .env.sample and set the variables to match your setup
- Run - Run

View File

@@ -1,10 +1,11 @@
import sys import sys
if __name__ == '__main__': if __name__ == "__main__":
# Check python version 3.6 or higher # Check python version 3.6 or higher
if not (3, 6) <= tuple(map(int, sys.version_info[:2])): if not (3, 6) <= tuple(map(int, sys.version_info[:2])):
print("This script requires Python 3.6 or higher") print("This script requires Python 3.6 or higher")
sys.exit(1) sys.exit(1)
from src.main import main from src.main import main
main() main()

View File

@@ -1,3 +1,4 @@
plexapi plexapi
requests requests
python-dotenv python-dotenv
aiohttp

View File

@@ -1,4 +1,4 @@
import os import os, copy
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -6,19 +6,22 @@ load_dotenv(override=True)
logfile = os.getenv("LOGFILE", "log.log") logfile = os.getenv("LOGFILE", "log.log")
def logger(message: str, log_type=0): def logger(message: str, log_type=0):
debug = str_to_bool(os.getenv("DEBUG", "True")) debug = str_to_bool(os.getenv("DEBUG", "False"))
debug_level = os.getenv("DEBUG_LEVEL", "info").lower() debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
output = str(message) output = str(message)
if log_type == 0: if log_type == 0:
pass pass
elif log_type == 1 and (debug or debug_level == "info"): elif log_type == 1 and (debug and debug_level in ("info", "debug")):
output = f"[INFO]: {output}" output = f"[INFO]: {output}"
elif log_type == 2: elif log_type == 2:
output = f"[ERROR]: {output}" output = f"[ERROR]: {output}"
elif log_type == 3 and (debug and debug_level == "debug"): elif log_type == 3 and (debug and debug_level == "debug"):
output = f"[DEBUG]: {output}" output = f"[DEBUG]: {output}"
elif log_type == 4:
output = f"[WARNING]: {output}"
else: else:
output = None output = None
@@ -27,6 +30,7 @@ def logger(message: str, log_type=0):
file = open(logfile, "a", encoding="utf-8") file = open(logfile, "a", encoding="utf-8")
file.write(output + "\n") file.write(output + "\n")
# Reimplementation of distutils.util.strtobool due to it being deprecated # Reimplementation of distutils.util.strtobool due to it being deprecated
# Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668 # Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
def str_to_bool(value: any) -> bool: def str_to_bool(value: any) -> bool:
@@ -34,6 +38,7 @@ def str_to_bool(value: any) -> bool:
return False return False
return str(value).lower() in ("y", "yes", "t", "true", "on", "1") return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
# Get mapped value # Get mapped value
def search_mapping(dictionary: dict, key_value: str): def search_mapping(dictionary: dict, key_value: str):
if key_value in dictionary.keys(): if key_value in dictionary.keys():
@@ -43,12 +48,124 @@ def search_mapping(dictionary: dict, key_value: str):
elif key_value in dictionary.values(): elif key_value in dictionary.values():
return list(dictionary.keys())[list(dictionary.values()).index(key_value)] return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
elif key_value.lower() in dictionary.values(): elif key_value.lower() in dictionary.values():
return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())] return list(dictionary.keys())[
list(dictionary.values()).index(key_value.lower())
]
else: else:
return None return None
def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): def setup_black_white_lists(
blacklist_library: str,
whitelist_library: str,
blacklist_library_type: str,
whitelist_library_type: str,
blacklist_users: str,
whitelist_users: str,
library_mapping=None,
user_mapping=None,
):
if blacklist_library:
if len(blacklist_library) > 0:
blacklist_library = blacklist_library.split(",")
blacklist_library = [x.strip() for x in blacklist_library]
if library_mapping:
temp_library = []
for library in blacklist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
blacklist_library = blacklist_library + temp_library
else:
blacklist_library = []
logger(f"Blacklist Library: {blacklist_library}", 1)
if whitelist_library:
if len(whitelist_library) > 0:
whitelist_library = whitelist_library.split(",")
whitelist_library = [x.strip() for x in whitelist_library]
if library_mapping:
temp_library = []
for library in whitelist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
whitelist_library = whitelist_library + temp_library
else:
whitelist_library = []
logger(f"Whitelist Library: {whitelist_library}", 1)
if blacklist_library_type:
if len(blacklist_library_type) > 0:
blacklist_library_type = blacklist_library_type.split(",")
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
else:
blacklist_library_type = []
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
if whitelist_library_type:
if len(whitelist_library_type) > 0:
whitelist_library_type = whitelist_library_type.split(",")
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
else:
whitelist_library_type = []
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
if blacklist_users:
if len(blacklist_users) > 0:
blacklist_users = blacklist_users.split(",")
blacklist_users = [x.lower().strip() for x in blacklist_users]
if user_mapping:
temp_users = []
for user in blacklist_users:
user_other = search_mapping(user_mapping, user)
if user_other:
temp_users.append(user_other)
blacklist_users = blacklist_users + temp_users
else:
blacklist_users = []
logger(f"Blacklist Users: {blacklist_users}", 1)
if whitelist_users:
if len(whitelist_users) > 0:
whitelist_users = whitelist_users.split(",")
whitelist_users = [x.lower().strip() for x in whitelist_users]
if user_mapping:
temp_users = []
for user in whitelist_users:
user_other = search_mapping(user_mapping, user)
if user_other:
temp_users.append(user_other)
whitelist_users = whitelist_users + temp_users
else:
whitelist_users = []
else:
whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1)
return (
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
)
def check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
skip_reason = None skip_reason = None
if library_type.lower() in blacklist_library_type: if library_type.lower() in blacklist_library_type:
@@ -80,15 +197,18 @@ def check_skip_logic(library_title, library_type, blacklist_library, whitelist_l
return skip_reason return skip_reason
def generate_library_guids_dict(user_list: dict, generate_output: int): def generate_library_guids_dict(user_list: dict):
# if generate_output is 0 then only generate shows, if 1 then only generate episodes, if 2 then generate movies, if 3 then generate shows and episodes
show_output_dict = {} show_output_dict = {}
episode_output_dict = {} episode_output_dict = {}
movies_output_dict = {} movies_output_dict = {}
if generate_output in (0, 3): # Handle the case where user_list is empty or does not contain the expected keys and values
if not user_list:
return show_output_dict, episode_output_dict, movies_output_dict
try:
show_output_keys = user_list.keys() show_output_keys = user_list.keys()
show_output_keys = ([ dict(x) for x in list(show_output_keys) ]) show_output_keys = [dict(x) for x in list(show_output_keys)]
for show_key in show_output_keys: for show_key in show_output_keys:
for provider_key, provider_value in show_key.items(): for provider_key, provider_value in show_key.items():
# Skip title # Skip title
@@ -100,9 +220,13 @@ def generate_library_guids_dict(user_list: dict, generate_output: int):
for show_location in provider_value: for show_location in provider_value:
show_output_dict[provider_key.lower()].append(show_location) show_output_dict[provider_key.lower()].append(show_location)
else: else:
show_output_dict[provider_key.lower()].append(provider_value.lower()) show_output_dict[provider_key.lower()].append(
provider_value.lower()
)
except Exception:
logger("Generating show_output_dict failed, skipping", 1)
if generate_output in (1, 3): try:
for show in user_list: for show in user_list:
for season in user_list[show]: for season in user_list[show]:
for episode in user_list[show][season]: for episode in user_list[show][season]:
@@ -111,11 +235,17 @@ def generate_library_guids_dict(user_list: dict, generate_output: int):
episode_output_dict[episode_key.lower()] = [] episode_output_dict[episode_key.lower()] = []
if episode_key == "locations": if episode_key == "locations":
for episode_location in episode_value: for episode_location in episode_value:
episode_output_dict[episode_key.lower()].append(episode_location) episode_output_dict[episode_key.lower()].append(
episode_location
)
else: else:
episode_output_dict[episode_key.lower()].append(episode_value.lower()) episode_output_dict[episode_key.lower()].append(
episode_value.lower()
)
except Exception:
logger("Generating episode_output_dict failed, skipping", 1)
if generate_output == 2: try:
for movie in user_list: for movie in user_list:
for movie_key, movie_value in movie.items(): for movie_key, movie_value in movie.items():
if movie_key.lower() not in movies_output_dict: if movie_key.lower() not in movies_output_dict:
@@ -125,15 +255,202 @@ def generate_library_guids_dict(user_list: dict, generate_output: int):
movies_output_dict[movie_key.lower()].append(movie_location) movies_output_dict[movie_key.lower()].append(movie_location)
else: else:
movies_output_dict[movie_key.lower()].append(movie_value.lower()) movies_output_dict[movie_key.lower()].append(movie_value.lower())
except Exception:
logger("Generating movies_output_dict failed, skipping", 1)
return show_output_dict, episode_output_dict, movies_output_dict return show_output_dict, episode_output_dict, movies_output_dict
def combine_watched_dicts(dicts: list):
combined_dict = {}
for single_dict in dicts:
for key, value in single_dict.items():
if key not in combined_dict:
combined_dict[key] = {}
for subkey, subvalue in value.items():
if subkey in combined_dict[key]:
# If the subkey already exists in the combined dictionary,
# check if the values are different and raise an exception if they are
if combined_dict[key][subkey] != subvalue:
raise ValueError(
f"Conflicting values for subkey '{subkey}' under key '{key}'"
)
else:
# If the subkey does not exist in the combined dictionary, add it
combined_dict[key][subkey] = subvalue
return combined_dict
def cleanup_watched(
watched_list_1, watched_list_2, user_mapping=None, library_mapping=None
):
modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from watched_list_1 that are in watched_list_2
for user_1 in watched_list_1:
user_other = None
if user_mapping:
user_other = search_mapping(user_mapping, user_1)
user_2 = get_other(watched_list_2, user_1, user_other)
if user_2 is None:
continue
for library_1 in watched_list_1[user_1]:
library_other = None
if library_mapping:
library_other = search_mapping(library_mapping, library_1)
library_2 = get_other(watched_list_2[user_2], library_1, library_other)
if library_2 is None:
continue
(
_,
episode_watched_list_2_keys_dict,
movies_watched_list_2_keys_dict,
) = generate_library_guids_dict(watched_list_2[user_2][library_2])
# Movies
if isinstance(watched_list_1[user_1][library_1], list):
for movie in watched_list_1[user_1][library_1]:
if is_movie_in_dict(movie, movies_watched_list_2_keys_dict):
logger(f"Removing {movie} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(movie)
# TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict):
for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1)
for season in watched_list_1[user_1][library_1][show_key_1]:
for episode in watched_list_1[user_1][library_1][show_key_1][
season
]:
if is_episode_in_dict(
episode, episode_watched_list_2_keys_dict
):
if (
episode
in modified_watched_list_1[user_1][library_1][
show_key_1
][season]
):
logger(
f"Removing {episode} from {show_key_dict['title']}",
3,
)
modified_watched_list_1[user_1][library_1][
show_key_1
][season].remove(episode)
# Remove empty seasons
if (
len(
modified_watched_list_1[user_1][library_1][show_key_1][
season
]
)
== 0
):
if (
season
in modified_watched_list_1[user_1][library_1][
show_key_1
]
):
logger(
f"Removing {season} from {show_key_dict['title']} because it is empty",
3,
)
del modified_watched_list_1[user_1][library_1][
show_key_1
][season]
# Remove empty shows
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
if show_key_1 in modified_watched_list_1[user_1][library_1]:
logger(
f"Removing {show_key_dict['title']} because it is empty",
3,
)
del modified_watched_list_1[user_1][library_1][show_key_1]
for user_1 in watched_list_1:
for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]:
# If library is empty then remove it
if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1]
if user_1 in modified_watched_list_1:
# If user is empty delete user
if len(modified_watched_list_1[user_1]) == 0:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
del modified_watched_list_1[user_1]
return modified_watched_list_1
def get_other(watched_list_2, object_1, object_2):
if object_1 in watched_list_2:
return object_1
elif object_2 in watched_list_2:
return object_2
else:
logger(f"{object_1} and {object_2} not found in watched list 2", 1)
return None
def is_movie_in_dict(movie, movies_watched_list_2_keys_dict):
# Iterate through the keys and values of the movie dictionary
for movie_key, movie_value in movie.items():
# If the key is "locations", check if the "locations" key is present in the movies_watched_list_2_keys_dict dictionary
if movie_key == "locations":
if "locations" in movies_watched_list_2_keys_dict.keys():
# Iterate through the locations in the movie dictionary
for location in movie_value:
# If the location is in the movies_watched_list_2_keys_dict dictionary, return True
if location in movies_watched_list_2_keys_dict["locations"]:
return True
# If the key is not "locations", check if the movie_key is present in the movies_watched_list_2_keys_dict dictionary
else:
if movie_key in movies_watched_list_2_keys_dict.keys():
# If the movie_value is in the movies_watched_list_2_keys_dict dictionary, return True
if movie_value in movies_watched_list_2_keys_dict[movie_key]:
return True
# If the loop completes without finding a match, return False
return False
def is_episode_in_dict(episode, episode_watched_list_2_keys_dict):
# Iterate through the keys and values of the episode dictionary
for episode_key, episode_value in episode.items():
# If the key is "locations", check if the "locations" key is present in the episode_watched_list_2_keys_dict dictionary
if episode_key == "locations":
if "locations" in episode_watched_list_2_keys_dict.keys():
# Iterate through the locations in the episode dictionary
for location in episode_value:
# If the location is in the episode_watched_list_2_keys_dict dictionary, return True
if location in episode_watched_list_2_keys_dict["locations"]:
return True
# If the key is not "locations", check if the episode_key is present in the episode_watched_list_2_keys_dict dictionary
else:
if episode_key in episode_watched_list_2_keys_dict.keys():
# If the episode_value is in the episode_watched_list_2_keys_dict dictionary, return True
if episode_value in episode_watched_list_2_keys_dict[episode_key]:
return True
# If the loop completes without finding a match, return False
return False
def future_thread_executor(args: list, workers: int = -1): def future_thread_executor(args: list, workers: int = -1):
futures_list = [] futures_list = []
results = [] results = []
if workers == -1: if workers == -1:
workers = min(32, os.cpu_count()*1.25) workers = min(32, os.cpu_count() * 2)
with ThreadPoolExecutor(max_workers=workers) as executor: with ThreadPoolExecutor(max_workers=workers) as executor:
for arg in args: for arg in args:

View File

@@ -1,11 +1,17 @@
import requests import asyncio, aiohttp
from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict, future_thread_executor from src.functions import (
logger,
search_mapping,
check_skip_logic,
generate_library_guids_dict,
combine_watched_dicts,
)
class Jellyfin():
class Jellyfin:
def __init__(self, baseurl, token): def __init__(self, baseurl, token):
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
self.session = requests.Session()
if not self.baseurl: if not self.baseurl:
raise Exception("Jellyfin baseurl not set") raise Exception("Jellyfin baseurl not set")
@@ -13,43 +19,49 @@ class Jellyfin():
if not self.token: if not self.token:
raise Exception("Jellyfin token not set") raise Exception("Jellyfin token not set")
self.users = self.get_users() self.users = asyncio.run(self.get_users())
async def query(self, query, query_type, session, identifiers=None):
def query(self, query, query_type):
try: try:
response = None results = None
headers = {"Accept": "application/json", "X-Emby-Token": self.token}
headers = {
"Accept": "application/json",
"X-Emby-Token": self.token
}
if query_type == "get":
response = self.session.get(self.baseurl + query, headers=headers)
elif query_type == "post":
authorization = ( authorization = (
'MediaBrowser , ' "MediaBrowser , "
'Client="other", ' 'Client="other", '
'Device="script", ' 'Device="script", '
'DeviceId="script", ' 'DeviceId="script", '
'Version="0.0.0"' 'Version="0.0.0"'
) )
headers["X-Emby-Authorization"] = authorization headers["X-Emby-Authorization"] = authorization
response = self.session.post(self.baseurl + query, headers=headers)
return response.json() if query_type == "get":
async with session.get(
self.baseurl + query, headers=headers
) as response:
results = await response.json()
elif query_type == "post":
async with session.post(
self.baseurl + query, headers=headers
) as response:
results = await response.json()
# append identifiers to results
if identifiers:
results["Identifiers"] = identifiers
return results
except Exception as e: except Exception as e:
logger(f"Jellyfin: Query failed {e}", 2) logger(f"Jellyfin: Query failed {e}", 2)
raise Exception(e) raise Exception(e)
def get_users(self): async def get_users(self):
try: try:
users = {} users = {}
query = "/Users" query_string = "/Users"
response = self.query(query, "get") async with aiohttp.ClientSession() as session:
response = await self.query(query_string, "get", session)
# If reponse is not empty # If reponse is not empty
if response: if response:
@@ -61,98 +73,353 @@ class Jellyfin():
logger(f"Jellyfin: Get users failed {e}", 2) logger(f"Jellyfin: Get users failed {e}", 2)
raise Exception(e) raise Exception(e)
def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): async def get_user_library_watched(
self, user_name, user_id, library_type, library_id, library_title
):
try: try:
user_name = user_name.lower() user_name = user_name.lower()
user_watched = {} user_watched = {}
user_watched[user_name] = {} user_watched[user_name] = {}
logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0) logger(
f"Jellyfin: Generating watched for {user_name} in library {library_title}",
0,
)
async with aiohttp.ClientSession() as session:
# Movies # Movies
if library_type == "Movie": if library_type == "Movie":
user_watched[user_name][library_title] = [] user_watched[user_name][library_title] = []
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get") watched = await self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
session,
)
for movie in watched["Items"]: for movie in watched["Items"]:
if movie["UserData"]["Played"] == True: # Check if the movie has been played
movie_guids = {} if (
movie_guids["title"] = movie["Name"] movie["UserData"]["Played"] is True
if movie["ProviderIds"]: and "MediaSources" in movie
# Lowercase movie["ProviderIds"] keys and movie["MediaSources"] is not {}
movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()} ):
if movie["MediaSources"]: logger(
movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]]) f"Jellyfin: Adding {movie['Name']} to {user_name} watched list",
3,
)
if "ProviderIds" in movie:
logger(
f"Jellyfin: {movie['Name']} {movie['ProviderIds']} {movie['MediaSources']}",
3,
)
else:
logger(
f"Jellyfin: {movie['Name']} {movie['MediaSources']['Path']}",
3,
)
# Create a dictionary for the movie with its title
movie_guids = {"title": movie["Name"]}
# If the movie has provider IDs, add them to the dictionary
if "ProviderIds" in movie:
movie_guids.update(
{
k.lower(): v
for k, v in movie["ProviderIds"].items()
}
)
# If the movie has media sources, add them to the dictionary
if "MediaSources" in movie:
movie_guids["locations"] = tuple(
[
x["Path"].split("/")[-1]
for x in movie["MediaSources"]
]
)
# Append the movie dictionary to the list for the given user and library
user_watched[user_name][library_title].append(movie_guids) user_watched[user_name][library_title].append(movie_guids)
logger(
f"Jellyfin: Added {movie_guids} to {user_name} watched list",
3,
)
# TV Shows # TV Shows
if library_type == "Episode": if library_type == "Series":
# Initialize an empty dictionary for the given user and library
user_watched[user_name][library_title] = {} user_watched[user_name][library_title] = {}
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds,Path", "get")
watched_shows = [x for x in watched["Items"] if x["Type"] == "Series"]
for show in watched_shows: # Retrieve a list of watched TV shows
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()} watched_shows = await self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount",
"get",
session,
)
# Filter the list of shows to only include those that have been partially or fully watched
watched_shows_filtered = []
for show in watched_shows["Items"]:
if "PlayedPercentage" in show["UserData"]:
if show["UserData"]["PlayedPercentage"] > 0:
watched_shows_filtered.append(show)
# Create a list of tasks to retrieve the seasons of each watched show
seasons_tasks = []
for show in watched_shows_filtered:
logger(
f"Jellyfin: Adding {show['Name']} to {user_name} watched list",
3,
)
show_guids = {
k.lower(): v for k, v in show["ProviderIds"].items()
}
show_guids["title"] = show["Name"] show_guids["title"] = show["Name"]
show_guids["locations"] = tuple([show["Path"].split("/")[-1]]) show_guids["locations"] = tuple([show["Path"].split("/")[-1]])
show_guids = frozenset(show_guids.items()) show_guids = frozenset(show_guids.items())
seasons = self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") show_identifiers = {
"show_guids": show_guids,
"show_id": show["Id"],
}
season_task = asyncio.ensure_future(
self.query(
f"/Shows/{show['Id']}/Seasons"
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount",
"get",
session,
frozenset(show_identifiers.items()),
)
)
seasons_tasks.append(season_task)
# Retrieve the seasons for each watched show
seasons_watched = await asyncio.gather(*seasons_tasks)
# Filter the list of seasons to only include those that have been partially or fully watched
seasons_watched_filtered = []
for seasons in seasons_watched:
seasons_watched_filtered_dict = {}
seasons_watched_filtered_dict["Identifiers"] = seasons[
"Identifiers"
]
seasons_watched_filtered_dict["Items"] = []
for season in seasons["Items"]:
if "PlayedPercentage" in season["UserData"]:
if season["UserData"]["PlayedPercentage"] > 0:
seasons_watched_filtered_dict["Items"].append(
season
)
if seasons_watched_filtered_dict["Items"]:
seasons_watched_filtered.append(
seasons_watched_filtered_dict
)
# Create a list of tasks to retrieve the episodes of each watched season
episodes_tasks = []
for seasons in seasons_watched_filtered:
if len(seasons["Items"]) > 0: if len(seasons["Items"]) > 0:
for season in seasons["Items"]: for season in seasons["Items"]:
episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") season_identifiers = dict(seasons["Identifiers"])
if len(episodes["Items"]) > 0: season_identifiers["season_id"] = season["Id"]
for episode in episodes["Items"]: season_identifiers["season_name"] = season["Name"]
if episode["UserData"]["Played"] == True: episode_task = asyncio.ensure_future(
if episode["ProviderIds"] or episode["MediaSources"]: self.query(
if show_guids not in user_watched[user_name][library_title]: f"/Shows/{season_identifiers['show_id']}/Episodes"
user_watched[user_name][library_title][show_guids] = {} + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources",
if season["Name"] not in user_watched[user_name][library_title][show_guids]: "get",
user_watched[user_name][library_title][show_guids][season["Name"]] = [] session,
frozenset(season_identifiers.items()),
)
)
episodes_tasks.append(episode_task)
# Lowercase episode["ProviderIds"] keys # Retrieve the episodes for each watched season
episode_guids = {} watched_episodes = await asyncio.gather(*episodes_tasks)
if episode["ProviderIds"]:
episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()} # Iterate through the watched episodes
if episode["MediaSources"]: for episodes in watched_episodes:
episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]]) # If the season has any watched episodes
user_watched[user_name][library_title][show_guids][season["Name"]].append(episode_guids) if len(episodes["Items"]) > 0:
# Create a dictionary for the season with its identifier and episodes
season_dict = {}
season_dict["Identifiers"] = dict(episodes["Identifiers"])
season_dict["Episodes"] = []
for episode in episodes["Items"]:
if (
episode["UserData"]["Played"] is True
and "MediaSources" in episode
and episode["MediaSources"] is not {}
):
# 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"]
]
)
# Add the episode dictionary to the season's list of episodes
season_dict["Episodes"].append(episode_dict)
# Add the season dictionary to the show's list of seasons
if (
season_dict["Identifiers"]["show_guids"]
not in user_watched[user_name][library_title]
):
user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
] = {}
if (
season_dict["Identifiers"]["season_name"]
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"]] = []
user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
][season_dict["Identifiers"]["season_name"]] = season_dict[
"Episodes"
]
logger(
f"Jellyfin: Added {season_dict['Episodes']} to {user_name} {season_dict['Identifiers']['show_guids']} watched list",
1,
)
logger(
f"Jellyfin: Got watched for {user_name} in library {library_title}", 1
)
if library_title in user_watched[user_name]:
logger(f"Jellyfin: {user_watched[user_name][library_title]}", 3)
return user_watched return user_watched
except Exception as e: except Exception as e:
logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2) logger(
f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
2,
)
raise Exception(e) raise Exception(e)
async def get_users_watched(
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): self,
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
try: try:
users_watched = {}
args = []
for user_name, user_id in users.items():
# Get all libraries # Get all libraries
user_name = user_name.lower() user_name = user_name.lower()
tasks_watched = []
libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] tasks_libraries = []
async with aiohttp.ClientSession() as session:
for library in libraries: libraries = await self.query(f"/Users/{user_id}/Views", "get", session)
library_title = library["Name"] for library in libraries["Items"]:
library_id = library["Id"] library_id = library["Id"]
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&limit=1", "get") library_title = library["Name"]
identifiers = {
"library_id": library_id,
"library_title": library_title,
}
task = asyncio.ensure_future(
self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&limit=1",
"get",
session,
identifiers=identifiers,
)
)
tasks_libraries.append(task)
libraries = await asyncio.gather(
*tasks_libraries, return_exceptions=True
)
for watched in libraries:
if len(watched["Items"]) == 0: if len(watched["Items"]) == 0:
logger(f"Jellyfin: No watched items found in library {library_title}", 1)
continue continue
else:
library_id = watched["Identifiers"]["library_id"]
library_title = watched["Identifiers"]["library_title"]
library_type = watched["Items"][0]["Type"] library_type = watched["Items"][0]["Type"]
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
if skip_reason: if skip_reason:
logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) logger(
f"Jellyfin: Skipping library {library_title} {skip_reason}",
1,
)
continue continue
args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) # Get watched for user
task = asyncio.ensure_future(
self.get_user_library_watched(
user_name, user_id, library_type, library_id, library_title
)
)
tasks_watched.append(task)
for user_watched in future_thread_executor(args): watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
for user, user_watched_temp in user_watched.items(): return watched
except Exception as e:
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
raise Exception(e)
async def get_watched(
self,
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping=None,
):
try:
users_watched = {}
watched = []
for user_name, user_id in users.items():
watched.append(
self.get_users_watched(
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
)
watched = await asyncio.gather(*watched, return_exceptions=True)
for user_watched in watched:
user_watched_temp = combine_watched_dicts(user_watched)
for user, user_watched_temp in user_watched_temp.items():
if user not in users_watched: if user not in users_watched:
users_watched[user] = {} users_watched[user] = {}
users_watched[user].update(user_watched_temp) users_watched[user].update(user_watched_temp)
@@ -162,106 +429,210 @@ class Jellyfin():
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2) logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
raise Exception(e) raise Exception(e)
def update_user_watched(self, user, user_id, library, library_id, videos, dryrun): async def update_user_watched(
self, user_name, user_id, library, library_id, videos, dryrun
):
try: try:
logger(f"Jellyfin: Updating watched for {user} in library {library}", 1) logger(
library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get") f"Jellyfin: Updating watched for {user_name} in library {library}", 1
library_type = library_search["Items"][0]["Type"] )
(
videos_shows_ids,
videos_episodes_ids,
videos_movies_ids,
) = generate_library_guids_dict(videos)
# Movies logger(
if library_type == "Movie": f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
_, _, videos_movies_ids = generate_library_guids_dict(videos, 2) 1,
)
jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", "get") async with aiohttp.ClientSession() as session:
if videos_movies_ids:
jellyfin_search = await self.query(
f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}"
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
session,
)
for jellyfin_video in jellyfin_search["Items"]: for jellyfin_video in jellyfin_search["Items"]:
movie_found = False movie_found = False
if "MediaSources" in jellyfin_video: if "MediaSources" in jellyfin_video:
for movie_location in jellyfin_video["MediaSources"]: for movie_location in jellyfin_video["MediaSources"]:
if movie_location["Path"].split("/")[-1] in videos_movies_ids["locations"]: if (
movie_location["Path"].split("/")[-1]
in videos_movies_ids["locations"]
):
movie_found = True movie_found = True
break break
if not movie_found: if not movie_found:
for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items(): for (
movie_provider_source,
movie_provider_id,
) in jellyfin_video["ProviderIds"].items():
if movie_provider_source.lower() in videos_movies_ids: if movie_provider_source.lower() in videos_movies_ids:
if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]: if (
movie_provider_id.lower()
in videos_movies_ids[
movie_provider_source.lower()
]
):
movie_found = True movie_found = True
break break
if movie_found: if movie_found:
jellyfin_video_id = jellyfin_video["Id"] jellyfin_video_id = jellyfin_video["Id"]
msg = f"{jellyfin_video['Name']} as watched for {user} in {library} for Jellyfin" msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin"
if not dryrun: if not dryrun:
logger(f"Marking {msg}", 0) logger(f"Marking {msg}", 0)
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post") await self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
"post",
session,
)
else: else:
logger(f"Dryrun {msg}", 0) logger(f"Dryrun {msg}", 0)
else:
logger(
f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}",
1,
)
# TV Shows # TV Shows
if library_type == "Episode": if videos_shows_ids and videos_episodes_ids:
videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3) jellyfin_search = await self.query(
f"/Users/{user_id}/Items"
jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,Path", "get") + f"?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}"
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,Path",
"get",
session,
)
jellyfin_shows = [x for x in jellyfin_search["Items"]] jellyfin_shows = [x for x in jellyfin_search["Items"]]
for jellyfin_show in jellyfin_shows: for jellyfin_show in jellyfin_shows:
show_found = False show_found = False
if jellyfin_show["Name"] == "The 13 Ghosts of Scooby-Doo":
print(jellyfin_show)
if "Path" in jellyfin_show: if "Path" in jellyfin_show:
if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]: if (
jellyfin_show["Path"].split("/")[-1]
in videos_shows_ids["locations"]
):
show_found = True show_found = True
if not show_found: if not show_found:
for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items(): for show_provider_source, show_provider_id in jellyfin_show[
"ProviderIds"
].items():
if show_provider_source.lower() in videos_shows_ids: if show_provider_source.lower() in videos_shows_ids:
if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]: if (
show_provider_id.lower()
in videos_shows_ids[
show_provider_source.lower()
]
):
show_found = True show_found = True
break break
if show_found: if show_found:
logger(
f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}",
1,
)
jellyfin_show_id = jellyfin_show["Id"] jellyfin_show_id = jellyfin_show["Id"]
jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") jellyfin_episodes = await self.query(
f"/Shows/{jellyfin_show_id}/Episodes"
+ f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
session,
)
for jellyfin_episode in jellyfin_episodes["Items"]: for jellyfin_episode in jellyfin_episodes["Items"]:
episode_found = False episode_found = False
if "MediaSources" in jellyfin_episode: if "MediaSources" in jellyfin_episode:
for episode_location in jellyfin_episode["MediaSources"]: for episode_location in jellyfin_episode[
if episode_location["Path"].split("/")[-1] in videos_episode_ids["locations"]: "MediaSources"
]:
if (
episode_location["Path"].split("/")[-1]
in videos_episodes_ids["locations"]
):
episode_found = True episode_found = True
break break
if not episode_found: if not episode_found:
for episode_provider_source, episode_provider_id in jellyfin_episode["ProviderIds"].items(): for (
if episode_provider_source.lower() in videos_episode_ids: episode_provider_source,
if episode_provider_id.lower() in videos_episode_ids[episode_provider_source.lower()]: episode_provider_id,
) in jellyfin_episode["ProviderIds"].items():
if (
episode_provider_source.lower()
in videos_episodes_ids
):
if (
episode_provider_id.lower()
in videos_episodes_ids[
episode_provider_source.lower()
]
):
episode_found = True episode_found = True
break break
if episode_found: if episode_found:
jellyfin_episode_id = jellyfin_episode["Id"] jellyfin_episode_id = jellyfin_episode["Id"]
msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']} as watched for {user} in {library} for Jellyfin" msg = (
f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']}"
+ f" as watched for {user_name} in {library} for Jellyfin"
)
if not dryrun: if not dryrun:
logger(f"Marked {msg}", 0) logger(f"Marked {msg}", 0)
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post") await self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
"post",
session,
)
else: else:
logger(f"Dryrun {msg}", 0) logger(f"Dryrun {msg}", 0)
else:
logger(
f"Jellyfin: Skipping episode {jellyfin_episode['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}",
3,
)
if (
not videos_movies_ids
and not videos_shows_ids
and not videos_episodes_ids
):
logger(
f"Jellyfin: No videos to mark as watched for {user_name} in library {library}",
1,
)
except Exception as e: except Exception as e:
logger(f"Jellyfin: Error updating watched for {user} in library {library}", 2) logger(
f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}",
2,
)
raise Exception(e) raise Exception(e)
async def update_watched(
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
):
try: try:
args = [] tasks = []
async with aiohttp.ClientSession() as session:
for user, libraries in watched_list.items(): for user, libraries in watched_list.items():
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
user_other = None user_other = None
user_name = None
if user_mapping: if user_mapping:
if user in user_mapping.keys(): if user in user_mapping.keys():
user_other = user_mapping[user] user_other = user_mapping[user]
@@ -272,16 +643,21 @@ class Jellyfin():
for key in self.users.keys(): for key in self.users.keys():
if user.lower() == key.lower(): if user.lower() == key.lower():
user_id = self.users[key] user_id = self.users[key]
user_name = key
break break
elif user_other and user_other.lower() == key.lower(): elif user_other and user_other.lower() == key.lower():
user_id = self.users[key] user_id = self.users[key]
user_name = key
break break
if not user_id: if not user_id:
logger(f"{user} {user_other} not found in Jellyfin", 2) logger(f"{user} {user_other} not found in Jellyfin", 2)
continue continue
jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] jellyfin_libraries = await self.query(
f"/Users/{user_id}/Views", "get", session
)
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
for library, videos in libraries.items(): for library, videos in libraries.items():
library_other = None library_other = None
@@ -291,17 +667,29 @@ class Jellyfin():
elif library in library_mapping.values(): elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library) library_other = search_mapping(library_mapping, library)
if library.lower() not in [
if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]: x["Name"].lower() for x in jellyfin_libraries
]:
if library_other: if library_other:
if library_other.lower() in [x["Name"].lower() for x in jellyfin_libraries]: if library_other.lower() in [
logger(f"Jellyfin: Library {library} not found, but {library_other} found, using {library_other}", 1) x["Name"].lower() for x in jellyfin_libraries
]:
logger(
f"Jellyfin: Library {library} not found, but {library_other} found, using {library_other}",
1,
)
library = library_other library = library_other
else: else:
logger(f"Jellyfin: Library {library} or {library_other} not found in library list", 2) logger(
f"Jellyfin: Library {library} or {library_other} not found in library list",
1,
)
continue continue
else: else:
logger(f"Jellyfin: Library {library} not found in library list", 2) logger(
f"Jellyfin: Library {library} not found in library list",
1,
)
continue continue
library_id = None library_id = None
@@ -311,9 +699,12 @@ class Jellyfin():
continue continue
if library_id: if library_id:
args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) task = self.update_user_watched(
user_name, user_id, library, library_id, videos, dryrun
)
tasks.append(task)
future_thread_executor(args) await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e: except Exception as e:
logger(f"Jellyfin: Error updating watched", 2) logger(f"Jellyfin: Error updating watched, {e}", 2)
raise Exception(e) raise Exception(e)

View File

@@ -1,214 +1,31 @@
import copy, os, traceback, json import os, traceback, json, asyncio
from dotenv import load_dotenv from dotenv import load_dotenv
from time import sleep from time import sleep, perf_counter
from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor from src.functions import (
logger,
str_to_bool,
search_mapping,
cleanup_watched,
setup_black_white_lists,
)
from src.plex import Plex from src.plex import Plex
from src.jellyfin import Jellyfin from src.jellyfin import Jellyfin
load_dotenv(override=True) load_dotenv(override=True)
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from plex_watched that are in jellyfin_watched def setup_users(
for user_1 in watched_list_1: server_1, server_2, blacklist_users, whitelist_users, user_mapping=None
user_other = None ):
if user_mapping:
user_other = search_mapping(user_mapping, user_1)
if user_1 in modified_watched_list_1:
if user_1 in watched_list_2:
user_2 = user_1
elif user_other in watched_list_2:
user_2 = user_other
else:
logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
continue
for library_1 in watched_list_1[user_1]:
library_other = None
if library_mapping:
library_other = search_mapping(library_mapping, library_1)
if library_1 in modified_watched_list_1[user_1]:
if library_1 in watched_list_2[user_2]:
library_2 = library_1
elif library_other in watched_list_2[user_2]:
library_2 = library_other
else:
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
continue
# Movies
if isinstance(watched_list_1[user_1][library_1], list):
_, _, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2], 2)
for movie in watched_list_1[user_1][library_1]:
movie_found = False
for movie_key, movie_value in movie.items():
if movie_key == "locations":
for location in movie_value:
if location in movies_watched_list_2_keys_dict["locations"]:
movie_found = True
break
else:
if movie_key in movies_watched_list_2_keys_dict.keys():
if movie_value in movies_watched_list_2_keys_dict[movie_key]:
movie_found = True
if movie_found:
logger(f"Removing {movie} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(movie)
break
# TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict):
# Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
show_watched_list_2_keys_dict, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 3)
for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1)
for season in watched_list_1[user_1][library_1][show_key_1]:
for episode in watched_list_1[user_1][library_1][show_key_1][season]:
episode_found = False
for episode_key, episode_value in episode.items():
# If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
if episode_key == "locations":
for location in episode_value:
if location in episode_watched_list_2_keys_dict["locations"]:
episode_found = True
break
else:
if episode_key in episode_watched_list_2_keys_dict.keys():
if episode_value in episode_watched_list_2_keys_dict[episode_key]:
episode_found = True
if episode_found:
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3)
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
break
# Remove empty seasons
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
logger(f"Removing {season} from {library_1} because it is empty", 3)
del modified_watched_list_1[user_1][library_1][show_key_1][season]
# If the show is empty, remove the show
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
if show_key_1 in modified_watched_list_1[user_1][library_1]:
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1][show_key_1]
for user_1 in watched_list_1:
for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]:
# If library is empty then remove it
if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1]
if user_1 in modified_watched_list_1:
# If user is empty delete user
if len(modified_watched_list_1[user_1]) == 0:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
del modified_watched_list_1[user_1]
return modified_watched_list_1
def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blacklist_library_type: str, whitelist_library_type: str, blacklist_users: str, whitelist_users: str, library_mapping=None, user_mapping=None):
if blacklist_library:
if len(blacklist_library) > 0:
blacklist_library = blacklist_library.split(",")
blacklist_library = [x.strip() for x in blacklist_library]
if library_mapping:
temp_library = []
for library in blacklist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
blacklist_library = blacklist_library + temp_library
else:
blacklist_library = []
logger(f"Blacklist Library: {blacklist_library}", 1)
if whitelist_library:
if len(whitelist_library) > 0:
whitelist_library = whitelist_library.split(",")
whitelist_library = [x.strip() for x in whitelist_library]
if library_mapping:
temp_library = []
for library in whitelist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
whitelist_library = whitelist_library + temp_library
else:
whitelist_library = []
logger(f"Whitelist Library: {whitelist_library}", 1)
if blacklist_library_type:
if len(blacklist_library_type) > 0:
blacklist_library_type = blacklist_library_type.split(",")
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
else:
blacklist_library_type = []
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
if whitelist_library_type:
if len(whitelist_library_type) > 0:
whitelist_library_type = whitelist_library_type.split(",")
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
else:
whitelist_library_type = []
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
if blacklist_users:
if len(blacklist_users) > 0:
blacklist_users = blacklist_users.split(",")
blacklist_users = [x.lower().strip() for x in blacklist_users]
if user_mapping:
temp_users = []
for user in blacklist_users:
user_other = search_mapping(user_mapping, user)
if user_other:
temp_users.append(user_other)
blacklist_users = blacklist_users + temp_users
else:
blacklist_users = []
logger(f"Blacklist Users: {blacklist_users}", 1)
if whitelist_users:
if len(whitelist_users) > 0:
whitelist_users = whitelist_users.split(",")
whitelist_users = [x.lower().strip() for x in whitelist_users]
if user_mapping:
temp_users = []
for user in whitelist_users:
user_other = search_mapping(user_mapping, user)
if user_other:
temp_users.append(user_other)
whitelist_users = whitelist_users + temp_users
else:
whitelist_users = []
else:
whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1)
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
# generate list of users from server 1 and server 2 # generate list of users from server 1 and server 2
server_1_type = server_1[0] server_1_type = server_1[0]
server_1_connection = server_1[1] server_1_connection = server_1[1]
server_2_type = server_2[0] server_2_type = server_2[0]
server_2_connection = server_2[1] server_2_connection = server_2[1]
logger(f"Server 1: {server_1_type} {server_1_connection}", 0)
logger(f"Server 2: {server_2_type} {server_2_connection}", 0)
server_1_users = [] server_1_users = []
if server_1_type == "plex": if server_1_type == "plex":
@@ -222,7 +39,6 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi
elif server_2_type == "jellyfin": elif server_2_type == "jellyfin":
server_2_users = [key.lower() for key in server_2_connection.users.keys()] server_2_users = [key.lower() for key in server_2_connection.users.keys()]
# combined list of overlapping users from plex and jellyfin # combined list of overlapping users from plex and jellyfin
users = {} users = {}
@@ -264,36 +80,53 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi
if server_1_type == "plex": if server_1_type == "plex":
output_server_1_users = [] output_server_1_users = []
for plex_user in server_1_connection.users: for plex_user in server_1_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): if (
plex_user.title.lower() in users_filtered.keys()
or plex_user.title.lower() in users_filtered.values()
):
output_server_1_users.append(plex_user) output_server_1_users.append(plex_user)
elif server_1_type == "jellyfin": elif server_1_type == "jellyfin":
output_server_1_users = {} output_server_1_users = {}
for jellyfin_user, jellyfin_id in server_1_connection.users.items(): for jellyfin_user, jellyfin_id in server_1_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): if (
jellyfin_user.lower() in users_filtered.keys()
or jellyfin_user.lower() in users_filtered.values()
):
output_server_1_users[jellyfin_user] = jellyfin_id output_server_1_users[jellyfin_user] = jellyfin_id
if server_2_type == "plex": if server_2_type == "plex":
output_server_2_users = [] output_server_2_users = []
for plex_user in server_2_connection.users: for plex_user in server_2_connection.users:
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): if (
plex_user.title.lower() in users_filtered.keys()
or plex_user.title.lower() in users_filtered.values()
):
output_server_2_users.append(plex_user) output_server_2_users.append(plex_user)
elif server_2_type == "jellyfin": elif server_2_type == "jellyfin":
output_server_2_users = {} output_server_2_users = {}
for jellyfin_user, jellyfin_id in server_2_connection.users.items(): for jellyfin_user, jellyfin_id in server_2_connection.users.items():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): if (
jellyfin_user.lower() in users_filtered.keys()
or jellyfin_user.lower() in users_filtered.values()
):
output_server_2_users[jellyfin_user] = jellyfin_id output_server_2_users[jellyfin_user] = jellyfin_id
if len(output_server_1_users) == 0: if len(output_server_1_users) == 0:
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}") raise Exception(
f"No users found for server 1, users found {users} filtered users {users_filtered}"
)
if len(output_server_2_users) == 0: if len(output_server_2_users) == 0:
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}") raise Exception(
f"No users found for server 2, users found {users} filtered users {users_filtered}"
)
logger(f"Server 1 users: {output_server_1_users}", 1) logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1) logger(f"Server 2 users: {output_server_2_users}", 1)
return output_server_1_users, output_server_2_users return output_server_1_users, output_server_2_users
def generate_server_connections(): def generate_server_connections():
servers = [] servers = []
@@ -302,27 +135,58 @@ def generate_server_connections():
plex_username = os.getenv("PLEX_USERNAME", None) plex_username = os.getenv("PLEX_USERNAME", None)
plex_password = os.getenv("PLEX_PASSWORD", None) plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None) plex_servername = os.getenv("PLEX_SERVERNAME", None)
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
if plex_baseurl and plex_token: if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",") plex_baseurl = plex_baseurl.split(",")
plex_token = plex_token.split(",") plex_token = plex_token.split(",")
if len(plex_baseurl) != len(plex_token): if len(plex_baseurl) != len(plex_token):
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") raise Exception(
"PLEX_BASEURL and PLEX_TOKEN must have the same number of entries"
)
for i, url in enumerate(plex_baseurl): for i, url in enumerate(plex_baseurl):
servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None))) servers.append(
(
"plex",
Plex(
baseurl=url.strip(),
token=plex_token[i].strip(),
username=None,
password=None,
servername=None,
ssl_bypass=ssl_bypass,
),
)
)
if plex_username and plex_password and plex_servername: if plex_username and plex_password and plex_servername:
plex_username = plex_username.split(",") plex_username = plex_username.split(",")
plex_password = plex_password.split(",") plex_password = plex_password.split(",")
plex_servername = plex_servername.split(",") plex_servername = plex_servername.split(",")
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername): if len(plex_username) != len(plex_password) or len(plex_username) != len(
raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries") plex_servername
):
raise Exception(
"PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries"
)
for i, username in enumerate(plex_username): for i, username in enumerate(plex_username):
servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip()))) 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,
),
)
)
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None) jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None) jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
@@ -332,13 +196,74 @@ def generate_server_connections():
jellyfin_token = jellyfin_token.split(",") jellyfin_token = jellyfin_token.split(",")
if len(jellyfin_baseurl) != len(jellyfin_token): if len(jellyfin_baseurl) != len(jellyfin_token):
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries") raise Exception(
"JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries"
)
for i, baseurl in enumerate(jellyfin_baseurl): for i, baseurl in enumerate(jellyfin_baseurl):
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()))) baseurl = baseurl.strip()
if baseurl[-1] == "/":
baseurl = baseurl[:-1]
servers.append(
(
"jellyfin",
Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip()),
)
)
return servers return servers
def get_server_watched(
server_connection: list,
users: dict,
blacklist_library: list,
whitelist_library: list,
blacklist_library_type: list,
whitelist_library_type: list,
library_mapping: dict,
):
if server_connection[0] == "plex":
return server_connection[1].get_watched(
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
elif server_connection[0] == "jellyfin":
return asyncio.run(
server_connection[1].get_watched(
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
)
def update_server_watched(
server_connection: list,
server_watched_filtered: dict,
user_mapping: dict,
library_mapping: dict,
dryrun: bool,
):
if server_connection[0] == "plex":
server_connection[1].update_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun
)
elif server_connection[0] == "jellyfin":
asyncio.run(
server_connection[1].update_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun
)
)
def main_loop(): def main_loop():
logfile = os.getenv("LOGFILE", "log.log") logfile = os.getenv("LOGFILE", "log.log")
# Delete logfile if it exists # Delete logfile if it exists
@@ -367,7 +292,23 @@ def main_loop():
blacklist_users = os.getenv("BLACKLIST_USERS", None) blacklist_users = os.getenv("BLACKLIST_USERS", None)
whitelist_users = os.getenv("WHITELIST_USERS", None) whitelist_users = os.getenv("WHITELIST_USERS", None)
blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users, library_mapping, user_mapping) (
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
) = setup_black_white_lists(
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
library_mapping,
user_mapping,
)
# Create server connections # Create server connections
logger("Creating server connections", 1) logger("Creating server connections", 1)
@@ -380,50 +321,88 @@ def main_loop():
# Start server_2 at the next server in the list # Start server_2 at the next server in the list
for server_2 in servers[servers.index(server_1) + 1 :]: for server_2 in servers[servers.index(server_1) + 1 :]:
server_1_connection = server_1[1]
server_2_connection = server_2[1]
# Create users list # Create users list
logger("Creating users list", 1) logger("Creating users list", 1)
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) server_1_users, server_2_users = setup_users(
server_1, server_2, blacklist_users, whitelist_users, user_mapping
)
logger("Creating watched lists", 1) logger("Creating watched lists", 1)
args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping] server_1_watched = get_server_watched(
, [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]] server_1,
server_1_users,
results = future_thread_executor(args) blacklist_library,
server_1_watched = results[0] whitelist_library,
server_2_watched = results[1] blacklist_library_type,
whitelist_library_type,
library_mapping,
)
logger("Finished creating watched list server 1", 1)
server_2_watched = get_server_watched(
server_2,
server_2_users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
logger("Finished creating watched list server 2", 1)
logger(f"Server 1 watched: {server_1_watched}", 3) logger(f"Server 1 watched: {server_1_watched}", 3)
logger(f"Server 2 watched: {server_2_watched}", 3) logger(f"Server 2 watched: {server_2_watched}", 3)
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
server_1_watched_filtered = copy.deepcopy(server_1_watched)
server_2_watched_filtered = copy.deepcopy(server_2_watched)
logger("Cleaning Server 1 Watched", 1) logger("Cleaning Server 1 Watched", 1)
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping) server_1_watched_filtered = cleanup_watched(
server_1_watched, server_2_watched, user_mapping, library_mapping
)
logger("Cleaning Server 2 Watched", 1) logger("Cleaning Server 2 Watched", 1)
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping) server_2_watched_filtered = cleanup_watched(
server_2_watched, server_1_watched, user_mapping, library_mapping
)
logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1) logger(
logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1) f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}",
1,
)
logger(
f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}",
1,
)
args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun] update_server_watched(
, [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]] server_1,
server_2_watched_filtered,
user_mapping,
library_mapping,
dryrun,
)
update_server_watched(
server_2,
server_1_watched_filtered,
user_mapping,
library_mapping,
dryrun,
)
future_thread_executor(args)
def main(): def main():
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
times = []
while(True): while True:
try: try:
start = perf_counter()
main_loop() main_loop()
end = perf_counter()
times.append(end - start)
if len(times) > 0:
logger(f"Average time: {sum(times) / len(times)}", 0)
logger(f"Looping in {sleep_duration}") logger(f"Looping in {sleep_duration}")
sleep(sleep_duration) sleep(sleep_duration)
except Exception as error: except Exception as error:
if isinstance(error, list): if isinstance(error, list):
for message in error: for message in error:
@@ -431,8 +410,8 @@ def main():
else: else:
logger(error, log_type=2) logger(error, log_type=2)
logger(traceback.format_exc(), 2) logger(traceback.format_exc(), 2)
logger(f"Retrying in {sleep_duration}", log_type=0) logger(f"Retrying in {sleep_duration}", log_type=0)
sleep(sleep_duration) sleep(sleep_duration)

View File

@@ -1,28 +1,289 @@
import re import re, requests
from urllib3.poolmanager import PoolManager
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor from src.functions import (
logger,
search_mapping,
check_skip_logic,
generate_library_guids_dict,
future_thread_executor,
)
# Bypass hostname validation for ssl. Taken from https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
class HostNameIgnoringAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=..., **pool_kwargs):
self.poolmanager = PoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
assert_hostname=False,
**pool_kwargs,
)
def get_user_library_watched(user, user_plex, library):
try:
user_name = user.title.lower()
user_watched = {}
user_watched[user_name] = {}
logger(
f"Plex: Generating watched for {user_name} in library {library.title}",
0,
)
library_videos = user_plex.library.section(library.title)
if library.type == "movie":
user_watched[user_name][library.title] = []
for video in library_videos.search(unwatched=False):
logger(f"Plex: Adding {video.title} to {user_name} watched list", 3)
logger(f"Plex: {video.title} {video.guids} {video.locations}", 3)
movie_guids = {}
for guid in video.guids:
# Extract source and id from guid.id
m = re.match(r"(.*)://(.*)", guid.id)
guid_source, guid_id = m.group(1).lower(), m.group(2)
movie_guids[guid_source] = guid_id
movie_guids["title"] = video.title
movie_guids["locations"] = tuple(
[x.split("/")[-1] for x in video.locations]
)
user_watched[user_name][library.title].append(movie_guids)
logger(f"Plex: Added {movie_guids} to {user_name} watched list", 3)
elif library.type == "show":
user_watched[user_name][library.title] = {}
for show in library_videos.search(unwatched=False):
logger(f"Plex: Adding {show.title} to {user_name} watched list", 3)
show_guids = {}
for show_guid in show.guids:
# Extract source and id from guid.id
m = re.match(r"(.*)://(.*)", show_guid.id)
show_guid_source, show_guid_id = m.group(1).lower(), m.group(2)
show_guids[show_guid_source] = show_guid_id
show_guids["title"] = show.title
show_guids["locations"] = tuple(
[x.split("/")[-1] for x in show.locations]
)
show_guids = frozenset(show_guids.items())
# Get all watched episodes for show
episode_guids = {}
for episode in show.watched():
if episode.viewCount > 0:
episode_guids_temp = {}
for guid in episode.guids:
# Extract after :// from guid.id
m = re.match(r"(.*)://(.*)", guid.id)
guid_source, guid_id = m.group(1).lower(), m.group(2)
episode_guids_temp[guid_source] = guid_id
episode_guids_temp["locations"] = tuple(
[x.split("/")[-1] for x in episode.locations]
)
if episode.parentTitle not in episode_guids:
episode_guids[episode.parentTitle] = []
episode_guids[episode.parentTitle].append(episode_guids_temp)
if episode_guids:
# append show, season, episode
if show_guids not in user_watched[user_name][library.title]:
user_watched[user_name][library.title][show_guids] = {}
user_watched[user_name][library.title][show_guids] = episode_guids
logger(
f"Plex: Added {episode_guids} to {user_name} {show_guids} watched list",
3,
)
logger(f"Plex: Got watched for {user_name} in library {library.title}", 1)
if library.title in user_watched[user_name]:
logger(f"Plex: {user_watched[user_name][library.title]}", 3)
return user_watched
except Exception as e:
logger(
f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}",
2,
)
raise Exception(e)
def update_user_watched(user, user_plex, library, videos, dryrun):
try:
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
(
videos_shows_ids,
videos_episodes_ids,
videos_movies_ids,
) = generate_library_guids_dict(videos)
logger(
f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
1,
)
library_videos = user_plex.library.section(library)
if videos_movies_ids:
for movies_search in library_videos.search(unwatched=True):
movie_found = False
for movie_location in movies_search.locations:
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
movie_found = True
break
if not movie_found:
for movie_guid in movies_search.guids:
movie_guid_source = (
re.search(r"(.*)://", movie_guid.id).group(1).lower()
)
movie_guid_id = re.search(r"://(.*)", movie_guid.id).group(1)
# If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
if movie_guid_source in videos_movies_ids.keys():
if movie_guid_id in videos_movies_ids[movie_guid_source]:
movie_found = True
break
if movie_found:
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
if not dryrun:
logger(f"Marked {msg}", 0)
movies_search.markWatched()
else:
logger(f"Dryrun {msg}", 0)
else:
logger(
f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}",
1,
)
if videos_shows_ids and videos_episodes_ids:
for show_search in library_videos.search(unwatched=True):
show_found = False
for show_location in show_search.locations:
if show_location.split("/")[-1] in videos_shows_ids["locations"]:
show_found = True
break
if not show_found:
for show_guid in show_search.guids:
show_guid_source = (
re.search(r"(.*)://", show_guid.id).group(1).lower()
)
show_guid_id = re.search(r"://(.*)", show_guid.id).group(1)
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
if show_guid_source in videos_shows_ids.keys():
if show_guid_id in videos_shows_ids[show_guid_source]:
show_found = True
break
if show_found:
for episode_search in show_search.episodes():
episode_found = False
for episode_location in episode_search.locations:
if (
episode_location.split("/")[-1]
in videos_episodes_ids["locations"]
):
episode_found = True
break
if not episode_found:
for episode_guid in episode_search.guids:
episode_guid_source = (
re.search(r"(.*)://", episode_guid.id)
.group(1)
.lower()
)
episode_guid_id = re.search(
r"://(.*)", episode_guid.id
).group(1)
# If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
if episode_guid_source in videos_episodes_ids.keys():
if (
episode_guid_id
in videos_episodes_ids[episode_guid_source]
):
episode_found = True
break
if episode_found:
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
if not dryrun:
logger(f"Marked {msg}", 0)
episode_search.markWatched()
else:
logger(f"Dryrun {msg}", 0)
else:
logger(
f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}",
3,
)
else:
logger(
f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}",
3,
)
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids:
logger(
f"Jellyfin: No videos to mark as watched for {user.title} in library {library}",
1,
)
except Exception as e:
logger(
f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}",
2,
)
raise Exception(e)
# class plex accept base url and token and username and password but default with none # class plex accept base url and token and username and password but default with none
class Plex: class Plex:
def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None): def __init__(
self,
baseurl=None,
token=None,
username=None,
password=None,
servername=None,
ssl_bypass=False,
):
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
self.username = username self.username = username
self.password = password self.password = password
self.servername = servername self.servername = servername
self.plex = self.login() self.ssl_bypass = ssl_bypass
self.plex = self.login(self.baseurl, self.token, ssl_bypass)
self.admin_user = self.plex.myPlexAccount() self.admin_user = self.plex.myPlexAccount()
self.users = self.get_users() self.users = self.get_users()
def login(self): def login(self, baseurl, token, ssl_bypass=False):
try: try:
if self.baseurl and self.token: if baseurl and token:
# Login via token # Login via token
plex = PlexServer(self.baseurl, self.token) if ssl_bypass:
session = requests.Session()
# By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
session.mount("https://", HostNameIgnoringAdapter())
plex = PlexServer(baseurl, token, session=session)
else:
plex = PlexServer(baseurl, token)
elif self.username and self.password and self.servername: elif self.username and self.password and self.servername:
# Login via plex account # Login via plex account
account = MyPlexAccount(self.username, self.password) account = MyPlexAccount(self.username, self.password)
@@ -39,7 +300,6 @@ class Plex:
logger(f"Plex: Failed to login, Error: {e}", 2) logger(f"Plex: Failed to login, Error: {e}", 2)
raise Exception(e) raise Exception(e)
def get_users(self): def get_users(self):
try: try:
users = self.plex.myPlexAccount().users() users = self.plex.myPlexAccount().users()
@@ -52,76 +312,15 @@ class Plex:
logger(f"Plex: Failed to get users, Error: {e}", 2) logger(f"Plex: Failed to get users, Error: {e}", 2)
raise Exception(e) raise Exception(e)
def get_user_watched(self, user, user_plex, library): def get_watched(
try: self,
user_name = user.title.lower() users,
user_watched = {} blacklist_library,
user_watched[user_name] = {} whitelist_library,
blacklist_library_type,
logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0) whitelist_library_type,
library_mapping,
if library.type == "movie": ):
user_watched[user_name][library.title] = []
library_videos = user_plex.library.section(library.title)
for video in library_videos.search(unwatched=False):
movie_guids = {}
for guid in video.guids:
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
guid_id = re.search(r'://(.*)', guid.id).group(1)
movie_guids[guid_source] = guid_id
movie_guids["title"] = video.title
movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations])
user_watched[user_name][library.title].append(movie_guids)
elif library.type == "show":
user_watched[user_name][library.title] = {}
library_videos = user_plex.library.section(library.title)
for show in library_videos.search(unwatched=False):
show_guids = {}
for show_guid in show.guids:
# Extract after :// from guid.id
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
show_guids[show_guid_source] = show_guid_id
show_guids["title"] = show.title
show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations])
show_guids = frozenset(show_guids.items())
for season in show.seasons():
episode_guids = []
for episode in season.episodes():
if episode.viewCount > 0:
episode_guids_temp = {}
for guid in episode.guids:
# Extract after :// from guid.id
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
guid_id = re.search(r'://(.*)', guid.id).group(1)
episode_guids_temp[guid_source] = guid_id
episode_guids_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations])
episode_guids.append(episode_guids_temp)
if episode_guids:
# append show, season, episode
if show_guids not in user_watched[user_name][library.title]:
user_watched[user_name][library.title][show_guids] = {}
if season.title not in user_watched[user_name][library.title][show_guids]:
user_watched[user_name][library.title][show_guids][season.title] = {}
user_watched[user_name][library.title][show_guids][season.title] = episode_guids
return user_watched
except Exception as e:
logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2)
raise Exception(e)
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
try: try:
# Get all libraries # Get all libraries
users_watched = {} users_watched = {}
@@ -131,7 +330,11 @@ class Plex:
if self.admin_user == user: if self.admin_user == user:
user_plex = self.plex user_plex = self.plex
else: else:
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) user_plex = self.login(
self.plex._baseurl,
user.get_token(self.plex.machineIdentifier),
self.ssl_bypass,
)
libraries = user_plex.library.sections() libraries = user_plex.library.sections()
@@ -139,13 +342,23 @@ class Plex:
library_title = library.title library_title = library.title
library_type = library.type library_type = library.type
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
if skip_reason: if skip_reason:
logger(f"Plex: Skipping library {library_title} {skip_reason}", 1) logger(
f"Plex: Skipping library {library_title} {skip_reason}", 1
)
continue continue
args.append([self.get_user_watched, user, user_plex, library]) args.append([get_user_library_watched, user, user_plex, library])
for user_watched in future_thread_executor(args): for user_watched in future_thread_executor(args):
for user, user_watched_temp in user_watched.items(): for user, user_watched_temp in user_watched.items():
@@ -158,97 +371,9 @@ class Plex:
logger(f"Plex: Failed to get watched, Error: {e}", 2) logger(f"Plex: Failed to get watched, Error: {e}", 2)
raise Exception(e) raise Exception(e)
def update_watched(
def update_user_watched (self, user, user_plex, library, videos, dryrun): self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
try: ):
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
library_videos = user_plex.library.section(library)
if library_videos.type == "movie":
_, _, videos_movies_ids = generate_library_guids_dict(videos, 2)
for movies_search in library_videos.search(unwatched=True):
movie_found = False
for movie_location in movies_search.locations:
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
movie_found = True
break
if not movie_found:
for movie_guid in movies_search.guids:
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
# If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
if movie_guid_source in videos_movies_ids.keys():
if movie_guid_id in videos_movies_ids[movie_guid_source]:
movie_found = True
break
if movie_found:
if movies_search.viewCount == 0:
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
if not dryrun:
logger(f"Marked {msg}", 0)
movies_search.markWatched()
else:
logger(f"Dryrun {msg}", 0)
elif library_videos.type == "show":
videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3)
for show_search in library_videos.search(unwatched=True):
show_found = False
for show_location in show_search.locations:
if show_location.split("/")[-1] in videos_shows_ids["locations"]:
show_found = True
break
if not show_found:
for show_guid in show_search.guids:
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
if show_guid_source in videos_shows_ids.keys():
if show_guid_id in videos_shows_ids[show_guid_source]:
show_found = True
break
if show_found:
for episode_search in show_search.episodes():
episode_found = False
for episode_location in episode_search.locations:
if episode_location.split("/")[-1] in videos_episode_ids["locations"]:
episode_found = True
break
if not episode_found:
for episode_guid in episode_search.guids:
episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower()
episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1)
# If episode provider source and episode provider id are in videos_episode_ids exactly, then the episode is in the list
if episode_guid_source in videos_episode_ids.keys():
if episode_guid_id in videos_episode_ids[episode_guid_source]:
episode_found = True
break
if episode_found:
if episode_search.viewCount == 0:
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
if not dryrun:
logger(f"Marked {msg}", 0)
episode_search.markWatched()
else:
logger(f"Dryrun {msg}", 0)
except Exception as e:
logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2)
raise Exception(e)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try: try:
args = [] args = []
@@ -272,7 +397,9 @@ class Plex:
if self.admin_user == user: if self.admin_user == user:
user_plex = self.plex user_plex = self.plex
else: else:
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) user_plex = PlexServer(
self.plex._baseurl, user.get_token(self.plex.machineIdentifier)
)
for library, videos in libraries.items(): for library, videos in libraries.items():
library_other = None library_other = None
@@ -286,18 +413,37 @@ class Plex:
library_list = user_plex.library.sections() library_list = user_plex.library.sections()
if library.lower() not in [x.title.lower() for x in library_list]: if library.lower() not in [x.title.lower() for x in library_list]:
if library_other: if library_other:
if library_other.lower() in [x.title.lower() for x in library_list]: if library_other.lower() in [
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) x.title.lower() for x in library_list
]:
logger(
f"Plex: Library {library} not found, but {library_other} found, using {library_other}",
1,
)
library = library_other library = library_other
else: else:
logger(f"Plex: Library {library} or {library_other} not found in library list", 2) logger(
f"Plex: Library {library} or {library_other} not found in library list",
1,
)
continue continue
else: else:
logger(f"Plex: Library {library} not found in library list", 2) logger(
f"Plex: Library {library} not found in library list",
1,
)
continue continue
args.append(
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) [
update_user_watched,
user,
user_plex,
library,
videos,
dryrun,
]
)
future_thread_executor(args) future_thread_executor(args)
except Exception as e: except Exception as e:

78
test/test_main.py Normal file
View File

@@ -0,0 +1,78 @@
import sys
import os
# getting the name of the directory
# where the this file is present.
current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name
# where the current directory is present.
parent = os.path.dirname(current)
# adding the parent directory to
# the sys.path.
sys.path.append(parent)
from src.main import setup_black_white_lists
def test_setup_black_white_lists():
# Simple
blacklist_library = "library1, library2"
whitelist_library = "library1, library2"
blacklist_library_type = "library_type1, library_type2"
whitelist_library_type = "library_type1, library_type2"
blacklist_users = "user1, user2"
whitelist_users = "user1, user2"
(
results_blacklist_library,
return_whitelist_library,
return_blacklist_library_type,
return_whitelist_library_type,
return_blacklist_users,
return_whitelist_users,
) = setup_black_white_lists(
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
)
assert results_blacklist_library == ["library1", "library2"]
assert return_whitelist_library == ["library1", "library2"]
assert return_blacklist_library_type == ["library_type1", "library_type2"]
assert return_whitelist_library_type == ["library_type1", "library_type2"]
assert return_blacklist_users == ["user1", "user2"]
assert return_whitelist_users == ["user1", "user2"]
# Library Mapping and user mapping
library_mapping = {"library1": "library3"}
user_mapping = {"user1": "user3"}
(
results_blacklist_library,
return_whitelist_library,
return_blacklist_library_type,
return_whitelist_library_type,
return_blacklist_users,
return_whitelist_users,
) = setup_black_white_lists(
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
library_mapping,
user_mapping,
)
assert results_blacklist_library == ["library1", "library2", "library3"]
assert return_whitelist_library == ["library1", "library2", "library3"]
assert return_blacklist_library_type == ["library_type1", "library_type2"]
assert return_whitelist_library_type == ["library_type1", "library_type2"]
assert return_blacklist_users == ["user1", "user2", "user3"]
assert return_whitelist_users == ["user1", "user2", "user3"]

View File

@@ -1,47 +0,0 @@
import sys
import os
# getting the name of the directory
# where the this file is present.
current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name
# where the current directory is present.
parent = os.path.dirname(current)
# adding the parent directory to
# the sys.path.
sys.path.append(parent)
from src.main import setup_black_white_lists
def test_setup_black_white_lists():
# Simple
blacklist_library = 'library1, library2'
whitelist_library = 'library1, library2'
blacklist_library_type = 'library_type1, library_type2'
whitelist_library_type = 'library_type1, library_type2'
blacklist_users = 'user1, user2'
whitelist_users = 'user1, user2'
results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users)
assert results_blacklist_library == ['library1', 'library2']
assert return_whitelist_library == ['library1', 'library2']
assert return_blacklist_library_type == ['library_type1', 'library_type2']
assert return_whitelist_library_type == ['library_type1', 'library_type2']
assert return_blacklist_users == ['user1', 'user2']
assert return_whitelist_users == ['user1', 'user2']
# Library Mapping and user mapping
library_mapping = { "library1": "library3" }
user_mapping = { "user1": "user3" }
results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users, library_mapping, user_mapping)
assert results_blacklist_library == ['library1', 'library2', 'library3']
assert return_whitelist_library == ['library1', 'library2', 'library3']
assert return_blacklist_library_type == ['library_type1', 'library_type2']
assert return_whitelist_library_type == ['library_type1', 'library_type2']
assert return_blacklist_users == ['user1', 'user2', 'user3']
assert return_whitelist_users == ['user1', 'user2', 'user3']

View File

@@ -16,86 +16,201 @@ sys.path.append(parent)
from src.main import cleanup_watched from src.main import cleanup_watched
tv_shows_watched_list_1 = { tv_shows_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, {
{'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)} "imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
},
{
"imdb": "tt0550487",
"tmdb": "282861",
"tvdb": "300385",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
},
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E01.mkv',)}, {"locations": ("Test S01E01.mkv",)},
{'locations': ('Test S01E02.mkv',)} {"locations": ("Test S01E02.mkv",)},
] ]
} },
} }
movies_watched_list_1 = [ movies_watched_list_1 = [
{"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, {
{"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)}, "imdb": "tt2380307",
"tmdb": "354912",
"title": "Coco",
"locations": ("Coco (2017) Remux-1080p.mkv",),
},
{
"tmdbcollection": "448150",
"imdb": "tt1431045",
"tmdb": "293660",
"title": "Deadpool",
"locations": ("Deadpool (2016) Remux-1080p.mkv",),
},
] ]
tv_shows_watched_list_2 = { tv_shows_watched_list_2 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)}, {
{'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} "imdb": "tt0550487",
"tmdb": "282861",
"tvdb": "300385",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
},
{
"imdb": "tt0550498",
"tmdb": "282865",
"tvdb": "300474",
"locations": (
"Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",
),
},
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E02.mkv',)}, {"locations": ("Test S01E02.mkv",)},
{'locations': ('Test S01E03.mkv',)} {"locations": ("Test S01E03.mkv",)},
] ]
} },
} }
movies_watched_list_2 = [ movies_watched_list_2 = [
{"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, {
{'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} "imdb": "tt2380307",
"tmdb": "354912",
"title": "Coco",
"locations": ("Coco (2017) Remux-1080p.mkv",),
},
{
"imdb": "tt0384793",
"tmdb": "9788",
"tvdb": "9103",
"title": "Accepted",
"locations": ("Accepted (2006) Remux-1080p.mkv",),
},
] ]
# Test to see if objects get deleted all the way up to the root. # Test to see if objects get deleted all the way up to the root.
tv_shows_2_watched_list_1 = { tv_shows_2_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, {
"imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
},
] ]
} }
} }
expected_tv_show_watched_list_1 = { expected_tv_show_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)} {
"imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [{"locations": ("Test S01E01.mkv",)}]
{'locations': ('Test S01E01.mkv',)} },
]
}
} }
expected_movie_watched_list_1 = [ expected_movie_watched_list_1 = [
{"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)} {
"tmdbcollection": "448150",
"imdb": "tt1431045",
"tmdb": "293660",
"title": "Deadpool",
"locations": ("Deadpool (2016) Remux-1080p.mkv",),
}
] ]
expected_tv_show_watched_list_2 = { expected_tv_show_watched_list_2 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} {
"imdb": "tt0550498",
"tmdb": "282865",
"tvdb": "300474",
"locations": (
"Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",
),
}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [{"locations": ("Test S01E03.mkv",)}]
{'locations': ('Test S01E03.mkv',)} },
]
}
} }
expected_movie_watched_list_2 = [ expected_movie_watched_list_2 = [
{'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} {
"imdb": "tt0384793",
"tmdb": "9788",
"tvdb": "9103",
"title": "Accepted",
"locations": ("Accepted (2006) Remux-1080p.mkv",),
}
] ]
@@ -104,28 +219,28 @@ def test_simple_cleanup_watched():
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_1, "TV Shows": tv_shows_watched_list_1,
"Movies": movies_watched_list_1, "Movies": movies_watched_list_1,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1,
}, },
} }
user_watched_list_2 = { user_watched_list_2 = {
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_2, "TV Shows": tv_shows_watched_list_2,
"Movies": movies_watched_list_2, "Movies": movies_watched_list_2,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1,
} }
} }
expected_watched_list_1 = { expected_watched_list_1 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_1 "TV Shows": expected_tv_show_watched_list_1,
, "Movies": expected_movie_watched_list_1 "Movies": expected_movie_watched_list_1,
} }
} }
expected_watched_list_2 = { expected_watched_list_2 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_2 "TV Shows": expected_tv_show_watched_list_2,
, "Movies": expected_movie_watched_list_2 "Movies": expected_movie_watched_list_2,
} }
} }
@@ -141,36 +256,46 @@ def test_mapping_cleanup_watched():
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_1, "TV Shows": tv_shows_watched_list_1,
"Movies": movies_watched_list_1, "Movies": movies_watched_list_1,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1,
}, },
} }
user_watched_list_2 = { user_watched_list_2 = {
"user2": { "user2": {
"Shows": tv_shows_watched_list_2, "Shows": tv_shows_watched_list_2,
"Movies": movies_watched_list_2, "Movies": movies_watched_list_2,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1,
} }
} }
expected_watched_list_1 = { expected_watched_list_1 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_1 "TV Shows": expected_tv_show_watched_list_1,
, "Movies": expected_movie_watched_list_1 "Movies": expected_movie_watched_list_1,
} }
} }
expected_watched_list_2 = { expected_watched_list_2 = {
"user2": { "user2": {
"Shows": expected_tv_show_watched_list_2 "Shows": expected_tv_show_watched_list_2,
, "Movies": expected_movie_watched_list_2 "Movies": expected_movie_watched_list_2,
} }
} }
user_mapping = {"user1": "user2"} user_mapping = {"user1": "user2"}
library_mapping = {"TV Shows": "Shows"} library_mapping = {"TV Shows": "Shows"}
return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2, user_mapping=user_mapping, library_mapping=library_mapping) return_watched_list_1 = cleanup_watched(
return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1, user_mapping=user_mapping, library_mapping=library_mapping) user_watched_list_1,
user_watched_list_2,
user_mapping=user_mapping,
library_mapping=library_mapping,
)
return_watched_list_2 = cleanup_watched(
user_watched_list_2,
user_watched_list_1,
user_mapping=user_mapping,
library_mapping=library_mapping,
)
assert return_watched_list_1 == expected_watched_list_1 assert return_watched_list_1 == expected_watched_list_1
assert return_watched_list_2 == expected_watched_list_2 assert return_watched_list_2 == expected_watched_list_2