13 Commits

Author SHA1 Message Date
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
11 changed files with 1299 additions and 485 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

@@ -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

@@ -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

@@ -4,7 +4,8 @@ from dotenv import load_dotenv
load_dotenv(override=True) 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", "True"))
@@ -29,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:
@@ -36,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():
@@ -45,12 +48,22 @@ 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 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:
@@ -89,7 +102,7 @@ def generate_library_guids_dict(user_list: dict):
try: 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
@@ -101,9 +114,11 @@ def generate_library_guids_dict(user_list: dict):
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(
except: provider_value.lower()
logger(f"Generating show_output_dict failed, skipping", 1) )
except Exception:
logger("Generating show_output_dict failed, skipping", 1)
try: try:
for show in user_list: for show in user_list:
@@ -114,11 +129,15 @@ def generate_library_guids_dict(user_list: dict):
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(
except: episode_value.lower()
logger(f"Generating episode_output_dict failed, skipping", 1) )
except Exception:
logger("Generating episode_output_dict failed, skipping", 1)
try: try:
for movie in user_list: for movie in user_list:
@@ -130,15 +149,16 @@ def generate_library_guids_dict(user_list: dict):
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: except Exception:
logger(f"Generating movies_output_dict failed, skipping", 1) 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): def combine_watched_dicts(dicts: list):
combined_dict = {} combined_dict = {}
for dict in dicts: for single_dict in dicts:
for key, value in dict.items(): for key, value in single_dict.items():
if key not in combined_dict: if key not in combined_dict:
combined_dict[key] = {} combined_dict[key] = {}
for subkey, subvalue in value.items(): for subkey, subvalue in value.items():
@@ -146,12 +166,13 @@ def combine_watched_dicts(dicts: list):
return combined_dict return combined_dict
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() * 1.25)
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,7 +1,14 @@
import asyncio, aiohttp import asyncio, aiohttp
from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict, future_thread_executor, combine_watched_dicts 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
@@ -14,16 +21,12 @@ class Jellyfin():
self.users = asyncio.run(self.get_users()) self.users = asyncio.run(self.get_users())
async def query(self, query, query_type, session, identifiers=None): async def query(self, query, query_type, session, identifiers=None):
try: try:
results = None results = None
headers = { headers = {"Accept": "application/json", "X-Emby-Token": self.token}
"Accept": "application/json",
"X-Emby-Token": self.token
}
authorization = ( authorization = (
'MediaBrowser , ' "MediaBrowser , "
'Client="other", ' 'Client="other", '
'Device="script", ' 'Device="script", '
'DeviceId="script", ' 'DeviceId="script", '
@@ -32,11 +35,15 @@ class Jellyfin():
headers["X-Emby-Authorization"] = authorization headers["X-Emby-Authorization"] = authorization
if query_type == "get": if query_type == "get":
async with session.get(self.baseurl + query, headers=headers) as response: async with session.get(
self.baseurl + query, headers=headers
) as response:
results = await response.json() results = await response.json()
elif query_type == "post": elif query_type == "post":
async with session.post(self.baseurl + query, headers=headers) as response: async with session.post(
self.baseurl + query, headers=headers
) as response:
results = await response.json() results = await response.json()
# append identifiers to results # append identifiers to results
@@ -45,9 +52,8 @@ class Jellyfin():
return results 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)
async def get_users(self): async def get_users(self):
try: try:
@@ -67,47 +73,79 @@ class Jellyfin():
logger(f"Jellyfin: Get users failed {e}", 2) logger(f"Jellyfin: Get users failed {e}", 2)
raise Exception(e) raise Exception(e)
async def get_user_watched(
async def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): 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,
)
# Movies # Movies
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
if library_type == "Movie": if library_type == "Movie":
user_watched[user_name][library_title] = [] user_watched[user_name][library_title] = []
watched = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) 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: if movie["UserData"]["Played"] is True:
movie_guids = {} movie_guids = {}
movie_guids["title"] = movie["Name"] movie_guids["title"] = movie["Name"]
if "ProviderIds" in movie: if "ProviderIds" in movie:
# Lowercase movie["ProviderIds"] keys # Lowercase movie["ProviderIds"] keys
movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()} movie_guids = {
k.lower(): v
for k, v in movie["ProviderIds"].items()
}
if "MediaSources" in movie: if "MediaSources" in movie:
movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]]) movie_guids["locations"] = tuple(
[
x["Path"].split("/")[-1]
for x in movie["MediaSources"]
]
)
user_watched[user_name][library_title].append(movie_guids) user_watched[user_name][library_title].append(movie_guids)
# TV Shows # TV Shows
if library_type == "Series": if library_type == "Series":
user_watched[user_name][library_title] = {} user_watched[user_name][library_title] = {}
watched_shows = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount", "get", session) watched_shows = await self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount",
"get",
session,
)
watched_shows_filtered = [] watched_shows_filtered = []
for show in watched_shows["Items"]: for show in watched_shows["Items"]:
if "PlayedPercentage" in show["UserData"]: if "PlayedPercentage" in show["UserData"]:
if show["UserData"]["PlayedPercentage"] > 0: if show["UserData"]["PlayedPercentage"] > 0:
watched_shows_filtered.append(show) watched_shows_filtered.append(show)
seasons_tasks = [] seasons_tasks = []
for show in watched_shows_filtered: for show in watched_shows_filtered:
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()} 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())
identifiers = {"show_guids": show_guids, "show_id": show["Id"]} identifiers = {"show_guids": show_guids, "show_id": show["Id"]}
task = asyncio.ensure_future(self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount", "get", session, frozenset(identifiers.items()))) task = asyncio.ensure_future(
self.query(
f"/Shows/{show['Id']}/Seasons"
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount",
"get",
session,
frozenset(identifiers.items()),
)
)
seasons_tasks.append(task) seasons_tasks.append(task)
seasons_watched = await asyncio.gather(*seasons_tasks) seasons_watched = await asyncio.gather(*seasons_tasks)
@@ -115,15 +153,21 @@ class Jellyfin():
for seasons in seasons_watched: for seasons in seasons_watched:
seasons_watched_filtered_dict = {} seasons_watched_filtered_dict = {}
seasons_watched_filtered_dict["Identifiers"] = seasons["Identifiers"] seasons_watched_filtered_dict["Identifiers"] = seasons[
"Identifiers"
]
seasons_watched_filtered_dict["Items"] = [] seasons_watched_filtered_dict["Items"] = []
for season in seasons["Items"]: for season in seasons["Items"]:
if "PlayedPercentage" in season["UserData"]: if "PlayedPercentage" in season["UserData"]:
if season["UserData"]["PlayedPercentage"] > 0: if season["UserData"]["PlayedPercentage"] > 0:
seasons_watched_filtered_dict["Items"].append(season) seasons_watched_filtered_dict["Items"].append(
season
)
if seasons_watched_filtered_dict["Items"]: if seasons_watched_filtered_dict["Items"]:
seasons_watched_filtered.append(seasons_watched_filtered_dict) seasons_watched_filtered.append(
seasons_watched_filtered_dict
)
episodes_tasks = [] episodes_tasks = []
for seasons in seasons_watched_filtered: for seasons in seasons_watched_filtered:
@@ -132,36 +176,88 @@ class Jellyfin():
season_identifiers = dict(seasons["Identifiers"]) season_identifiers = dict(seasons["Identifiers"])
season_identifiers["season_id"] = season["Id"] season_identifiers["season_id"] = season["Id"]
season_identifiers["season_name"] = season["Name"] season_identifiers["season_name"] = season["Name"]
task = asyncio.ensure_future(self.query(f"/Shows/{season_identifiers['show_id']}/Episodes?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources", "get", session, frozenset(season_identifiers.items()))) task = asyncio.ensure_future(
self.query(
f"/Shows/{season_identifiers['show_id']}/Episodes"
+ f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources",
"get",
session,
frozenset(season_identifiers.items()),
)
)
episodes_tasks.append(task) episodes_tasks.append(task)
watched_episodes = await asyncio.gather(*episodes_tasks) watched_episodes = await asyncio.gather(*episodes_tasks)
for episodes in watched_episodes: for episodes in watched_episodes:
if len(episodes["Items"]) > 0: if len(episodes["Items"]) > 0:
for episode in episodes["Items"]: for episode in episodes["Items"]:
if episode["UserData"]["Played"] == True: if episode["UserData"]["Played"] is True:
if "ProviderIds" in episode or "MediaSources" in episode: if (
episode_identifiers = dict(episodes["Identifiers"]) "ProviderIds" in episode
or "MediaSources" in episode
):
episode_identifiers = dict(
episodes["Identifiers"]
)
show_guids = episode_identifiers["show_guids"] show_guids = episode_identifiers["show_guids"]
if show_guids not in user_watched[user_name][library_title]: if (
user_watched[user_name][library_title][show_guids] = {} show_guids
if episode_identifiers["season_name"] not in user_watched[user_name][library_title][show_guids]: not in user_watched[user_name][
user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]] = [] library_title
]
):
user_watched[user_name][library_title][
show_guids
] = {}
if (
episode_identifiers["season_name"]
not in user_watched[user_name][
library_title
][show_guids]
):
user_watched[user_name][library_title][
show_guids
][episode_identifiers["season_name"]] = []
episode_guids = {} episode_guids = {}
if "ProviderIds" in episode: if "ProviderIds" in episode:
episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()} episode_guids = {
k.lower(): v
for k, v in episode[
"ProviderIds"
].items()
}
if "MediaSources" in episode: if "MediaSources" in episode:
episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]]) episode_guids["locations"] = tuple(
user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]].append(episode_guids) [
x["Path"].split("/")[-1]
for x in episode["MediaSources"]
]
)
user_watched[user_name][library_title][
show_guids
][episode_identifiers["season_name"]].append(
episode_guids
)
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(
async def get_users_watched(self, user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): self,
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
try: try:
# Get all libraries # Get all libraries
user_name = user_name.lower() user_name = user_name.lower()
@@ -173,11 +269,24 @@ class Jellyfin():
for library in libraries["Items"]: for library in libraries["Items"]:
library_id = library["Id"] library_id = library["Id"]
library_title = library["Name"] library_title = library["Name"]
identifiers = {"library_id": library_id, "library_title": library_title} identifiers = {
task = asyncio.ensure_future(self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&limit=1", "get", session, identifiers=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) tasks_libraries.append(task)
libraries = await asyncio.gather(*tasks_libraries, return_exceptions=True) libraries = await asyncio.gather(
*tasks_libraries, return_exceptions=True
)
for watched in libraries: for watched in libraries:
if len(watched["Items"]) == 0: if len(watched["Items"]) == 0:
@@ -187,14 +296,29 @@ class Jellyfin():
library_title = watched["Identifiers"]["library_title"] 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
# Get watched for user # Get watched for user
task = asyncio.ensure_future(self.get_user_watched(user_name, user_id, library_type, library_id, library_title)) task = asyncio.ensure_future(
self.get_user_watched(
user_name, user_id, library_type, library_id, library_title
)
)
tasks_watched.append(task) tasks_watched.append(task)
watched = await asyncio.gather(*tasks_watched, return_exceptions=True) watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
@@ -203,14 +327,31 @@ class Jellyfin():
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2) logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
raise Exception(e) raise Exception(e)
async def get_watched(
async def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): self,
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping=None,
):
try: try:
users_watched = {} users_watched = {}
watched = [] watched = []
for user_name, user_id in users.items(): for user_name, user_id in users.items():
watched.append(await self.get_users_watched(user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)) watched.append(
await self.get_users_watched(
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
)
for user_watched in watched: for user_watched in watched:
user_watched_temp = combine_watched_dicts(user_watched) user_watched_temp = combine_watched_dicts(user_watched)
@@ -224,107 +365,203 @@ 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)
async def update_user_watched(
async def update_user_watched(self, user_name, user_id, library, library_id, videos, dryrun): self, user_name, user_id, library, library_id, videos, dryrun
):
try: try:
logger(f"Jellyfin: Updating watched for {user_name} in library {library}", 1) logger(
videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos) f"Jellyfin: Updating watched for {user_name} in library {library}", 1
)
(
videos_shows_ids,
videos_episodes_ids,
videos_movies_ids,
) = generate_library_guids_dict(videos)
logger(f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1) logger(
f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
1,
)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
if videos_movies_ids: if videos_movies_ids:
jellyfin_search = await self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) 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
break
if not movie_found:
for (
movie_provider_source,
movie_provider_id,
) in jellyfin_video["ProviderIds"].items():
if movie_provider_source.lower() in videos_movies_ids:
if (
movie_provider_id.lower()
in videos_movies_ids[
movie_provider_source.lower()
]
):
movie_found = True movie_found = True
break break
if not movie_found: if movie_found:
for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items(): jellyfin_video_id = jellyfin_video["Id"]
if movie_provider_source.lower() in videos_movies_ids: msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin"
if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]: if not dryrun:
movie_found = True logger(f"Marking {msg}", 0)
break await self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
if movie_found: "post",
jellyfin_video_id = jellyfin_video["Id"] session,
msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin" )
if not dryrun:
logger(f"Marking {msg}", 0)
await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post", session)
else:
logger(f"Dryrun {msg}", 0)
else: else:
logger(f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 1) 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 videos_shows_ids and videos_episodes_ids: if videos_shows_ids and videos_episodes_ids:
jellyfin_search = await self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,Path", "get", session) 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,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 "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) 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 = await self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) 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_episodes_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_episodes_ids: episode_provider_source,
if episode_provider_id.lower() in videos_episodes_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['Name']} as watched for {user_name} 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)
await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post", session) 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: else:
logger(f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 1) logger(
f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}",
1,
)
else: else:
logger(f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", 1) logger(
f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}",
1,
)
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids: if (
logger(f"Jellyfin: No videos to mark as watched for {user_name} in library {library}", 1) 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_name} 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(
async 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:
tasks = [] tasks = []
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@@ -353,7 +590,9 @@ class Jellyfin():
logger(f"{user} {user_other} not found in Jellyfin", 2) logger(f"{user} {user_other} not found in Jellyfin", 2)
continue continue
jellyfin_libraries = await self.query(f"/Users/{user_id}/Views", "get", session) jellyfin_libraries = await self.query(
f"/Users/{user_id}/Views", "get", session
)
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]] jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
for library, videos in libraries.items(): for library, videos in libraries.items():
@@ -364,17 +603,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",
2,
)
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",
2,
)
continue continue
library_id = None library_id = None
@@ -384,10 +635,12 @@ class Jellyfin():
continue continue
if library_id: if library_id:
task = self.update_user_watched(user_name, user_id, library, library_id, videos, dryrun) task = self.update_user_watched(
user_name, user_id, library, library_id, videos, dryrun
)
tasks.append(task) tasks.append(task)
await asyncio.gather(*tasks, return_exceptions=True) 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

