Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16879cc728 | ||
|
|
942ec3533f | ||
|
|
9f6edfc91a | ||
|
|
827ace2e97 | ||
|
|
f6b57a1b4d | ||
|
|
88a7526721 | ||
|
|
1efb4d8543 | ||
|
|
7571e9a343 | ||
|
|
7640e9ee03 | ||
|
|
50ed3d6400 | ||
|
|
c9a373851f | ||
|
|
a3f3db8f4e | ||
|
|
de619de923 | ||
|
|
852d8dc3c3 | ||
|
|
c104973f95 | ||
|
|
8b7fc5e323 | ||
|
|
afb71d8e00 | ||
|
|
34d97f8dde | ||
|
|
2ad6b3afdf | ||
|
|
7cd492dc98 | ||
|
|
74b5ea7b5e | ||
|
|
21fe4875eb | ||
|
|
aeb86f6b85 | ||
|
|
70ef31ff47 | ||
|
|
0584a85f90 | ||
|
|
beb4e667ae | ||
|
|
7695994ec2 |
@@ -2,8 +2,8 @@
|
|||||||
DRYRUN = "True"
|
DRYRUN = "True"
|
||||||
## Additional logging information
|
## Additional logging information
|
||||||
DEBUG = "True"
|
DEBUG = "True"
|
||||||
## Debugging level, INFO is default, DEBUG is more verbose
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
DEBUG_LEVEL = "INFO"
|
DEBUG_LEVEL = "info"
|
||||||
## How often to run the script in seconds
|
## How often to run the script in seconds
|
||||||
SLEEP_DURATION = "3600"
|
SLEEP_DURATION = "3600"
|
||||||
## Log file where all output will be written to
|
## Log file where all output will be written to
|
||||||
@@ -16,6 +16,7 @@ LOGFILE = "log.log"
|
|||||||
|
|
||||||
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
|
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
|
||||||
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
|
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
PLEX_BASEURL = "http://localhost:32400"
|
PLEX_BASEURL = "http://localhost:32400"
|
||||||
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
||||||
PLEX_TOKEN = "SuperSecretToken"
|
PLEX_TOKEN = "SuperSecretToken"
|
||||||
@@ -26,6 +27,7 @@ PLEX_TOKEN = "SuperSecretToken"
|
|||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
JELLYFIN_BASEURL = "http://localhost:8096"
|
JELLYFIN_BASEURL = "http://localhost:8096"
|
||||||
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
||||||
JELLYFIN_TOKEN = "SuperSecretToken"
|
JELLYFIN_TOKEN = "SuperSecretToken"
|
||||||
|
|||||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -10,8 +10,20 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
run: pip install -r requirements.txt && pip install -r test/requirements.txt
|
||||||
|
|
||||||
|
- name: "Run tests"
|
||||||
|
run: pytest -vvv
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: pytest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
|
*.prof
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python",
|
"name": "Python: Main",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "main.py",
|
"program": "main.py",
|
||||||
|
|||||||
25
Dockerfile
25
Dockerfile
@@ -1,5 +1,30 @@
|
|||||||
FROM python:3-slim
|
FROM python:3-slim
|
||||||
|
|
||||||
|
ENV DRYRUN 'True'
|
||||||
|
ENV DEBUG 'True'
|
||||||
|
ENV DEBUG_LEVEL 'INFO'
|
||||||
|
ENV SLEEP_DURATION '3600'
|
||||||
|
ENV LOGFILE 'log.log'
|
||||||
|
|
||||||
|
ENV USER_MAPPING '{ "User Test": "User Test2" }'
|
||||||
|
ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }'
|
||||||
|
|
||||||
|
ENV PLEX_BASEURL 'http://localhost:32400'
|
||||||
|
ENV PLEX_TOKEN ''
|
||||||
|
ENV PLEX_USERNAME ''
|
||||||
|
ENV PLEX_PASSWORD ''
|
||||||
|
ENV PLEX_SERVERNAME ''
|
||||||
|
|
||||||
|
ENV JELLYFIN_BASEURL 'http://localhost:8096'
|
||||||
|
ENV JELLYFIN_TOKEN ''
|
||||||
|
|
||||||
|
ENV BLACKLIST_LIBRARY ''
|
||||||
|
ENV WHITELIST_LIBRARY ''
|
||||||
|
ENV BLACKLIST_LIBRARY_TYPE ''
|
||||||
|
ENV WHITELIST_LIBRARY_TYPE ''
|
||||||
|
ENV BLACKLIST_USERS ''
|
||||||
|
ENV WHITELIST_USERS ''
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ./requirements.txt ./
|
COPY ./requirements.txt ./
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ Sync watched between jellyfin and plex
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Keep in sync all your users watched history between jellyfin and plex locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases.
|
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by enterying multiple options in the .env plex/jellyfin section seperated by commas.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -52,7 +56,7 @@ Keep in sync all your users watched history between jellyfin and plex locally. T
|
|||||||
|
|
||||||
#### With .env
|
#### With .env
|
||||||
|
|
||||||
- Create a .env file similar to .env.sample and set the MNEMONIC variable to your seed phrase
|
- Create a .env file similar to .env.sample and set the variables to match your setup
|
||||||
|
|
||||||
- Run
|
- Run
|
||||||
|
|
||||||
|
|||||||
311
main.py
311
main.py
@@ -1,307 +1,10 @@
|
|||||||
import copy, os, traceback, json
|
import sys
|
||||||
from dotenv import load_dotenv
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict
|
if __name__ == '__main__':
|
||||||
from src.plex import Plex
|
# Check python version 3.6 or higher
|
||||||
from src.jellyfin import Jellyfin
|
if not (3, 6) <= tuple(map(int, sys.version_info[:2])):
|
||||||
|
print("This script requires Python 3.6 or higher")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
load_dotenv(override=True)
|
from src.main import main
|
||||||
|
|
||||||
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
|
|
||||||
modified_watched_list_1 = copy.deepcopy(watched_list_1)
|
|
||||||
|
|
||||||
# remove entries from plex_watched that are in jellyfin_watched
|
|
||||||
for user_1 in watched_list_1:
|
|
||||||
user_other = None
|
|
||||||
if user_mapping:
|
|
||||||
user_other = search_mapping(user_mapping, user_1)
|
|
||||||
if user_1 in modified_watched_list_1:
|
|
||||||
if user_1 in watched_list_2:
|
|
||||||
user_2 = user_1
|
|
||||||
elif user_other in watched_list_2:
|
|
||||||
user_2 = user_other
|
|
||||||
else:
|
|
||||||
logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for library_1 in watched_list_1[user_1]:
|
|
||||||
library_other = None
|
|
||||||
if library_mapping:
|
|
||||||
library_other = search_mapping(library_mapping, library_1)
|
|
||||||
if library_1 in modified_watched_list_1[user_1]:
|
|
||||||
if library_1 in watched_list_2[user_2]:
|
|
||||||
library_2 = library_1
|
|
||||||
elif library_other in watched_list_2[user_2]:
|
|
||||||
library_2 = library_other
|
|
||||||
else:
|
|
||||||
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Movies
|
|
||||||
if isinstance(watched_list_1[user_1][library_1], list):
|
|
||||||
for item in watched_list_1[user_1][library_1]:
|
|
||||||
for watch_list_1_key, watch_list_1_value in item.items():
|
|
||||||
for watch_list_2_item in watched_list_2[user_2][library_2]:
|
|
||||||
for watch_list_2_item_key, watch_list_2_item_value in watch_list_2_item.items():
|
|
||||||
if watch_list_1_key == watch_list_2_item_key and watch_list_1_value == watch_list_2_item_value:
|
|
||||||
if item in modified_watched_list_1[user_1][library_1]:
|
|
||||||
logger(f"Removing {item} from {library_1}", 3)
|
|
||||||
modified_watched_list_1[user_1][library_1].remove(item)
|
|
||||||
|
|
||||||
|
|
||||||
# TV Shows
|
|
||||||
elif isinstance(watched_list_1[user_1][library_1], dict):
|
|
||||||
# Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
|
|
||||||
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 1)
|
|
||||||
|
|
||||||
for show_key_1 in watched_list_1[user_1][library_1].keys():
|
|
||||||
show_key_dict = dict(show_key_1)
|
|
||||||
for season in watched_list_1[user_1][library_1][show_key_1]:
|
|
||||||
for episode in watched_list_1[user_1][library_1][show_key_1][season]:
|
|
||||||
for episode_key, episode_item in episode.items():
|
|
||||||
# If episode_key and episode_item are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
|
|
||||||
if episode_key in episode_watched_list_2_keys_dict.keys():
|
|
||||||
if episode_item in episode_watched_list_2_keys_dict[episode_key]:
|
|
||||||
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
|
|
||||||
logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3)
|
|
||||||
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
|
|
||||||
|
|
||||||
# Remove empty seasons
|
|
||||||
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
|
|
||||||
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
|
|
||||||
logger(f"Removing {season} from {library_1} because it is empty", 3)
|
|
||||||
del modified_watched_list_1[user_1][library_1][show_key_1][season]
|
|
||||||
|
|
||||||
# If the show is empty, remove the show
|
|
||||||
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
|
|
||||||
if show_key_1 in modified_watched_list_1[user_1][library_1]:
|
|
||||||
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
|
|
||||||
del modified_watched_list_1[user_1][library_1][show_key_1]
|
|
||||||
|
|
||||||
# If library is empty then remove it
|
|
||||||
if len(modified_watched_list_1[user_1][library_1]) == 0:
|
|
||||||
if library_1 in modified_watched_list_1[user_1]:
|
|
||||||
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
|
|
||||||
del modified_watched_list_1[user_1][library_1]
|
|
||||||
|
|
||||||
# If user is empty delete user
|
|
||||||
if len(modified_watched_list_1[user_1]) == 0:
|
|
||||||
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
|
|
||||||
del modified_watched_list_1[user_1]
|
|
||||||
|
|
||||||
return modified_watched_list_1
|
|
||||||
|
|
||||||
def setup_black_white_lists(library_mapping=None):
|
|
||||||
blacklist_library = os.getenv("BLACKLIST_LIBRARY")
|
|
||||||
if blacklist_library:
|
|
||||||
if len(blacklist_library) > 0:
|
|
||||||
blacklist_library = blacklist_library.split(",")
|
|
||||||
blacklist_library = [x.strip() for x in blacklist_library]
|
|
||||||
if library_mapping:
|
|
||||||
temp_library = []
|
|
||||||
for library in blacklist_library:
|
|
||||||
library_other = search_mapping(library_mapping, library)
|
|
||||||
if library_other:
|
|
||||||
temp_library.append(library_other)
|
|
||||||
|
|
||||||
blacklist_library = blacklist_library + temp_library
|
|
||||||
else:
|
|
||||||
blacklist_library = []
|
|
||||||
|
|
||||||
logger(f"Blacklist Library: {blacklist_library}", 1)
|
|
||||||
|
|
||||||
whitelist_library = os.getenv("WHITELIST_LIBRARY")
|
|
||||||
if whitelist_library:
|
|
||||||
if len(whitelist_library) > 0:
|
|
||||||
whitelist_library = whitelist_library.split(",")
|
|
||||||
whitelist_library = [x.strip() for x in whitelist_library]
|
|
||||||
if library_mapping:
|
|
||||||
temp_library = []
|
|
||||||
for library in whitelist_library:
|
|
||||||
library_other = search_mapping(library_mapping, library)
|
|
||||||
if library_other:
|
|
||||||
temp_library.append(library_other)
|
|
||||||
|
|
||||||
whitelist_library = whitelist_library + temp_library
|
|
||||||
else:
|
|
||||||
whitelist_library = []
|
|
||||||
logger(f"Whitelist Library: {whitelist_library}", 1)
|
|
||||||
|
|
||||||
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE")
|
|
||||||
if blacklist_library_type:
|
|
||||||
if len(blacklist_library_type) > 0:
|
|
||||||
blacklist_library_type = blacklist_library_type.split(",")
|
|
||||||
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
|
|
||||||
else:
|
|
||||||
blacklist_library_type = []
|
|
||||||
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
|
|
||||||
|
|
||||||
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE")
|
|
||||||
if whitelist_library_type:
|
|
||||||
if len(whitelist_library_type) > 0:
|
|
||||||
whitelist_library_type = whitelist_library_type.split(",")
|
|
||||||
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
|
|
||||||
else:
|
|
||||||
whitelist_library_type = []
|
|
||||||
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
|
|
||||||
|
|
||||||
blacklist_users = os.getenv("BLACKLIST_USERS")
|
|
||||||
if blacklist_users:
|
|
||||||
if len(blacklist_users) > 0:
|
|
||||||
blacklist_users = blacklist_users.split(",")
|
|
||||||
blacklist_users = [x.lower().strip() for x in blacklist_users]
|
|
||||||
else:
|
|
||||||
blacklist_users = []
|
|
||||||
logger(f"Blacklist Users: {blacklist_users}", 1)
|
|
||||||
|
|
||||||
whitelist_users = os.getenv("WHITELIST_USERS")
|
|
||||||
if whitelist_users:
|
|
||||||
if len(whitelist_users) > 0:
|
|
||||||
whitelist_users = whitelist_users.split(",")
|
|
||||||
whitelist_users = [x.lower().strip() for x in whitelist_users]
|
|
||||||
else:
|
|
||||||
whitelist_users = []
|
|
||||||
else:
|
|
||||||
whitelist_users = []
|
|
||||||
logger(f"Whitelist Users: {whitelist_users}", 1)
|
|
||||||
|
|
||||||
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
|
|
||||||
|
|
||||||
def setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping=None):
|
|
||||||
|
|
||||||
# generate list of users from plex.users
|
|
||||||
plex_users = [ x.title.lower() for x in plex.users ]
|
|
||||||
jellyfin_users = [ key.lower() for key in jellyfin.users.keys() ]
|
|
||||||
|
|
||||||
# combined list of overlapping users from plex and jellyfin
|
|
||||||
users = {}
|
|
||||||
|
|
||||||
for plex_user in plex_users:
|
|
||||||
if user_mapping:
|
|
||||||
jellyfin_plex_mapped_user = search_mapping(user_mapping, plex_user)
|
|
||||||
if jellyfin_plex_mapped_user:
|
|
||||||
users[plex_user] = jellyfin_plex_mapped_user
|
|
||||||
continue
|
|
||||||
|
|
||||||
if plex_user in jellyfin_users:
|
|
||||||
users[plex_user] = plex_user
|
|
||||||
|
|
||||||
for jellyfin_user in jellyfin_users:
|
|
||||||
if user_mapping:
|
|
||||||
plex_jellyfin_mapped_user = search_mapping(user_mapping, jellyfin_user)
|
|
||||||
if plex_jellyfin_mapped_user:
|
|
||||||
users[plex_jellyfin_mapped_user] = jellyfin_user
|
|
||||||
continue
|
|
||||||
|
|
||||||
if jellyfin_user in plex_users:
|
|
||||||
users[jellyfin_user] = jellyfin_user
|
|
||||||
|
|
||||||
logger(f"User list that exist on both servers {users}", 1)
|
|
||||||
|
|
||||||
users_filtered = {}
|
|
||||||
for user in users:
|
|
||||||
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
|
|
||||||
if len(whitelist_users) > 0:
|
|
||||||
if user not in whitelist_users and users[user] not in whitelist_users:
|
|
||||||
logger(f"{user} or {users[user]} is not in whitelist", 1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if user not in blacklist_users and users[user] not in blacklist_users:
|
|
||||||
users_filtered[user] = users[user]
|
|
||||||
|
|
||||||
logger(f"Filtered user list {users_filtered}", 1)
|
|
||||||
|
|
||||||
plex_users = []
|
|
||||||
for plex_user in plex.users:
|
|
||||||
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
|
|
||||||
plex_users.append(plex_user)
|
|
||||||
|
|
||||||
jellyfin_users = {}
|
|
||||||
for jellyfin_user, jellyfin_id in jellyfin.users.items():
|
|
||||||
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
|
|
||||||
jellyfin_users[jellyfin_user] = jellyfin_id
|
|
||||||
|
|
||||||
if len(plex_users) == 0:
|
|
||||||
raise Exception(f"No plex users found, users found {users} filtered users {users_filtered}")
|
|
||||||
|
|
||||||
if len(jellyfin_users) == 0:
|
|
||||||
raise Exception(f"No jellyfin users found, users found {users} filtered users {users_filtered}")
|
|
||||||
|
|
||||||
logger(f"plex_users: {plex_users}", 1)
|
|
||||||
logger(f"jellyfin_users: {jellyfin_users}", 1)
|
|
||||||
|
|
||||||
return plex_users, jellyfin_users
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logfile = os.getenv("LOGFILE","log.log")
|
|
||||||
# Delete logfile if it exists
|
|
||||||
if os.path.exists(logfile):
|
|
||||||
os.remove(logfile)
|
|
||||||
|
|
||||||
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
|
|
||||||
logger(f"Dryrun: {dryrun}", 1)
|
|
||||||
|
|
||||||
user_mapping = os.getenv("USER_MAPPING")
|
|
||||||
if user_mapping:
|
|
||||||
user_mapping = json.loads(user_mapping.lower())
|
|
||||||
logger(f"User Mapping: {user_mapping}", 1)
|
|
||||||
|
|
||||||
library_mapping = os.getenv("LIBRARY_MAPPING")
|
|
||||||
if library_mapping:
|
|
||||||
library_mapping = json.loads(library_mapping)
|
|
||||||
logger(f"Library Mapping: {library_mapping}", 1)
|
|
||||||
|
|
||||||
plex = Plex()
|
|
||||||
jellyfin = Jellyfin()
|
|
||||||
|
|
||||||
# Create (black/white)lists
|
|
||||||
blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(library_mapping)
|
|
||||||
|
|
||||||
# Create users list
|
|
||||||
plex_users, jellyfin_users = setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping)
|
|
||||||
|
|
||||||
plex_watched = plex.get_plex_watched(plex_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
|
|
||||||
jellyfin_watched = jellyfin.get_jellyfin_watched(jellyfin_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
|
|
||||||
|
|
||||||
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
|
|
||||||
plex_watched_filtered = copy.deepcopy(plex_watched)
|
|
||||||
jellyfin_watched_filtered = copy.deepcopy(jellyfin_watched)
|
|
||||||
|
|
||||||
logger("Cleaning Plex Watched", 1)
|
|
||||||
plex_watched = cleanup_watched(plex_watched_filtered, jellyfin_watched_filtered, user_mapping, library_mapping)
|
|
||||||
|
|
||||||
logger("Cleaning Jellyfin Watched", 1)
|
|
||||||
jellyfin_watched = cleanup_watched(jellyfin_watched_filtered, plex_watched_filtered, user_mapping, library_mapping)
|
|
||||||
|
|
||||||
logger(f"plex_watched that needs to be synced to jellyfin:\n{plex_watched}", 1)
|
|
||||||
logger(f"jellyfin_watched that needs to be synced to plex:\n{jellyfin_watched}", 1)
|
|
||||||
|
|
||||||
# Update watched status
|
|
||||||
plex.update_watched(jellyfin_watched, user_mapping, library_mapping, dryrun)
|
|
||||||
jellyfin.update_watched(plex_watched, user_mapping, library_mapping, dryrun)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sleep_timer = float(os.getenv("SLEEP_TIMER", "3600"))
|
|
||||||
|
|
||||||
while(True):
|
|
||||||
try:
|
|
||||||
main()
|
main()
|
||||||
logger(f"Looping in {sleep_timer}")
|
|
||||||
except Exception as error:
|
|
||||||
if isinstance(error, list):
|
|
||||||
for message in error:
|
|
||||||
logger(message, log_type=2)
|
|
||||||
else:
|
|
||||||
logger(error, log_type=2)
|
|
||||||
|
|
||||||
|
|
||||||
logger(traceback.format_exc(), 2)
|
|
||||||
logger(f"Retrying in {sleep_timer}", log_type=0)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger("Exiting", log_type=0)
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
sleep(sleep_timer)
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
plexapi
|
plexapi
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
aiohttp
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import os
|
import os
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from dotenv import load_dotenv
|
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, 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"))
|
||||||
debug_level = os.getenv("DEBUG_LEVEL", "INFO")
|
debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
|
||||||
|
|
||||||
output = str(message)
|
output = str(message)
|
||||||
if log_type == 0:
|
if log_type == 0:
|
||||||
pass
|
pass
|
||||||
elif log_type == 1 and (debug or debug_level == "INFO"):
|
elif log_type == 1 and (debug and debug_level == "info"):
|
||||||
output = f"[INFO]: {output}"
|
output = f"[INFO]: {output}"
|
||||||
elif log_type == 2:
|
elif log_type == 2:
|
||||||
output = f"[ERROR]: {output}"
|
output = f"[ERROR]: {output}"
|
||||||
elif log_type == 3 and (debug and debug_level == "DEBUG"):
|
elif log_type == 3 and (debug and debug_level == "debug"):
|
||||||
output = f"[DEBUG]: {output}"
|
output = f"[DEBUG]: {output}"
|
||||||
|
elif log_type == 4:
|
||||||
|
output = f"[WARNING]: {output}"
|
||||||
else:
|
else:
|
||||||
output = None
|
output = None
|
||||||
|
|
||||||
@@ -37,11 +41,11 @@ def search_mapping(dictionary: dict, key_value: str):
|
|||||||
if key_value in dictionary.keys():
|
if key_value in dictionary.keys():
|
||||||
return dictionary[key_value]
|
return dictionary[key_value]
|
||||||
elif key_value.lower() in dictionary.keys():
|
elif key_value.lower() in dictionary.keys():
|
||||||
return dictionary[key_value]
|
return dictionary[key_value.lower()]
|
||||||
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)]
|
return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -78,39 +82,87 @@ def check_skip_logic(library_title, library_type, blacklist_library, whitelist_l
|
|||||||
return skip_reason
|
return skip_reason
|
||||||
|
|
||||||
|
|
||||||
def generate_library_guids_dict(user_list: dict, generate_output: int):
|
def generate_library_guids_dict(user_list: dict):
|
||||||
# if generate_output is 0 then only generate shows, if 1 then only generate episodes, if 2 then generate movies, if 3 then generate shows and episodes
|
|
||||||
show_output_dict = {}
|
show_output_dict = {}
|
||||||
episode_output_dict = {}
|
episode_output_dict = {}
|
||||||
movies_output_dict = {}
|
movies_output_dict = {}
|
||||||
|
|
||||||
if generate_output in (0, 3):
|
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, prvider_value in show_key.items():
|
for provider_key, provider_value in show_key.items():
|
||||||
# Skip title
|
# Skip title
|
||||||
if provider_key.lower() == "title":
|
if provider_key.lower() == "title":
|
||||||
continue
|
continue
|
||||||
if provider_key.lower() not in show_output_dict:
|
if provider_key.lower() not in show_output_dict:
|
||||||
show_output_dict[provider_key.lower()] = []
|
show_output_dict[provider_key.lower()] = []
|
||||||
show_output_dict[provider_key.lower()].append(prvider_value.lower())
|
if provider_key.lower() == "locations":
|
||||||
|
for show_location in provider_value:
|
||||||
|
show_output_dict[provider_key.lower()].append(show_location)
|
||||||
|
else:
|
||||||
|
show_output_dict[provider_key.lower()].append(provider_value.lower())
|
||||||
|
except:
|
||||||
|
logger(f"Generating show_output_dict failed, skipping", 1)
|
||||||
|
|
||||||
if generate_output in (1, 3):
|
try:
|
||||||
for show in user_list:
|
for show in user_list:
|
||||||
for season in user_list[show]:
|
for season in user_list[show]:
|
||||||
for episode in user_list[show][season]:
|
for episode in user_list[show][season]:
|
||||||
for episode_key, episode_value in episode.items():
|
for episode_key, episode_value in episode.items():
|
||||||
if episode_key.lower() not in episode_output_dict:
|
if episode_key.lower() not in episode_output_dict:
|
||||||
episode_output_dict[episode_key.lower()] = []
|
episode_output_dict[episode_key.lower()] = []
|
||||||
|
if episode_key == "locations":
|
||||||
|
for episode_location in episode_value:
|
||||||
|
episode_output_dict[episode_key.lower()].append(episode_location)
|
||||||
|
else:
|
||||||
episode_output_dict[episode_key.lower()].append(episode_value.lower())
|
episode_output_dict[episode_key.lower()].append(episode_value.lower())
|
||||||
|
except:
|
||||||
|
logger(f"Generating episode_output_dict failed, skipping", 1)
|
||||||
|
|
||||||
if generate_output == 2:
|
try:
|
||||||
for movie in user_list:
|
for movie in user_list:
|
||||||
for movie_key, movie_value in movie.items():
|
for movie_key, movie_value in movie.items():
|
||||||
if movie_key.lower() not in movies_output_dict:
|
if movie_key.lower() not in movies_output_dict:
|
||||||
movies_output_dict[movie_key.lower()] = []
|
movies_output_dict[movie_key.lower()] = []
|
||||||
|
if movie_key == "locations":
|
||||||
|
for movie_location in movie_value:
|
||||||
|
movies_output_dict[movie_key.lower()].append(movie_location)
|
||||||
|
else:
|
||||||
movies_output_dict[movie_key.lower()].append(movie_value.lower())
|
movies_output_dict[movie_key.lower()].append(movie_value.lower())
|
||||||
|
except:
|
||||||
|
logger(f"Generating movies_output_dict failed, skipping", 1)
|
||||||
|
|
||||||
return show_output_dict, episode_output_dict, movies_output_dict
|
return show_output_dict, episode_output_dict, movies_output_dict
|
||||||
|
|
||||||
|
def combine_watched_dicts(dicts: list):
|
||||||
|
combined_dict = {}
|
||||||
|
for dict in dicts:
|
||||||
|
for key, value in dict.items():
|
||||||
|
if key not in combined_dict:
|
||||||
|
combined_dict[key] = {}
|
||||||
|
for subkey, subvalue in value.items():
|
||||||
|
combined_dict[key][subkey] = subvalue
|
||||||
|
|
||||||
|
return combined_dict
|
||||||
|
|
||||||
|
def future_thread_executor(args: list, workers: int = -1):
|
||||||
|
futures_list = []
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if workers == -1:
|
||||||
|
workers = min(32, os.cpu_count()*1.25)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
for arg in args:
|
||||||
|
# * arg unpacks the list into actual arguments
|
||||||
|
futures_list.append(executor.submit(*arg))
|
||||||
|
|
||||||
|
for future in futures_list:
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
results.append(result)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|||||||
454
src/jellyfin.py
454
src/jellyfin.py
@@ -1,16 +1,10 @@
|
|||||||
import requests, os
|
import asyncio, aiohttp
|
||||||
from dotenv import load_dotenv
|
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, str_to_bool, check_skip_logic, generate_library_guids_dict
|
|
||||||
|
|
||||||
load_dotenv(override=True)
|
|
||||||
|
|
||||||
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL")
|
|
||||||
jellyfin_token = os.getenv("JELLYFIN_TOKEN")
|
|
||||||
|
|
||||||
class Jellyfin():
|
class Jellyfin():
|
||||||
def __init__(self):
|
def __init__(self, baseurl, token):
|
||||||
self.baseurl = jellyfin_baseurl
|
self.baseurl = baseurl
|
||||||
self.token = jellyfin_token
|
self.token = token
|
||||||
|
|
||||||
if not self.baseurl:
|
if not self.baseurl:
|
||||||
raise Exception("Jellyfin baseurl not set")
|
raise Exception("Jellyfin baseurl not set")
|
||||||
@@ -18,17 +12,16 @@ class Jellyfin():
|
|||||||
if not self.token:
|
if not self.token:
|
||||||
raise Exception("Jellyfin token not set")
|
raise Exception("Jellyfin token not set")
|
||||||
|
|
||||||
self.users = self.get_users()
|
self.users = asyncio.run(self.get_users())
|
||||||
|
|
||||||
|
|
||||||
def query(self, query, query_type):
|
async def query(self, query, query_type, session, identifiers=None):
|
||||||
try:
|
try:
|
||||||
response = None
|
results = None
|
||||||
|
headers = {
|
||||||
if query_type == "get":
|
"Accept": "application/json",
|
||||||
response = requests.get(self.baseurl + query, headers={"accept":"application/json", "X-Emby-Token": self.token})
|
"X-Emby-Token": self.token
|
||||||
|
}
|
||||||
elif query_type == "post":
|
|
||||||
authorization = (
|
authorization = (
|
||||||
'MediaBrowser , '
|
'MediaBrowser , '
|
||||||
'Client="other", '
|
'Client="other", '
|
||||||
@@ -36,18 +29,33 @@ class Jellyfin():
|
|||||||
'DeviceId="script", '
|
'DeviceId="script", '
|
||||||
'Version="0.0.0"'
|
'Version="0.0.0"'
|
||||||
)
|
)
|
||||||
response = requests.post(self.baseurl + query, headers={"accept":"application/json", "X-Emby-Authorization": authorization, "X-Emby-Token": self.token})
|
headers["X-Emby-Authorization"] = authorization
|
||||||
|
|
||||||
|
if query_type == "get":
|
||||||
|
async with session.get(self.baseurl + query, headers=headers) as response:
|
||||||
|
results = await response.json()
|
||||||
|
|
||||||
|
elif query_type == "post":
|
||||||
|
async with session.post(self.baseurl + query, headers=headers) as response:
|
||||||
|
results = await response.json()
|
||||||
|
|
||||||
|
# append identifiers to results
|
||||||
|
if identifiers:
|
||||||
|
results["Identifiers"] = identifiers
|
||||||
|
return results
|
||||||
|
|
||||||
return response.json()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger(e, 2)
|
logger(f"Jellyfin: Query failed {e}", 2)
|
||||||
logger(response, 2)
|
raise Exception(e)
|
||||||
|
|
||||||
def get_users(self):
|
|
||||||
|
async def get_users(self):
|
||||||
|
try:
|
||||||
users = {}
|
users = {}
|
||||||
|
|
||||||
query = "/Users"
|
query_string = "/Users"
|
||||||
response = self.query(query, "get")
|
async with aiohttp.ClientSession() as session:
|
||||||
|
response = await self.query(query_string, "get", session)
|
||||||
|
|
||||||
# If reponse is not empty
|
# If reponse is not empty
|
||||||
if response:
|
if response:
|
||||||
@@ -55,25 +63,128 @@ class Jellyfin():
|
|||||||
users[user["Name"]] = user["Id"]
|
users[user["Name"]] = user["Id"]
|
||||||
|
|
||||||
return users
|
return users
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Jellyfin: Get users failed {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
def get_jellyfin_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None):
|
|
||||||
users_watched = {}
|
|
||||||
|
|
||||||
for user_name, user_id in users.items():
|
async def get_user_watched(self, user_name, user_id, library_type, library_id, library_title):
|
||||||
|
try:
|
||||||
|
user_name = user_name.lower()
|
||||||
|
user_watched = {}
|
||||||
|
user_watched[user_name] = {}
|
||||||
|
|
||||||
|
logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0)
|
||||||
|
# Movies
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
if library_type == "Movie":
|
||||||
|
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)
|
||||||
|
for movie in watched["Items"]:
|
||||||
|
if movie["UserData"]["Played"] == True:
|
||||||
|
movie_guids = {}
|
||||||
|
movie_guids["title"] = movie["Name"]
|
||||||
|
if "ProviderIds" in movie:
|
||||||
|
# Lowercase movie["ProviderIds"] keys
|
||||||
|
movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()}
|
||||||
|
if "MediaSources" in movie:
|
||||||
|
movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]])
|
||||||
|
user_watched[user_name][library_title].append(movie_guids)
|
||||||
|
|
||||||
|
# TV Shows
|
||||||
|
if library_type == "Series":
|
||||||
|
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_filtered = []
|
||||||
|
for show in watched_shows["Items"]:
|
||||||
|
if "PlayedPercentage" in show["UserData"]:
|
||||||
|
if show["UserData"]["PlayedPercentage"] > 0:
|
||||||
|
watched_shows_filtered.append(show)
|
||||||
|
seasons_tasks = []
|
||||||
|
for show in watched_shows_filtered:
|
||||||
|
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
|
||||||
|
show_guids["title"] = show["Name"]
|
||||||
|
show_guids["locations"] = tuple([show["Path"].split("/")[-1]])
|
||||||
|
show_guids = frozenset(show_guids.items())
|
||||||
|
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())))
|
||||||
|
seasons_tasks.append(task)
|
||||||
|
|
||||||
|
seasons_watched = await asyncio.gather(*seasons_tasks)
|
||||||
|
seasons_watched_filtered = []
|
||||||
|
|
||||||
|
for seasons in seasons_watched:
|
||||||
|
seasons_watched_filtered_dict = {}
|
||||||
|
seasons_watched_filtered_dict["Identifiers"] = seasons["Identifiers"]
|
||||||
|
seasons_watched_filtered_dict["Items"] = []
|
||||||
|
for season in seasons["Items"]:
|
||||||
|
if "PlayedPercentage" in season["UserData"]:
|
||||||
|
if season["UserData"]["PlayedPercentage"] > 0:
|
||||||
|
seasons_watched_filtered_dict["Items"].append(season)
|
||||||
|
|
||||||
|
if seasons_watched_filtered_dict["Items"]:
|
||||||
|
seasons_watched_filtered.append(seasons_watched_filtered_dict)
|
||||||
|
|
||||||
|
episodes_tasks = []
|
||||||
|
for seasons in seasons_watched_filtered:
|
||||||
|
if len(seasons["Items"]) > 0:
|
||||||
|
for season in seasons["Items"]:
|
||||||
|
season_identifiers = dict(seasons["Identifiers"])
|
||||||
|
season_identifiers["season_id"] = season["Id"]
|
||||||
|
season_identifiers["season_name"] = season["Name"]
|
||||||
|
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())))
|
||||||
|
episodes_tasks.append(task)
|
||||||
|
|
||||||
|
watched_episodes = await asyncio.gather(*episodes_tasks)
|
||||||
|
for episodes in watched_episodes:
|
||||||
|
if len(episodes["Items"]) > 0:
|
||||||
|
for episode in episodes["Items"]:
|
||||||
|
if episode["UserData"]["Played"] == True:
|
||||||
|
if "ProviderIds" in episode or "MediaSources" in episode:
|
||||||
|
episode_identifiers = dict(episodes["Identifiers"])
|
||||||
|
show_guids = episode_identifiers["show_guids"]
|
||||||
|
if show_guids not in user_watched[user_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 = {}
|
||||||
|
if "ProviderIds" in episode:
|
||||||
|
episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()}
|
||||||
|
if "MediaSources" in episode:
|
||||||
|
episode_guids["locations"] = tuple([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
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_users_watched(self, user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
|
||||||
|
try:
|
||||||
# Get all libraries
|
# Get all libraries
|
||||||
user_name = user_name.lower()
|
user_name = user_name.lower()
|
||||||
|
tasks_watched = []
|
||||||
|
|
||||||
libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"]
|
tasks_libraries = []
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
for library in libraries:
|
libraries = await self.query(f"/Users/{user_id}/Views", "get", session)
|
||||||
library_title = library["Name"]
|
for library in libraries["Items"]:
|
||||||
library_id = library["Id"]
|
library_id = library["Id"]
|
||||||
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&limit=1", "get")
|
library_title = library["Name"]
|
||||||
|
identifiers = {"library_id": library_id, "library_title": library_title}
|
||||||
|
task = asyncio.ensure_future(self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&limit=1", "get", session, identifiers=identifiers))
|
||||||
|
tasks_libraries.append(task)
|
||||||
|
|
||||||
|
libraries = await asyncio.gather(*tasks_libraries, return_exceptions=True)
|
||||||
|
|
||||||
|
for watched in libraries:
|
||||||
if len(watched["Items"]) == 0:
|
if len(watched["Items"]) == 0:
|
||||||
logger(f"Jellyfin: No watched items found in library {library_title}", 1)
|
|
||||||
continue
|
continue
|
||||||
else:
|
|
||||||
|
library_id = watched["Identifiers"]["library_id"]
|
||||||
|
library_title = watched["Identifiers"]["library_title"]
|
||||||
library_type = watched["Items"][0]["Type"]
|
library_type = watched["Items"][0]["Type"]
|
||||||
|
|
||||||
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
|
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
|
||||||
@@ -82,94 +193,188 @@ class Jellyfin():
|
|||||||
logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1)
|
logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0)
|
# Get watched for user
|
||||||
# Movies
|
task = asyncio.ensure_future(self.get_user_watched(user_name, user_id, library_type, library_id, library_title))
|
||||||
if library_type == "Movie":
|
tasks_watched.append(task)
|
||||||
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
for movie in watched["Items"]:
|
|
||||||
if movie["UserData"]["Played"] == True:
|
|
||||||
if movie["ProviderIds"]:
|
|
||||||
if user_name not in users_watched:
|
|
||||||
users_watched[user_name] = {}
|
|
||||||
if library_title not in users_watched[user_name]:
|
|
||||||
users_watched[user_name][library_title] = []
|
|
||||||
# Lowercase movie["ProviderIds"] keys
|
|
||||||
movie["ProviderIds"] = {k.lower(): v for k, v in movie["ProviderIds"].items()}
|
|
||||||
users_watched[user_name][library_title].append(movie["ProviderIds"])
|
|
||||||
|
|
||||||
# TV Shows
|
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
|
||||||
if library_type == "Episode":
|
return watched
|
||||||
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds", "get")
|
except Exception as e:
|
||||||
watched_shows = [x for x in watched["Items"] if x["Type"] == "Series"]
|
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
for show in watched_shows:
|
|
||||||
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
|
|
||||||
show_guids["title"] = show["Name"]
|
|
||||||
show_guids = frozenset(show_guids.items())
|
|
||||||
seasons = self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
if len(seasons["Items"]) > 0:
|
|
||||||
for season in seasons["Items"]:
|
|
||||||
episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
if len(episodes["Items"]) > 0:
|
|
||||||
for episode in episodes["Items"]:
|
|
||||||
if episode["UserData"]["Played"] == True:
|
|
||||||
if episode["ProviderIds"]:
|
|
||||||
if user_name not in users_watched:
|
|
||||||
users_watched[user_name] = {}
|
|
||||||
if library_title not in users_watched[user_name]:
|
|
||||||
users_watched[user_name][library_title] = {}
|
|
||||||
if show_guids not in users_watched[user_name][library_title]:
|
|
||||||
users_watched[user_name][library_title][show_guids] = {}
|
|
||||||
if season["Name"] not in users_watched[user_name][library_title][show_guids]:
|
|
||||||
users_watched[user_name][library_title][show_guids][season["Name"]] = []
|
|
||||||
|
|
||||||
# Lowercase episode["ProviderIds"] keys
|
async def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None):
|
||||||
episode["ProviderIds"] = {k.lower(): v for k, v in episode["ProviderIds"].items()}
|
try:
|
||||||
users_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"])
|
users_watched = {}
|
||||||
|
watched = []
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
for user_watched in watched:
|
||||||
|
user_watched_temp = combine_watched_dicts(user_watched)
|
||||||
|
for user, user_watched_temp in user_watched_temp.items():
|
||||||
|
if user not in users_watched:
|
||||||
|
users_watched[user] = {}
|
||||||
|
users_watched[user].update(user_watched_temp)
|
||||||
|
|
||||||
return users_watched
|
return users_watched
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
|
|
||||||
|
async def update_user_watched(self, user_name, user_id, library, library_id, videos, dryrun):
|
||||||
|
try:
|
||||||
|
logger(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)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
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)
|
||||||
|
for jellyfin_video in jellyfin_search["Items"]:
|
||||||
|
movie_found = False
|
||||||
|
|
||||||
|
if "MediaSources" in jellyfin_video:
|
||||||
|
for movie_location in jellyfin_video["MediaSources"]:
|
||||||
|
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
|
||||||
|
break
|
||||||
|
|
||||||
|
if movie_found:
|
||||||
|
jellyfin_video_id = jellyfin_video["Id"]
|
||||||
|
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:
|
||||||
|
logger(f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TV Shows
|
||||||
|
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_shows = [x for x in jellyfin_search["Items"]]
|
||||||
|
|
||||||
|
for jellyfin_show in jellyfin_shows:
|
||||||
|
show_found = False
|
||||||
|
|
||||||
|
if "Path" in jellyfin_show:
|
||||||
|
if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]:
|
||||||
|
show_found = True
|
||||||
|
|
||||||
|
if not show_found:
|
||||||
|
for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items():
|
||||||
|
if show_provider_source.lower() in videos_shows_ids:
|
||||||
|
if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]:
|
||||||
|
show_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if show_found:
|
||||||
|
logger(f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", 1)
|
||||||
|
jellyfin_show_id = jellyfin_show["Id"]
|
||||||
|
jellyfin_episodes = await self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get", session)
|
||||||
|
|
||||||
|
for jellyfin_episode in jellyfin_episodes["Items"]:
|
||||||
|
episode_found = False
|
||||||
|
|
||||||
|
if "MediaSources" in jellyfin_episode:
|
||||||
|
for episode_location in jellyfin_episode["MediaSources"]:
|
||||||
|
if episode_location["Path"].split("/")[-1] in videos_episodes_ids["locations"]:
|
||||||
|
episode_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not episode_found:
|
||||||
|
for episode_provider_source, 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
|
||||||
|
break
|
||||||
|
|
||||||
|
if episode_found:
|
||||||
|
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"
|
||||||
|
if not dryrun:
|
||||||
|
logger(f"Marked {msg}", 0)
|
||||||
|
await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post", session)
|
||||||
|
else:
|
||||||
|
logger(f"Dryrun {msg}", 0)
|
||||||
|
else:
|
||||||
|
logger(f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 1)
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
logger(f"Jellyfin: No videos to mark as watched for {user_name} in library {library}", 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Jellyfin: Error updating watched for {user_name} in library {library}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
|
||||||
|
try:
|
||||||
|
tasks = []
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
for user, libraries in watched_list.items():
|
for user, libraries in watched_list.items():
|
||||||
if user_mapping:
|
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
|
||||||
user_other = None
|
user_other = None
|
||||||
|
user_name = None
|
||||||
|
if user_mapping:
|
||||||
if user in user_mapping.keys():
|
if user in user_mapping.keys():
|
||||||
user_other = user_mapping[user]
|
user_other = user_mapping[user]
|
||||||
elif user in user_mapping.values():
|
elif user in user_mapping.values():
|
||||||
user_other = search_mapping(user_mapping, user)
|
user_other = search_mapping(user_mapping, user)
|
||||||
|
|
||||||
if user_other:
|
|
||||||
logger(f"Swapping user {user} with {user_other}", 1)
|
|
||||||
user = user_other
|
|
||||||
|
|
||||||
user_id = None
|
user_id = None
|
||||||
for key in self.users.keys():
|
for key in self.users.keys():
|
||||||
if user.lower() == key.lower():
|
if user.lower() == key.lower():
|
||||||
user_id = self.users[key]
|
user_id = self.users[key]
|
||||||
|
user_name = key
|
||||||
|
break
|
||||||
|
elif user_other and user_other.lower() == key.lower():
|
||||||
|
user_id = self.users[key]
|
||||||
|
user_name = key
|
||||||
break
|
break
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
logger(f"{user} not found in Jellyfin", 2)
|
logger(f"{user} {user_other} not found in Jellyfin", 2)
|
||||||
break
|
continue
|
||||||
|
|
||||||
jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"]
|
jellyfin_libraries = await self.query(f"/Users/{user_id}/Views", "get", session)
|
||||||
|
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
|
||||||
|
|
||||||
for library, videos in libraries.items():
|
for library, videos in libraries.items():
|
||||||
if library_mapping:
|
|
||||||
library_other = None
|
library_other = None
|
||||||
|
if library_mapping:
|
||||||
if library in library_mapping.keys():
|
if library in library_mapping.keys():
|
||||||
library_other = library_mapping[library]
|
library_other = library_mapping[library]
|
||||||
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_other:
|
|
||||||
logger(f"Swapping library {library} with {library_other}", 1)
|
|
||||||
library = library_other
|
|
||||||
|
|
||||||
if library not in [x["Name"] for x in jellyfin_libraries]:
|
if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]:
|
||||||
logger(f"{library} not found in Jellyfin", 2)
|
if library_other:
|
||||||
|
if library_other.lower() in [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
|
||||||
|
else:
|
||||||
|
logger(f"Jellyfin: Library {library} or {library_other} not found in library list", 2)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger(f"Jellyfin: Library {library} not found in library list", 2)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
library_id = None
|
library_id = None
|
||||||
@@ -179,59 +384,10 @@ class Jellyfin():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if library_id:
|
if library_id:
|
||||||
logger(f"Jellyfin: Updating watched for {user} in library {library}", 1)
|
task = self.update_user_watched(user_name, user_id, library, library_id, videos, dryrun)
|
||||||
library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get")
|
tasks.append(task)
|
||||||
library_type = library_search["Items"][0]["Type"]
|
|
||||||
|
|
||||||
# Movies
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
if library_type == "Movie":
|
except Exception as e:
|
||||||
_, _, videos_movies_ids = generate_library_guids_dict(videos, 2)
|
logger(f"Jellyfin: Error updating watched", 2)
|
||||||
|
raise Exception(e)
|
||||||
jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
for jellyfin_video in jellyfin_search["Items"]:
|
|
||||||
if str_to_bool(jellyfin_video["UserData"]["Played"]) == False:
|
|
||||||
jellyfin_video_id = jellyfin_video["Id"]
|
|
||||||
|
|
||||||
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()]:
|
|
||||||
msg = f"{jellyfin_video['Name']} as watched for {user} in {library} for Jellyfin"
|
|
||||||
if not dryrun:
|
|
||||||
logger(f"Marking {msg}", 0)
|
|
||||||
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post")
|
|
||||||
else:
|
|
||||||
logger(f"Dryrun {msg}", 0)
|
|
||||||
break
|
|
||||||
|
|
||||||
# TV Shows
|
|
||||||
if library_type == "Episode":
|
|
||||||
videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3)
|
|
||||||
|
|
||||||
jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
jellyfin_shows = [x for x in jellyfin_search["Items"]]
|
|
||||||
|
|
||||||
for jellyfin_show in jellyfin_shows:
|
|
||||||
show_found = False
|
|
||||||
for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items():
|
|
||||||
if show_provider_source.lower() in videos_shows_ids:
|
|
||||||
if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]:
|
|
||||||
show_found = True
|
|
||||||
jellyfin_show_id = jellyfin_show["Id"]
|
|
||||||
jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds", "get")
|
|
||||||
for jellyfin_episode in jellyfin_episodes["Items"]:
|
|
||||||
if str_to_bool(jellyfin_episode["UserData"]["Played"]) == False:
|
|
||||||
jellyfin_episode_id = jellyfin_episode["Id"]
|
|
||||||
|
|
||||||
for episode_provider_source, episode_provider_id in jellyfin_episode["ProviderIds"].items():
|
|
||||||
if episode_provider_source.lower() in videos_episode_ids:
|
|
||||||
if episode_provider_id.lower() in videos_episode_ids[episode_provider_source.lower()]:
|
|
||||||
msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']} as watched for {user} in {library} for Jellyfin"
|
|
||||||
if not dryrun:
|
|
||||||
logger(f"Marked {msg}", 0)
|
|
||||||
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post")
|
|
||||||
else:
|
|
||||||
logger(f"Dryrun {msg}", 0)
|
|
||||||
break
|
|
||||||
|
|
||||||
if show_found:
|
|
||||||
break
|
|
||||||
|
|||||||
450
src/main.py
Normal file
450
src/main.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
import copy, os, traceback, json, asyncio
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
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.plex import Plex
|
||||||
|
from src.jellyfin import Jellyfin
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
|
||||||
|
modified_watched_list_1 = copy.deepcopy(watched_list_1)
|
||||||
|
|
||||||
|
# remove entries from plex_watched that are in jellyfin_watched
|
||||||
|
for user_1 in watched_list_1:
|
||||||
|
user_other = None
|
||||||
|
if user_mapping:
|
||||||
|
user_other = search_mapping(user_mapping, user_1)
|
||||||
|
if user_1 in modified_watched_list_1:
|
||||||
|
if user_1 in watched_list_2:
|
||||||
|
user_2 = user_1
|
||||||
|
elif user_other in watched_list_2:
|
||||||
|
user_2 = user_other
|
||||||
|
else:
|
||||||
|
logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for library_1 in watched_list_1[user_1]:
|
||||||
|
library_other = None
|
||||||
|
if library_mapping:
|
||||||
|
library_other = search_mapping(library_mapping, library_1)
|
||||||
|
if library_1 in modified_watched_list_1[user_1]:
|
||||||
|
if library_1 in watched_list_2[user_2]:
|
||||||
|
library_2 = library_1
|
||||||
|
elif library_other in watched_list_2[user_2]:
|
||||||
|
library_2 = library_other
|
||||||
|
else:
|
||||||
|
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
_, episode_watched_list_2_keys_dict, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2])
|
||||||
|
|
||||||
|
# Movies
|
||||||
|
if isinstance(watched_list_1[user_1][library_1], list):
|
||||||
|
for movie in watched_list_1[user_1][library_1]:
|
||||||
|
movie_found = False
|
||||||
|
for movie_key, movie_value in movie.items():
|
||||||
|
if movie_key == "locations":
|
||||||
|
if "locations" in movies_watched_list_2_keys_dict.keys():
|
||||||
|
for location in movie_value:
|
||||||
|
if location in movies_watched_list_2_keys_dict["locations"]:
|
||||||
|
movie_found = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if movie_key in movies_watched_list_2_keys_dict.keys():
|
||||||
|
if movie_value in movies_watched_list_2_keys_dict[movie_key]:
|
||||||
|
movie_found = True
|
||||||
|
|
||||||
|
if movie_found:
|
||||||
|
logger(f"Removing {movie} from {library_1}", 3)
|
||||||
|
modified_watched_list_1[user_1][library_1].remove(movie)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# TV Shows
|
||||||
|
elif isinstance(watched_list_1[user_1][library_1], dict):
|
||||||
|
# Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1
|
||||||
|
|
||||||
|
for show_key_1 in watched_list_1[user_1][library_1].keys():
|
||||||
|
show_key_dict = dict(show_key_1)
|
||||||
|
for season in watched_list_1[user_1][library_1][show_key_1]:
|
||||||
|
for episode in watched_list_1[user_1][library_1][show_key_1][season]:
|
||||||
|
episode_found = False
|
||||||
|
for episode_key, episode_value in episode.items():
|
||||||
|
# If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
|
||||||
|
if episode_key == "locations":
|
||||||
|
if "locations" in episode_watched_list_2_keys_dict.keys():
|
||||||
|
for location in episode_value:
|
||||||
|
if location in episode_watched_list_2_keys_dict["locations"]:
|
||||||
|
episode_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
if episode_key in episode_watched_list_2_keys_dict.keys():
|
||||||
|
if episode_value in episode_watched_list_2_keys_dict[episode_key]:
|
||||||
|
episode_found = True
|
||||||
|
|
||||||
|
if episode_found:
|
||||||
|
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
|
||||||
|
logger(f"Removing {episode} from {show_key_dict['title']}", 3)
|
||||||
|
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Remove empty seasons
|
||||||
|
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
|
||||||
|
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
|
||||||
|
logger(f"Removing {season} from {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 len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
|
||||||
|
if show_key_1 in modified_watched_list_1[user_1][library_1]:
|
||||||
|
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
|
||||||
|
del modified_watched_list_1[user_1][library_1][show_key_1]
|
||||||
|
|
||||||
|
for user_1 in watched_list_1:
|
||||||
|
for library_1 in watched_list_1[user_1]:
|
||||||
|
if library_1 in modified_watched_list_1[user_1]:
|
||||||
|
# If library is empty then remove it
|
||||||
|
if len(modified_watched_list_1[user_1][library_1]) == 0:
|
||||||
|
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
|
||||||
|
del modified_watched_list_1[user_1][library_1]
|
||||||
|
|
||||||
|
if user_1 in modified_watched_list_1:
|
||||||
|
# If user is empty delete user
|
||||||
|
if len(modified_watched_list_1[user_1]) == 0:
|
||||||
|
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
|
||||||
|
del modified_watched_list_1[user_1]
|
||||||
|
|
||||||
|
return modified_watched_list_1
|
||||||
|
|
||||||
|
def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blacklist_library_type: str, whitelist_library_type: str, blacklist_users: str, whitelist_users: str, library_mapping=None, user_mapping=None):
|
||||||
|
if blacklist_library:
|
||||||
|
if len(blacklist_library) > 0:
|
||||||
|
blacklist_library = blacklist_library.split(",")
|
||||||
|
blacklist_library = [x.strip() for x in blacklist_library]
|
||||||
|
if library_mapping:
|
||||||
|
temp_library = []
|
||||||
|
for library in blacklist_library:
|
||||||
|
library_other = search_mapping(library_mapping, library)
|
||||||
|
if library_other:
|
||||||
|
temp_library.append(library_other)
|
||||||
|
|
||||||
|
blacklist_library = blacklist_library + temp_library
|
||||||
|
else:
|
||||||
|
blacklist_library = []
|
||||||
|
logger(f"Blacklist Library: {blacklist_library}", 1)
|
||||||
|
|
||||||
|
if whitelist_library:
|
||||||
|
if len(whitelist_library) > 0:
|
||||||
|
whitelist_library = whitelist_library.split(",")
|
||||||
|
whitelist_library = [x.strip() for x in whitelist_library]
|
||||||
|
if library_mapping:
|
||||||
|
temp_library = []
|
||||||
|
for library in whitelist_library:
|
||||||
|
library_other = search_mapping(library_mapping, library)
|
||||||
|
if library_other:
|
||||||
|
temp_library.append(library_other)
|
||||||
|
|
||||||
|
whitelist_library = whitelist_library + temp_library
|
||||||
|
else:
|
||||||
|
whitelist_library = []
|
||||||
|
logger(f"Whitelist Library: {whitelist_library}", 1)
|
||||||
|
|
||||||
|
if blacklist_library_type:
|
||||||
|
if len(blacklist_library_type) > 0:
|
||||||
|
blacklist_library_type = blacklist_library_type.split(",")
|
||||||
|
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
|
||||||
|
else:
|
||||||
|
blacklist_library_type = []
|
||||||
|
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
|
||||||
|
|
||||||
|
if whitelist_library_type:
|
||||||
|
if len(whitelist_library_type) > 0:
|
||||||
|
whitelist_library_type = whitelist_library_type.split(",")
|
||||||
|
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
|
||||||
|
else:
|
||||||
|
whitelist_library_type = []
|
||||||
|
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
|
||||||
|
|
||||||
|
if blacklist_users:
|
||||||
|
if len(blacklist_users) > 0:
|
||||||
|
blacklist_users = blacklist_users.split(",")
|
||||||
|
blacklist_users = [x.lower().strip() for x in blacklist_users]
|
||||||
|
if user_mapping:
|
||||||
|
temp_users = []
|
||||||
|
for user in blacklist_users:
|
||||||
|
user_other = search_mapping(user_mapping, user)
|
||||||
|
if user_other:
|
||||||
|
temp_users.append(user_other)
|
||||||
|
|
||||||
|
blacklist_users = blacklist_users + temp_users
|
||||||
|
else:
|
||||||
|
blacklist_users = []
|
||||||
|
logger(f"Blacklist Users: {blacklist_users}", 1)
|
||||||
|
|
||||||
|
if whitelist_users:
|
||||||
|
if len(whitelist_users) > 0:
|
||||||
|
whitelist_users = whitelist_users.split(",")
|
||||||
|
whitelist_users = [x.lower().strip() for x in whitelist_users]
|
||||||
|
if user_mapping:
|
||||||
|
temp_users = []
|
||||||
|
for user in whitelist_users:
|
||||||
|
user_other = search_mapping(user_mapping, user)
|
||||||
|
if user_other:
|
||||||
|
temp_users.append(user_other)
|
||||||
|
|
||||||
|
whitelist_users = whitelist_users + temp_users
|
||||||
|
else:
|
||||||
|
whitelist_users = []
|
||||||
|
else:
|
||||||
|
whitelist_users = []
|
||||||
|
logger(f"Whitelist Users: {whitelist_users}", 1)
|
||||||
|
|
||||||
|
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
|
||||||
|
|
||||||
|
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
|
||||||
|
|
||||||
|
# generate list of users from server 1 and server 2
|
||||||
|
server_1_type = server_1[0]
|
||||||
|
server_1_connection = server_1[1]
|
||||||
|
server_2_type = server_2[0]
|
||||||
|
server_2_connection = server_2[1]
|
||||||
|
print(f"Server 1: {server_1_type} {server_1_connection}")
|
||||||
|
print(f"Server 2: {server_2_type} {server_2_connection}")
|
||||||
|
|
||||||
|
server_1_users = []
|
||||||
|
if server_1_type == "plex":
|
||||||
|
server_1_users = [ x.title.lower() for x in server_1_connection.users ]
|
||||||
|
elif server_1_type == "jellyfin":
|
||||||
|
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ]
|
||||||
|
|
||||||
|
server_2_users = []
|
||||||
|
if server_2_type == "plex":
|
||||||
|
server_2_users = [ x.title.lower() for x in server_2_connection.users ]
|
||||||
|
elif server_2_type == "jellyfin":
|
||||||
|
server_2_users = [ key.lower() for key in server_2_connection.users.keys() ]
|
||||||
|
|
||||||
|
|
||||||
|
# combined list of overlapping users from plex and jellyfin
|
||||||
|
users = {}
|
||||||
|
|
||||||
|
for server_1_user in server_1_users:
|
||||||
|
if user_mapping:
|
||||||
|
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
|
||||||
|
if jellyfin_plex_mapped_user:
|
||||||
|
users[server_1_user] = jellyfin_plex_mapped_user
|
||||||
|
continue
|
||||||
|
|
||||||
|
if server_1_user in server_2_users:
|
||||||
|
users[server_1_user] = server_1_user
|
||||||
|
|
||||||
|
for server_2_user in server_2_users:
|
||||||
|
if user_mapping:
|
||||||
|
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
|
||||||
|
if plex_jellyfin_mapped_user:
|
||||||
|
users[plex_jellyfin_mapped_user] = server_2_user
|
||||||
|
continue
|
||||||
|
|
||||||
|
if server_2_user in server_1_users:
|
||||||
|
users[server_2_user] = server_2_user
|
||||||
|
|
||||||
|
logger(f"User list that exist on both servers {users}", 1)
|
||||||
|
|
||||||
|
users_filtered = {}
|
||||||
|
for user in users:
|
||||||
|
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
|
||||||
|
if len(whitelist_users) > 0:
|
||||||
|
if user not in whitelist_users and users[user] not in whitelist_users:
|
||||||
|
logger(f"{user} or {users[user]} is not in whitelist", 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user not in blacklist_users and users[user] not in blacklist_users:
|
||||||
|
users_filtered[user] = users[user]
|
||||||
|
|
||||||
|
logger(f"Filtered user list {users_filtered}", 1)
|
||||||
|
|
||||||
|
if server_1_type == "plex":
|
||||||
|
output_server_1_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():
|
||||||
|
output_server_1_users.append(plex_user)
|
||||||
|
elif server_1_type == "jellyfin":
|
||||||
|
output_server_1_users = {}
|
||||||
|
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():
|
||||||
|
output_server_1_users[jellyfin_user] = jellyfin_id
|
||||||
|
|
||||||
|
if server_2_type == "plex":
|
||||||
|
output_server_2_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():
|
||||||
|
output_server_2_users.append(plex_user)
|
||||||
|
elif server_2_type == "jellyfin":
|
||||||
|
output_server_2_users = {}
|
||||||
|
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():
|
||||||
|
output_server_2_users[jellyfin_user] = jellyfin_id
|
||||||
|
|
||||||
|
if len(output_server_1_users) == 0:
|
||||||
|
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}")
|
||||||
|
|
||||||
|
if len(output_server_2_users) == 0:
|
||||||
|
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 2 users: {output_server_2_users}", 1)
|
||||||
|
|
||||||
|
return output_server_1_users, output_server_2_users
|
||||||
|
|
||||||
|
def generate_server_connections():
|
||||||
|
servers = []
|
||||||
|
|
||||||
|
plex_baseurl = os.getenv("PLEX_BASEURL", None)
|
||||||
|
plex_token = os.getenv("PLEX_TOKEN", None)
|
||||||
|
plex_username = os.getenv("PLEX_USERNAME", None)
|
||||||
|
plex_password = os.getenv("PLEX_PASSWORD", None)
|
||||||
|
plex_servername = os.getenv("PLEX_SERVERNAME", None)
|
||||||
|
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
|
||||||
|
|
||||||
|
if plex_baseurl and plex_token:
|
||||||
|
plex_baseurl = plex_baseurl.split(",")
|
||||||
|
plex_token = plex_token.split(",")
|
||||||
|
|
||||||
|
if len(plex_baseurl) != len(plex_token):
|
||||||
|
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries")
|
||||||
|
|
||||||
|
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)))
|
||||||
|
|
||||||
|
if plex_username and plex_password and plex_servername:
|
||||||
|
plex_username = plex_username.split(",")
|
||||||
|
plex_password = plex_password.split(",")
|
||||||
|
plex_servername = plex_servername.split(",")
|
||||||
|
|
||||||
|
if len(plex_username) != len(plex_password) or len(plex_username) != len(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):
|
||||||
|
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_token = os.getenv("JELLYFIN_TOKEN", None)
|
||||||
|
|
||||||
|
if jellyfin_baseurl and jellyfin_token:
|
||||||
|
jellyfin_baseurl = jellyfin_baseurl.split(",")
|
||||||
|
jellyfin_token = jellyfin_token.split(",")
|
||||||
|
|
||||||
|
if len(jellyfin_baseurl) != len(jellyfin_token):
|
||||||
|
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries")
|
||||||
|
|
||||||
|
for i, baseurl in enumerate(jellyfin_baseurl):
|
||||||
|
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
|
||||||
|
|
||||||
|
return servers
|
||||||
|
|
||||||
|
|
||||||
|
def main_loop():
|
||||||
|
logfile = os.getenv("LOGFILE","log.log")
|
||||||
|
# Delete logfile if it exists
|
||||||
|
if os.path.exists(logfile):
|
||||||
|
os.remove(logfile)
|
||||||
|
|
||||||
|
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
|
||||||
|
logger(f"Dryrun: {dryrun}", 1)
|
||||||
|
|
||||||
|
user_mapping = os.getenv("USER_MAPPING")
|
||||||
|
if user_mapping:
|
||||||
|
user_mapping = json.loads(user_mapping.lower())
|
||||||
|
logger(f"User Mapping: {user_mapping}", 1)
|
||||||
|
|
||||||
|
library_mapping = os.getenv("LIBRARY_MAPPING")
|
||||||
|
if library_mapping:
|
||||||
|
library_mapping = json.loads(library_mapping)
|
||||||
|
logger(f"Library Mapping: {library_mapping}", 1)
|
||||||
|
|
||||||
|
# Create (black/white)lists
|
||||||
|
logger("Creating (black/white)lists", 1)
|
||||||
|
blacklist_library = os.getenv("BLACKLIST_LIBRARY", None)
|
||||||
|
whitelist_library = os.getenv("WHITELIST_LIBRARY", None)
|
||||||
|
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None)
|
||||||
|
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None)
|
||||||
|
blacklist_users = os.getenv("BLACKLIST_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)
|
||||||
|
|
||||||
|
# Create server connections
|
||||||
|
logger("Creating server connections", 1)
|
||||||
|
servers = generate_server_connections()
|
||||||
|
|
||||||
|
for server_1 in servers:
|
||||||
|
# If server is the final server in the list, then we are done with the loop
|
||||||
|
if server_1 == servers[-1]:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Start server_2 at the next server in the list
|
||||||
|
for server_2 in servers[servers.index(server_1) + 1:]:
|
||||||
|
|
||||||
|
server_1_connection = server_1[1]
|
||||||
|
server_2_connection = server_2[1]
|
||||||
|
|
||||||
|
# Create users list
|
||||||
|
logger("Creating users list", 1)
|
||||||
|
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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))
|
||||||
|
logger("Finished creating watched list server 2", 1)
|
||||||
|
logger(f"Server 1 watched: {server_1_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)
|
||||||
|
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping)
|
||||||
|
|
||||||
|
logger("Cleaning Server 2 Watched", 1)
|
||||||
|
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(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))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
|
||||||
|
times = []
|
||||||
|
while(True):
|
||||||
|
try:
|
||||||
|
start = perf_counter()
|
||||||
|
main_loop()
|
||||||
|
end = perf_counter()
|
||||||
|
times.append(end - start)
|
||||||
|
|
||||||
|
logger(f"Looping in {sleep_duration}")
|
||||||
|
sleep(sleep_duration)
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
if isinstance(error, list):
|
||||||
|
for message in error:
|
||||||
|
logger(message, log_type=2)
|
||||||
|
else:
|
||||||
|
logger(error, log_type=2)
|
||||||
|
|
||||||
|
|
||||||
|
logger(traceback.format_exc(), 2)
|
||||||
|
logger(f"Retrying in {sleep_duration}", log_type=0)
|
||||||
|
sleep(sleep_duration)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if len(times) > 0:
|
||||||
|
logger(f"Average time: {sum(times) / len(times)}", 0)
|
||||||
|
logger("Exiting", log_type=0)
|
||||||
|
os._exit(0)
|
||||||
296
src/plex.py
296
src/plex.py
@@ -1,34 +1,32 @@
|
|||||||
import re, os
|
import re, requests
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict
|
|
||||||
from plexapi.server import PlexServer
|
from plexapi.server import PlexServer
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
|
|
||||||
load_dotenv(override=True)
|
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor
|
||||||
|
|
||||||
plex_baseurl = os.getenv("PLEX_BASEURL")
|
|
||||||
plex_token = os.getenv("PLEX_TOKEN")
|
|
||||||
username = os.getenv("PLEX_USERNAME")
|
|
||||||
password = os.getenv("PLEX_PASSWORD")
|
|
||||||
servername = os.getenv("PLEX_SERVERNAME")
|
|
||||||
|
|
||||||
# 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):
|
def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None, ssl_bypass=False):
|
||||||
self.baseurl = plex_baseurl
|
self.baseurl = baseurl
|
||||||
self.token = plex_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.plex_login()
|
self.plex = self.login(ssl_bypass)
|
||||||
self.admin_user = self.plex.myPlexAccount()
|
self.admin_user = self.plex.myPlexAccount()
|
||||||
self.users = self.get_plex_users()
|
self.users = self.get_users()
|
||||||
|
|
||||||
def plex_login(self):
|
def login(self, ssl_bypass=False):
|
||||||
try:
|
try:
|
||||||
if self.baseurl and self.token:
|
if self.baseurl and self.token:
|
||||||
# Login via token
|
# Login via token
|
||||||
|
if ssl_bypass:
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
plex = PlexServer(self.baseurl, self.token, session=session)
|
||||||
|
else:
|
||||||
plex = PlexServer(self.baseurl, self.token)
|
plex = PlexServer(self.baseurl, self.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
|
||||||
@@ -44,47 +42,60 @@ class Plex:
|
|||||||
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
|
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
|
||||||
else:
|
else:
|
||||||
logger(f"Plex: Failed to login, Error: {e}", 2)
|
logger(f"Plex: Failed to login, Error: {e}", 2)
|
||||||
return None
|
raise Exception(e)
|
||||||
|
|
||||||
|
|
||||||
def get_plex_users(self):
|
def get_users(self):
|
||||||
|
try:
|
||||||
users = self.plex.myPlexAccount().users()
|
users = self.plex.myPlexAccount().users()
|
||||||
|
|
||||||
# append self to users
|
# append self to users
|
||||||
users.append(self.plex.myPlexAccount())
|
users.append(self.plex.myPlexAccount())
|
||||||
|
|
||||||
return users
|
return users
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Plex: Failed to get users, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
def get_plex_user_watched(self, user, library):
|
|
||||||
if self.admin_user == user:
|
|
||||||
user_plex = self.plex
|
|
||||||
else:
|
|
||||||
user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier))
|
|
||||||
|
|
||||||
watched = None
|
def get_user_watched(self, 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":
|
if library.type == "movie":
|
||||||
watched = []
|
user_watched[user_name][library.title] = []
|
||||||
|
|
||||||
library_videos = user_plex.library.section(library.title)
|
library_videos = user_plex.library.section(library.title)
|
||||||
for video in library_videos.search(unmatched=False, unwatched=False):
|
for video in library_videos.search(unwatched=False):
|
||||||
guids = {}
|
movie_guids = {}
|
||||||
for guid in video.guids:
|
for guid in video.guids:
|
||||||
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
|
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
|
||||||
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
||||||
guids[guid_source] = guid_id
|
movie_guids[guid_source] = guid_id
|
||||||
watched.append(guids)
|
|
||||||
|
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":
|
elif library.type == "show":
|
||||||
watched = {}
|
user_watched[user_name][library.title] = {}
|
||||||
|
|
||||||
library_videos = user_plex.library.section(library.title)
|
library_videos = user_plex.library.section(library.title)
|
||||||
for show in library_videos.search(unmatched=False, unwatched=False):
|
for show in library_videos.search(unwatched=False):
|
||||||
show_guids = {}
|
show_guids = {}
|
||||||
for show_guid in show.guids:
|
for show_guid in show.guids:
|
||||||
show_guids["title"] = show.title
|
|
||||||
# Extract after :// from guid.id
|
# Extract after :// from guid.id
|
||||||
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
||||||
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
||||||
show_guids[show_guid_source] = show_guid_id
|
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())
|
show_guids = frozenset(show_guids.items())
|
||||||
|
|
||||||
for season in show.seasons():
|
for season in show.seasons():
|
||||||
@@ -98,24 +109,38 @@ class Plex:
|
|||||||
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
||||||
episode_guids_temp[guid_source] = guid_id
|
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)
|
episode_guids.append(episode_guids_temp)
|
||||||
|
|
||||||
if episode_guids:
|
if episode_guids:
|
||||||
# append show, season, episode
|
# append show, season, episode
|
||||||
if show_guids not in watched:
|
if show_guids not in user_watched[user_name][library.title]:
|
||||||
watched[show_guids] = {}
|
user_watched[user_name][library.title][show_guids] = {}
|
||||||
if season.title not in watched[show_guids]:
|
if season.title not in user_watched[user_name][library.title][show_guids]:
|
||||||
watched[show_guids][season.title] = {}
|
user_watched[user_name][library.title][show_guids][season.title] = {}
|
||||||
watched[show_guids][season.title] = episode_guids
|
user_watched[user_name][library.title][show_guids][season.title] = episode_guids
|
||||||
|
|
||||||
return watched
|
|
||||||
|
|
||||||
def get_plex_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
|
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:
|
||||||
# Get all libraries
|
# Get all libraries
|
||||||
libraries = self.plex.library.sections()
|
|
||||||
users_watched = {}
|
users_watched = {}
|
||||||
|
args = []
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if self.admin_user == user:
|
||||||
|
user_plex = self.plex
|
||||||
|
else:
|
||||||
|
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
|
||||||
|
|
||||||
|
libraries = user_plex.library.sections()
|
||||||
|
|
||||||
# for not in blacklist
|
|
||||||
for library in libraries:
|
for library in libraries:
|
||||||
library_title = library.title
|
library_title = library.title
|
||||||
library_type = library.type
|
library_type = library.type
|
||||||
@@ -126,89 +151,66 @@ class Plex:
|
|||||||
logger(f"Plex: Skipping library {library_title} {skip_reason}", 1)
|
logger(f"Plex: Skipping library {library_title} {skip_reason}", 1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for user in users:
|
args.append([self.get_user_watched, user, user_plex, library])
|
||||||
logger(f"Plex: Generating watched for {user.title} in library {library_title}", 0)
|
|
||||||
user_name = user.title.lower()
|
for user_watched in future_thread_executor(args):
|
||||||
watched = self.get_plex_user_watched(user, library)
|
for user, user_watched_temp in user_watched.items():
|
||||||
if watched:
|
if user not in users_watched:
|
||||||
if user_name not in users_watched:
|
users_watched[user] = {}
|
||||||
users_watched[user_name] = {}
|
users_watched[user].update(user_watched_temp)
|
||||||
if library_title not in users_watched[user_name]:
|
|
||||||
users_watched[user_name][library_title] = []
|
|
||||||
users_watched[user_name][library_title] = watched
|
|
||||||
|
|
||||||
return users_watched
|
return users_watched
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Plex: Failed to get watched, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
|
|
||||||
for user, libraries in watched_list.items():
|
|
||||||
if user_mapping:
|
|
||||||
user_other = None
|
|
||||||
|
|
||||||
if user in user_mapping.keys():
|
def update_user_watched (self, user, user_plex, library, videos, dryrun):
|
||||||
user_other = user_mapping[user]
|
try:
|
||||||
elif user in user_mapping.values():
|
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
|
||||||
user_other = search_mapping(user_mapping, user)
|
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)
|
||||||
|
|
||||||
if user_other:
|
library_videos = user_plex.library.section(library)
|
||||||
logger(f"Swapping user {user} with {user_other}", 1)
|
if videos_movies_ids:
|
||||||
user = user_other
|
for movies_search in library_videos.search(unwatched=True):
|
||||||
|
movie_found = False
|
||||||
for index, value in enumerate(self.users):
|
for movie_location in movies_search.locations:
|
||||||
if user.lower() == value.title.lower():
|
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
|
||||||
user = self.users[index]
|
movie_found = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.admin_user == user:
|
if not movie_found:
|
||||||
user_plex = self.plex
|
|
||||||
else:
|
|
||||||
user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier))
|
|
||||||
|
|
||||||
for library, videos in libraries.items():
|
|
||||||
if library_mapping:
|
|
||||||
library_other = None
|
|
||||||
|
|
||||||
if library in library_mapping.keys():
|
|
||||||
library_other = library_mapping[library]
|
|
||||||
elif library in library_mapping.values():
|
|
||||||
library_other = search_mapping(library_mapping, library)
|
|
||||||
|
|
||||||
if library_other:
|
|
||||||
logger(f"Swapping library {library} with {library_other}", 1)
|
|
||||||
library = library_other
|
|
||||||
|
|
||||||
# if library in plex library list
|
|
||||||
library_list = user_plex.library.sections()
|
|
||||||
if library.lower() not in [x.title.lower() for x in library_list]:
|
|
||||||
logger(f"Library {library} not found in Plex library list", 2)
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
|
|
||||||
library_videos = user_plex.library.section(library)
|
|
||||||
|
|
||||||
if library_videos.type == "movie":
|
|
||||||
_, _, videos_movies_ids = generate_library_guids_dict(videos, 2)
|
|
||||||
for movies_search in library_videos.search(unmatched=False, unwatched=True):
|
|
||||||
for movie_guid in movies_search.guids:
|
for movie_guid in movies_search.guids:
|
||||||
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
|
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
|
||||||
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
|
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 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_source in videos_movies_ids.keys():
|
||||||
if movie_guid_id in videos_movies_ids[movie_guid_source]:
|
if movie_guid_id in videos_movies_ids[movie_guid_source]:
|
||||||
if movies_search.viewCount == 0:
|
movie_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if movie_found:
|
||||||
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
|
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
logger(f"Marked {msg}", 0)
|
logger(f"Marked {msg}", 0)
|
||||||
movies_search.markWatched()
|
movies_search.markWatched()
|
||||||
else:
|
else:
|
||||||
logger(f"Dryrun {msg}", 0)
|
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
|
break
|
||||||
|
|
||||||
|
if not show_found:
|
||||||
elif library_videos.type == "show":
|
|
||||||
videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3)
|
|
||||||
|
|
||||||
for show_search in library_videos.search(unmatched=False, unwatched=True):
|
|
||||||
show_found = False
|
|
||||||
for show_guid in show_search.guids:
|
for show_guid in show_search.guids:
|
||||||
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
||||||
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
||||||
@@ -217,22 +219,100 @@ class Plex:
|
|||||||
if show_guid_source in videos_shows_ids.keys():
|
if show_guid_source in videos_shows_ids.keys():
|
||||||
if show_guid_id in videos_shows_ids[show_guid_source]:
|
if show_guid_id in videos_shows_ids[show_guid_source]:
|
||||||
show_found = True
|
show_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if show_found:
|
||||||
for episode_search in show_search.episodes():
|
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:
|
for episode_guid in episode_search.guids:
|
||||||
episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower()
|
episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower()
|
||||||
episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1)
|
episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1)
|
||||||
|
|
||||||
# If episode provider source and episode provider id are in videos_episode_ids exactly, then the episode is in the list
|
# If episode 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_episode_ids.keys():
|
if episode_guid_source in videos_episodes_ids.keys():
|
||||||
if episode_guid_id in videos_episode_ids[episode_guid_source]:
|
if episode_guid_id in videos_episodes_ids[episode_guid_source]:
|
||||||
if episode_search.viewCount == 0:
|
episode_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if episode_found:
|
||||||
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
logger(f"Marked {msg}", 0)
|
logger(f"Marked {msg}", 0)
|
||||||
episode_search.markWatched()
|
episode_search.markWatched()
|
||||||
else:
|
else:
|
||||||
logger(f"Dryrun {msg}", 0)
|
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:
|
||||||
|
args = []
|
||||||
|
|
||||||
|
for user, libraries in watched_list.items():
|
||||||
|
user_other = None
|
||||||
|
# If type of user is dict
|
||||||
|
if user_mapping:
|
||||||
|
if user in user_mapping.keys():
|
||||||
|
user_other = user_mapping[user]
|
||||||
|
elif user in user_mapping.values():
|
||||||
|
user_other = search_mapping(user_mapping, user)
|
||||||
|
|
||||||
|
for index, value in enumerate(self.users):
|
||||||
|
if user.lower() == value.title.lower():
|
||||||
|
user = self.users[index]
|
||||||
|
break
|
||||||
|
elif user_other and user_other.lower() == value.title.lower():
|
||||||
|
user = self.users[index]
|
||||||
break
|
break
|
||||||
|
|
||||||
if show_found:
|
if self.admin_user == user:
|
||||||
break
|
user_plex = self.plex
|
||||||
|
else:
|
||||||
|
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
|
||||||
|
|
||||||
|
for library, videos in libraries.items():
|
||||||
|
library_other = None
|
||||||
|
if library_mapping:
|
||||||
|
if library in library_mapping.keys():
|
||||||
|
library_other = library_mapping[library]
|
||||||
|
elif library in library_mapping.values():
|
||||||
|
library_other = search_mapping(library_mapping, library)
|
||||||
|
|
||||||
|
# if library in plex library list
|
||||||
|
library_list = user_plex.library.sections()
|
||||||
|
if library.lower() not in [x.title.lower() for x in library_list]:
|
||||||
|
if library_other:
|
||||||
|
if library_other.lower() in [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
|
||||||
|
else:
|
||||||
|
logger(f"Plex: Library {library} or {library_other} not found in library list", 2)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger(f"Plex: Library {library} not found in library list", 2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun])
|
||||||
|
|
||||||
|
future_thread_executor(args)
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"Plex: Failed to update watched, Error: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|||||||
1
test/requirements.txt
Normal file
1
test/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pytest
|
||||||
47
test/test_main_.py
Normal file
47
test/test_main_.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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']
|
||||||
176
test/test_main_cleanup_watched.py
Normal file
176
test/test_main_cleanup_watched.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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 cleanup_watched
|
||||||
|
|
||||||
|
tv_shows_watched_list_1 = {
|
||||||
|
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
|
||||||
|
"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',)}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset({("title", "Test"), ("locations", ("Test",))}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'locations': ('Test S01E01.mkv',)},
|
||||||
|
{'locations': ('Test S01E02.mkv',)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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',)},
|
||||||
|
]
|
||||||
|
|
||||||
|
tv_shows_watched_list_2 = {
|
||||||
|
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
|
||||||
|
"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",)}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset({("title", "Test"), ("locations", ("Test",))}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'locations': ('Test S01E02.mkv',)},
|
||||||
|
{'locations': ('Test S01E03.mkv',)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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',)}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test to see if objects get deleted all the way up to the root.
|
||||||
|
tv_shows_2_watched_list_1 = {
|
||||||
|
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_tv_show_watched_list_1 = {
|
||||||
|
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset({("title", "Test"), ("locations", ("Test",))}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'locations': ('Test S01E01.mkv',)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_movie_watched_list_1 = [
|
||||||
|
{"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)}
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_tv_show_watched_list_2 = {
|
||||||
|
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset({("title", "Test"), ("locations", ("Test",))}): {
|
||||||
|
"Season 1": [
|
||||||
|
{'locations': ('Test S01E03.mkv',)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_movie_watched_list_2 = [
|
||||||
|
{'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_cleanup_watched():
|
||||||
|
user_watched_list_1 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": tv_shows_watched_list_1,
|
||||||
|
"Movies": movies_watched_list_1,
|
||||||
|
"Other Shows": tv_shows_2_watched_list_1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
user_watched_list_2 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": tv_shows_watched_list_2,
|
||||||
|
"Movies": movies_watched_list_2,
|
||||||
|
"Other Shows": tv_shows_2_watched_list_1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_watched_list_1 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": expected_tv_show_watched_list_1
|
||||||
|
, "Movies": expected_movie_watched_list_1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_watched_list_2 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": expected_tv_show_watched_list_2
|
||||||
|
, "Movies": expected_movie_watched_list_2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2)
|
||||||
|
return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1)
|
||||||
|
|
||||||
|
assert return_watched_list_1 == expected_watched_list_1
|
||||||
|
assert return_watched_list_2 == expected_watched_list_2
|
||||||
|
|
||||||
|
|
||||||
|
def test_mapping_cleanup_watched():
|
||||||
|
user_watched_list_1 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": tv_shows_watched_list_1,
|
||||||
|
"Movies": movies_watched_list_1,
|
||||||
|
"Other Shows": tv_shows_2_watched_list_1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
user_watched_list_2 = {
|
||||||
|
"user2": {
|
||||||
|
"Shows": tv_shows_watched_list_2,
|
||||||
|
"Movies": movies_watched_list_2,
|
||||||
|
"Other Shows": tv_shows_2_watched_list_1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_watched_list_1 = {
|
||||||
|
"user1": {
|
||||||
|
"TV Shows": expected_tv_show_watched_list_1
|
||||||
|
, "Movies": expected_movie_watched_list_1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_watched_list_2 = {
|
||||||
|
"user2": {
|
||||||
|
"Shows": expected_tv_show_watched_list_2
|
||||||
|
, "Movies": expected_movie_watched_list_2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_mapping = { "user1": "user2" }
|
||||||
|
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_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_2 == expected_watched_list_2
|
||||||
Reference in New Issue
Block a user