@@ -2,13 +2,21 @@ import copy, os, traceback, json, asyncio
from dotenv import load_dotenv from dotenv import load_dotenv
from time import sleep, perf_counter 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,
generate_library_guids_dict,
)
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):
def cleanup_watched(
watched_list_1, watched_list_2, user_mapping=None, library_mapping=None
):
modified_watched_list_1 = copy.deepcopy(watched_list_1) modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from plex_watched that are in jellyfin_watched # remove entries from plex_watched that are in jellyfin_watched
@@ -35,10 +43,17 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m
elif library_other in watched_list_2[user_2]: elif library_other in watched_list_2[user_2]:
library_2 = library_other library_2 = library_other
else: else:
logger(f"library {library_1} and {library_other} not found in watched list 2", 1) logger(
f"library {library_1} and {library_other} not found in watched list 2",
1,
)
continue continue
_, episode_watched_list_2_keys_dict, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2]) (
_,
episode_watched_list_2_keys_dict,
movies_watched_list_2_keys_dict,
) = generate_library_guids_dict(watched_list_2[user_2][library_2])
# Movies # Movies
if isinstance(watched_list_1[user_1][library_1], list): if isinstance(watched_list_1[user_1][library_1], list):
@@ -46,22 +61,39 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m
movie_found = False movie_found = False
for movie_key, movie_value in movie.items(): for movie_key, movie_value in movie.items():
if movie_key == "locations": if movie_key == "locations":
if "locations" in movies_watched_list_2_keys_dict.keys(): if (
"locations"
in movies_watched_list_2_keys_dict.keys()
):
for location in movie_value: for location in movie_value:
if location in movies_watched_list_2_keys_dict["locations"]: if (
location
in movies_watched_list_2_keys_dict[
"locations"
]
):
movie_found = True movie_found = True
break break
else: else:
if movie_key in movies_watched_list_2_keys_dict.keys(): if (
if movie_value in movies_watched_list_2_keys_dict[movie_key]: 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 movie_found = True
if movie_found: if movie_found:
logger(f"Removing {movie} from {library_1}", 3) logger(f"Removing {movie} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(movie) modified_watched_list_1[user_1][library_1].remove(
movie
)
break break
# TV Shows # TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict): 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 # Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
@@ -69,47 +101,107 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m
for show_key_1 in watched_list_1[user_1][library_1].keys(): for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1) show_key_dict = dict(show_key_1)
for season in watched_list_1[user_1][library_1][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]: for episode in watched_list_1[user_1][library_1][
show_key_1
][season]:
episode_found = False episode_found = False
for episode_key, episode_value in episode.items(): 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 and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
if episode_key == "locations": if episode_key == "locations":
if "locations" in episode_watched_list_2_keys_dict.keys(): if (
"locations"
in episode_watched_list_2_keys_dict.keys()
):
for location in episode_value: for location in episode_value:
if location in episode_watched_list_2_keys_dict["locations"]: if (
location
in episode_watched_list_2_keys_dict[
"locations"
]
):
episode_found = True episode_found = True
break break
else: else:
if episode_key in episode_watched_list_2_keys_dict.keys(): if (
if episode_value in episode_watched_list_2_keys_dict[episode_key]: 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 episode_found = True
if episode_found: if episode_found:
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]: if (
logger(f"Removing {episode} from {show_key_dict['title']}", 3) episode
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(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)
break break
# Remove empty seasons # Remove empty seasons
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0: if (
if season in modified_watched_list_1[user_1][library_1][show_key_1]: len(
logger(f"Removing {season} from {show_key_dict['title']} because it is empty", 3) modified_watched_list_1[user_1][library_1][
del modified_watched_list_1[user_1][library_1][show_key_1][season] 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]
# If the show is empty, remove the show # If the show is empty, remove the show
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0: if (
if show_key_1 in modified_watched_list_1[user_1][library_1]: len(
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1) modified_watched_list_1[user_1][library_1][
del modified_watched_list_1[user_1][library_1][show_key_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 user_1 in watched_list_1:
for library_1 in watched_list_1[user_1]: for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]: if library_1 in modified_watched_list_1[user_1]:
# If library is empty then remove it # If library is empty then remove it
if len(modified_watched_list_1[user_1][library_1]) == 0: if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1) logger(f"Removing {library_1} from {user_1} because it is empty", 1)
del modified_watched_list_1[user_1][library_1] del modified_watched_list_1[user_1][library_1]
if user_1 in modified_watched_list_1: if user_1 in modified_watched_list_1:
# If user is empty delete user # If user is empty delete user
@@ -119,7 +211,17 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m
return modified_watched_list_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):
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 blacklist_library:
if len(blacklist_library) > 0: if len(blacklist_library) > 0:
blacklist_library = blacklist_library.split(",") blacklist_library = blacklist_library.split(",")
@@ -202,9 +304,19 @@ def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blac
whitelist_users = [] whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1) logger(f"Whitelist Users: {whitelist_users}", 1)
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users 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):
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]
@@ -216,23 +328,22 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi
server_1_users = [] server_1_users = []
if server_1_type == "plex": if server_1_type == "plex":
server_1_users = [ x.title.lower() for x in server_1_connection.users ] server_1_users = [x.title.lower() for x in server_1_connection.users]
elif server_1_type == "jellyfin": elif server_1_type == "jellyfin":
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ] server_1_users = [key.lower() for key in server_1_connection.users.keys()]
server_2_users = [] server_2_users = []
if server_2_type == "plex": if server_2_type == "plex":
server_2_users = [ x.title.lower() for x in server_2_connection.users ] server_2_users = [x.title.lower() for x in server_2_connection.users]
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 = {}
for server_1_user in server_1_users: for server_1_user in server_1_users:
if user_mapping: if user_mapping:
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user) jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
if jellyfin_plex_mapped_user: if jellyfin_plex_mapped_user:
users[server_1_user] = jellyfin_plex_mapped_user users[server_1_user] = jellyfin_plex_mapped_user
continue continue
@@ -242,7 +353,7 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi
for server_2_user in server_2_users: for server_2_user in server_2_users:
if user_mapping: if user_mapping:
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
if plex_jellyfin_mapped_user: if plex_jellyfin_mapped_user:
users[plex_jellyfin_mapped_user] = server_2_user users[plex_jellyfin_mapped_user] = server_2_user
continue continue
@@ -268,36 +379,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 = []
@@ -313,21 +441,51 @@ def generate_server_connections():
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, ssl_bypass=ssl_bypass))) 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(), ssl_bypass=ssl_bypass))) 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)
@@ -337,16 +495,23 @@ 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()))) servers.append(
(
"jellyfin",
Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()),
)
)
return servers return servers
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
if os.path.exists(logfile): if os.path.exists(logfile):
os.remove(logfile) os.remove(logfile)
@@ -373,7 +538,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)
@@ -385,49 +566,83 @@ def main_loop():
break break
# 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_1_connection = server_1[1]
server_2_connection = server_2[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)
server_1_watched = server_1_connection.get_watched(server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) server_1_watched = server_1_connection.get_watched(
server_1_users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
logger("Finished creating watched list server 1", 1) logger("Finished creating watched list server 1", 1)
server_2_watched = asyncio.run(server_2_connection.get_watched(server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)) server_2_watched = asyncio.run(
server_2_connection.get_watched(
server_2_users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
)
logger("Finished creating watched list server 2", 1) 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,
)
server_1_connection.update_watched(
server_2_watched_filtered, user_mapping, library_mapping, dryrun
)
asyncio.run(
server_2_connection.update_watched(
server_1_watched_filtered, user_mapping, library_mapping, dryrun
)
)
server_1_connection.update_watched(server_2_watched_filtered, user_mapping, library_mapping, dryrun)
asyncio.run(server_2_connection.update_watched(server_1_watched_filtered, user_mapping, library_mapping, dryrun))
def main(): def main():
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
times = [] times = []
while(True): while True:
try: try:
start = perf_counter() start = perf_counter()
main_loop() main_loop()
end = perf_counter() end = perf_counter()
times.append(end - start) 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)
@@ -438,13 +653,10 @@ 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)
except KeyboardInterrupt: except KeyboardInterrupt:
if len(times) > 0:
logger(f"Average time: {sum(times) / len(times)}", 0)
logger("Exiting", log_type=0) logger("Exiting", log_type=0)
os._exit(0) os._exit(0)

View File

@@ -1,33 +1,282 @@
import re, requests 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_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,
)
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 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}",
1,
)
else:
logger(
f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}",
1,
)
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, ssl_bypass=False): 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(ssl_bypass) 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, ssl_bypass=False): 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
if ssl_bypass: if ssl_bypass:
session = requests.Session() session = requests.Session()
session.verify = False # By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
plex = PlexServer(self.baseurl, self.token, session=session) session.mount("https://", HostNameIgnoringAdapter())
else: plex = PlexServer(baseurl, token, session=session)
plex = PlexServer(self.baseurl, self.token) 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)
@@ -44,7 +293,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()
@@ -57,77 +305,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_watched(
def get_user_watched(self, user, user_plex, library): self,
try: users,
user_name = user.title.lower() blacklist_library,
user_watched = {} whitelist_library,
user_watched[user_name] = {} blacklist_library_type,
whitelist_library_type,
logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0) 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 = {}
@@ -137,7 +323,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 = self.login(
self.plex._baseurl, user.get_token(self.plex.machineIdentifier), self.ssl_bypass
)
libraries = user_plex.library.sections() libraries = user_plex.library.sections()
@@ -145,13 +333,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_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():
@@ -164,104 +362,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)
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}", 1)
else:
logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1)
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)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try: try:
args = [] args = []
@@ -285,7 +388,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
@@ -299,18 +404,36 @@ 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",
2,
)
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", 2
)
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