7 Commits

Author SHA1 Message Date
Luigi311
16879cc728 Merge pull request #26 from luigi311/dev
Use async for jellyfin
2022-08-17 21:49:34 -06:00
Luigi311
942ec3533f Cleanup log file on runs 2022-08-17 21:43:51 -06:00
Luigi311
9f6edfc91a Merge branch 'main' into dev 2022-08-17 21:40:25 -06:00
Luigi311
827ace2e97 cleanup 2022-08-17 21:20:28 -06:00
Luigi311
f6b57a1b4d Update README.md 2022-07-10 01:38:42 -06:00
Luigi311
88a7526721 Use async for jellyfin (#23)
* Use async

* Massive jellyfin watched speedup

Co-authored-by: Luigi311 <luigi311.lg@gmail.com>
2022-07-10 01:30:12 -06:00
luigi311
1efb4d8543 Fix debug 2022-07-06 17:22:35 -06:00
16 changed files with 2625 additions and 2530 deletions

View File

@@ -1,42 +1,42 @@
## Do not mark any shows/movies as played and instead just output to log if they would of been marked. ## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
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
LOGFILE = "log.log" LOGFILE = "log.log"
## Map usernames between plex and jellyfin in the event that they are different, order does not matter ## Map usernames between plex and jellyfin in the event that they are different, order does not matter
#USER_MAPPING = { "testuser2": "testuser3" } #USER_MAPPING = { "testuser2": "testuser3" }
## Map libraries between plex and jellyfin in the even that they are different, order does not matter ## Map libraries between plex and jellyfin in the even that they are different, order does not matter
#LIBRARY_MAPPING = { "Shows": "TV Shows" } #LIBRARY_MAPPING = { "Shows": "TV Shows" }
## 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 ## 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"
## If not using plex token then use username and password of the server admin along with the servername ## If not using plex token then use username and password of the server admin along with the servername
#PLEX_USERNAME = "" #PLEX_USERNAME = ""
#PLEX_PASSWORD = "" #PLEX_PASSWORD = ""
#PLEX_SERVERNAME = "Plex Server" #PLEX_SERVERNAME = "Plex Server"
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
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"
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded. ## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
#BLACKLIST_LIBRARY = "" #BLACKLIST_LIBRARY = ""
#WHITELIST_LIBRARY = "" #WHITELIST_LIBRARY = ""
#BLACKLIST_LIBRARY_TYPE = "" #BLACKLIST_LIBRARY_TYPE = ""
#WHITELIST_LIBRARY_TYPE = "" #WHITELIST_LIBRARY_TYPE = ""
#BLACKLIST_USERS = "" #BLACKLIST_USERS = ""
WHITELIST_USERS = "testuser1,testuser2" WHITELIST_USERS = "testuser1,testuser2"

View File

@@ -1,86 +1,86 @@
name: CI name: CI
on: on:
push: push:
paths-ignore: paths-ignore:
- .gitignore - .gitignore
- "*.md" - "*.md"
pull_request: pull_request:
paths-ignore: paths-ignore:
- .gitignore - .gitignore
- "*.md" - "*.md"
jobs: jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: "Install dependencies" - name: "Install dependencies"
run: pip install -r requirements.txt && pip install -r test/requirements.txt run: pip install -r requirements.txt && pip install -r test/requirements.txt
- name: "Run tests" - name: "Run tests"
run: pytest -vvv run: pytest -vvv
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pytest needs: pytest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
if: "${{ env.DOCKER_USERNAME != '' }}" if: "${{ env.DOCKER_USERNAME != '' }}"
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched # list of Docker images to use as base name for tags images: ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched # list of Docker images to use as base name for tags
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=sha type=sha
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to DockerHub
if: "${{ steps.docker_meta.outcome == 'success' }}" if: "${{ steps.docker_meta.outcome == 'success' }}"
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: Build - name: Build
id: build id: build
if: "${{ steps.docker_meta.outcome == 'skipped' }}" if: "${{ steps.docker_meta.outcome == 'skipped' }}"
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: false push: false
tags: jellyplex-watched:action tags: jellyplex-watched:action
- name: Build Push - name: Build Push
id: build_push id: build_push
if: "${{ steps.docker_meta.outcome == 'success' }}" if: "${{ steps.docker_meta.outcome == 'success' }}"
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
# Echo digest so users can validate their image # Echo digest so users can validate their image
- name: Image digest - name: Image digest
if: "${{ steps.docker_meta.outcome == 'success' }}" if: "${{ steps.docker_meta.outcome == 'success' }}"
run: echo "${{ steps.build_push.outputs.digest }}" run: echo "${{ steps.build_push.outputs.digest }}"

264
.gitignore vendored
View File

@@ -1,132 +1,132 @@
.env .env
*.prof *.prof
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/ pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
.python-version .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow # PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.env .env
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/

32
.vscode/launch.json vendored
View File

@@ -1,16 +1,16 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Main", "name": "Python: Main",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "main.py", "program": "main.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true
} }
] ]
} }

View File

@@ -1,35 +1,35 @@
FROM python:3-slim FROM python:3-slim
ENV DRYRUN 'True' ENV DRYRUN 'True'
ENV DEBUG 'True' ENV DEBUG 'True'
ENV DEBUG_LEVEL 'INFO' ENV DEBUG_LEVEL 'INFO'
ENV SLEEP_DURATION '3600' ENV SLEEP_DURATION '3600'
ENV LOGFILE 'log.log' ENV LOGFILE 'log.log'
ENV USER_MAPPING '{ "User Test": "User Test2" }' ENV USER_MAPPING '{ "User Test": "User Test2" }'
ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }' ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }'
ENV PLEX_BASEURL 'http://localhost:32400' ENV PLEX_BASEURL 'http://localhost:32400'
ENV PLEX_TOKEN '' ENV PLEX_TOKEN ''
ENV PLEX_USERNAME '' ENV PLEX_USERNAME ''
ENV PLEX_PASSWORD '' ENV PLEX_PASSWORD ''
ENV PLEX_SERVERNAME '' ENV PLEX_SERVERNAME ''
ENV JELLYFIN_BASEURL 'http://localhost:8096' ENV JELLYFIN_BASEURL 'http://localhost:8096'
ENV JELLYFIN_TOKEN '' ENV JELLYFIN_TOKEN ''
ENV BLACKLIST_LIBRARY '' ENV BLACKLIST_LIBRARY ''
ENV WHITELIST_LIBRARY '' ENV WHITELIST_LIBRARY ''
ENV BLACKLIST_LIBRARY_TYPE '' ENV BLACKLIST_LIBRARY_TYPE ''
ENV WHITELIST_LIBRARY_TYPE '' ENV WHITELIST_LIBRARY_TYPE ''
ENV BLACKLIST_USERS '' ENV BLACKLIST_USERS ''
ENV WHITELIST_USERS '' ENV WHITELIST_USERS ''
WORKDIR /app WORKDIR /app
COPY ./requirements.txt ./ COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
CMD ["python", "-u", "main.py"] CMD ["python", "-u", "main.py"]

1348
LICENSE

File diff suppressed because it is too large Load Diff

146
README.md
View File

@@ -1,73 +1,73 @@
# JellyPlex-Watched # JellyPlex-Watched
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=luigi311/JellyPlex-Watched&amp;utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=luigi311/JellyPlex-Watched&amp;utm_campaign=Badge_Grade)
Sync watched between jellyfin and plex Sync watched between jellyfin and plex
## Description ## Description
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. 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 ## Configuration
## Installation ## Installation
### Baremetal ### Baremetal
- Setup virtualenv of your choice - Setup virtualenv of your choice
- Install dependencies - Install dependencies
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
- Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens - Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens
- Run - Run
```bash ```bash
python main.py python main.py
``` ```
### Docker ### Docker
- Build docker image - Build docker image
```bash ```bash
docker build -t jellyplex-watched . docker build -t jellyplex-watched .
``` ```
- or use pre-built image - or use pre-built image
```bash ```bash
docker pull luigi311/jellyplex-watched:latest docker pull luigi311/jellyplex-watched:latest
``` ```
#### With variables #### With variables
- Run - Run
```bash ```bash
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest
``` ```
#### 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
```bash ```bash
docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest
``` ```
## Contributing ## Contributing
I am open to recieving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches. I am open to recieving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches.
## License ## License
This is currently under the GNU General Public License v3.0. This is currently under the GNU General Public License v3.0.

20
main.py
View File

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

View File

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

View File

@@ -1,155 +1,168 @@
import os import os
from concurrent.futures import ThreadPoolExecutor 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: str, log_type=0): def logger(message: str, log_type=0):
debug = str_to_bool(os.getenv("DEBUG", "True")) debug = str_to_bool(os.getenv("DEBUG", "True"))
debug_level = os.getenv("DEBUG_LEVEL", "info").lower() debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
output = str(message) output = str(message)
if log_type == 0: if log_type == 0:
pass pass
elif log_type == 1 and (debug or debug_level == "info"): elif log_type == 1 and (debug and debug_level == "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}"
else: elif log_type == 4:
output = None output = f"[WARNING]: {output}"
else:
if output is not None: output = None
print(output)
file = open(logfile, "a", encoding="utf-8") if output is not None:
file.write(output + "\n") print(output)
file = open(logfile, "a", encoding="utf-8")
# Reimplementation of distutils.util.strtobool due to it being deprecated file.write(output + "\n")
# Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
def str_to_bool(value: any) -> bool: # Reimplementation of distutils.util.strtobool due to it being deprecated
if not value: # Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
return False def str_to_bool(value: any) -> bool:
return str(value).lower() in ("y", "yes", "t", "true", "on", "1") if not value:
return False
# Get mapped value return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
def search_mapping(dictionary: dict, key_value: str):
if key_value in dictionary.keys(): # Get mapped value
return dictionary[key_value] def search_mapping(dictionary: dict, key_value: str):
elif key_value.lower() in dictionary.keys(): if key_value in dictionary.keys():
return dictionary[key_value.lower()] return dictionary[key_value]
elif key_value in dictionary.values(): elif key_value.lower() in dictionary.keys():
return list(dictionary.keys())[list(dictionary.values()).index(key_value)] return dictionary[key_value.lower()]
elif key_value.lower() in dictionary.values(): elif key_value in dictionary.values():
return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())] return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
else: elif key_value.lower() in dictionary.values():
return None return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())]
else:
return None
def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
skip_reason = None
def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
if library_type.lower() in blacklist_library_type: skip_reason = None
skip_reason = "is blacklist_library_type"
if library_type.lower() in blacklist_library_type:
if library_title.lower() in [x.lower() for x in blacklist_library]: skip_reason = "is blacklist_library_type"
skip_reason = "is blacklist_library"
if library_title.lower() in [x.lower() for x in blacklist_library]:
library_other = None skip_reason = "is blacklist_library"
if library_mapping:
library_other = search_mapping(library_mapping, library_title) library_other = None
if library_other: if library_mapping:
if library_other.lower() in [x.lower() for x in blacklist_library]: library_other = search_mapping(library_mapping, library_title)
skip_reason = "is blacklist_library" if library_other:
if library_other.lower() in [x.lower() for x in blacklist_library]:
if len(whitelist_library_type) > 0: skip_reason = "is blacklist_library"
if library_type.lower() not in whitelist_library_type:
skip_reason = "is not whitelist_library_type" if len(whitelist_library_type) > 0:
if library_type.lower() not in whitelist_library_type:
# if whitelist is not empty and library is not in whitelist skip_reason = "is not whitelist_library_type"
if len(whitelist_library) > 0:
if library_title.lower() not in [x.lower() for x in whitelist_library]: # if whitelist is not empty and library is not in whitelist
skip_reason = "is not whitelist_library" if len(whitelist_library) > 0:
if library_title.lower() not in [x.lower() for x in whitelist_library]:
if library_other: skip_reason = "is not whitelist_library"
if library_other.lower() not in [x.lower() for x in whitelist_library]:
skip_reason = "is not whitelist_library" if library_other:
if library_other.lower() not in [x.lower() for x in whitelist_library]:
return skip_reason skip_reason = "is not whitelist_library"
return skip_reason
def generate_library_guids_dict(user_list: dict):
show_output_dict = {}
episode_output_dict = {} def generate_library_guids_dict(user_list: dict):
movies_output_dict = {} show_output_dict = {}
episode_output_dict = {}
try: movies_output_dict = {}
show_output_keys = user_list.keys()
show_output_keys = ([ dict(x) for x in list(show_output_keys) ]) try:
for show_key in show_output_keys: show_output_keys = user_list.keys()
for provider_key, provider_value in show_key.items(): show_output_keys = ([ dict(x) for x in list(show_output_keys) ])
# Skip title for show_key in show_output_keys:
if provider_key.lower() == "title": for provider_key, provider_value in show_key.items():
continue # Skip title
if provider_key.lower() not in show_output_dict: if provider_key.lower() == "title":
show_output_dict[provider_key.lower()] = [] continue
if provider_key.lower() == "locations": if provider_key.lower() not in show_output_dict:
for show_location in provider_value: show_output_dict[provider_key.lower()] = []
show_output_dict[provider_key.lower()].append(show_location) if provider_key.lower() == "locations":
else: for show_location in provider_value:
show_output_dict[provider_key.lower()].append(provider_value.lower()) show_output_dict[provider_key.lower()].append(show_location)
except: else:
pass show_output_dict[provider_key.lower()].append(provider_value.lower())
except:
try: logger(f"Generating show_output_dict failed, skipping", 1)
for show in user_list:
for season in user_list[show]: try:
for episode in user_list[show][season]: for show in user_list:
for episode_key, episode_value in episode.items(): for season in user_list[show]:
if episode_key.lower() not in episode_output_dict: for episode in user_list[show][season]:
episode_output_dict[episode_key.lower()] = [] for episode_key, episode_value in episode.items():
if episode_key == "locations": if episode_key.lower() not in episode_output_dict:
for episode_location in episode_value: episode_output_dict[episode_key.lower()] = []
episode_output_dict[episode_key.lower()].append(episode_location) if episode_key == "locations":
else: for episode_location in episode_value:
episode_output_dict[episode_key.lower()].append(episode_value.lower()) episode_output_dict[episode_key.lower()].append(episode_location)
except: else:
pass episode_output_dict[episode_key.lower()].append(episode_value.lower())
except:
try: logger(f"Generating episode_output_dict failed, skipping", 1)
for movie in user_list:
for movie_key, movie_value in movie.items(): try:
if movie_key.lower() not in movies_output_dict: for movie in user_list:
movies_output_dict[movie_key.lower()] = [] for movie_key, movie_value in movie.items():
if movie_key == "locations": if movie_key.lower() not in movies_output_dict:
for movie_location in movie_value: movies_output_dict[movie_key.lower()] = []
movies_output_dict[movie_key.lower()].append(movie_location) if movie_key == "locations":
else: for movie_location in movie_value:
movies_output_dict[movie_key.lower()].append(movie_value.lower()) movies_output_dict[movie_key.lower()].append(movie_location)
except: else:
pass movies_output_dict[movie_key.lower()].append(movie_value.lower())
except:
return show_output_dict, episode_output_dict, movies_output_dict logger(f"Generating movies_output_dict failed, skipping", 1)
def future_thread_executor(args: list, workers: int = -1): return show_output_dict, episode_output_dict, movies_output_dict
futures_list = []
results = [] def combine_watched_dicts(dicts: list):
combined_dict = {}
if workers == -1: for dict in dicts:
workers = min(32, os.cpu_count()*1.25) for key, value in dict.items():
if key not in combined_dict:
with ThreadPoolExecutor(max_workers=workers) as executor: combined_dict[key] = {}
for arg in args: for subkey, subvalue in value.items():
# * arg unpacks the list into actual arguments combined_dict[key][subkey] = subvalue
futures_list.append(executor.submit(*arg))
return combined_dict
for future in futures_list:
try: def future_thread_executor(args: list, workers: int = -1):
result = future.result() futures_list = []
results.append(result) results = []
except Exception as e:
raise Exception(e) if workers == -1:
workers = min(32, os.cpu_count()*1.25)
return results
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

View File

@@ -1,327 +1,393 @@
import requests import asyncio, aiohttp
from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict, future_thread_executor from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict, future_thread_executor, combine_watched_dicts
class Jellyfin(): class Jellyfin():
def __init__(self, baseurl, token): def __init__(self, baseurl, token):
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
self.session = requests.Session()
if not self.baseurl:
if not self.baseurl: raise Exception("Jellyfin baseurl not set")
raise Exception("Jellyfin baseurl not set")
if not self.token:
if not self.token: raise Exception("Jellyfin token not set")
raise Exception("Jellyfin token not set")
self.users = asyncio.run(self.get_users())
self.users = self.get_users()
async def query(self, query, query_type, session, identifiers=None):
def query(self, query, query_type): try:
try: results = None
response = None headers = {
"Accept": "application/json",
headers = { "X-Emby-Token": self.token
"Accept": "application/json", }
"X-Emby-Token": self.token authorization = (
} 'MediaBrowser , '
if query_type == "get": 'Client="other", '
response = self.session.get(self.baseurl + query, headers=headers) 'Device="script", '
'DeviceId="script", '
elif query_type == "post": 'Version="0.0.0"'
authorization = ( )
'MediaBrowser , ' headers["X-Emby-Authorization"] = authorization
'Client="other", '
'Device="script", ' if query_type == "get":
'DeviceId="script", ' async with session.get(self.baseurl + query, headers=headers) as response:
'Version="0.0.0"' results = await response.json()
)
headers["X-Emby-Authorization"] = authorization elif query_type == "post":
response = self.session.post(self.baseurl + query, headers=headers) async with session.post(self.baseurl + query, headers=headers) as response:
results = await response.json()
return response.json()
# append identifiers to results
except Exception as e: if identifiers:
logger(f"Jellyfin: Query failed {e}", 2) results["Identifiers"] = identifiers
raise Exception(e) return results
def get_users(self): except Exception as e:
try: logger(f"Jellyfin: Query failed {e}", 2)
users = {} raise Exception(e)
query = "/Users"
response = self.query(query, "get") async def get_users(self):
try:
# If reponse is not empty users = {}
if response:
for user in response: query_string = "/Users"
users[user["Name"]] = user["Id"] async with aiohttp.ClientSession() as session:
response = await self.query(query_string, "get", session)
return users
except Exception as e: # If reponse is not empty
logger(f"Jellyfin: Get users failed {e}", 2) if response:
raise Exception(e) for user in response:
users[user["Name"]] = user["Id"]
def get_user_watched(self, user_name, user_id, library_type, library_id, library_title):
try: return users
user_name = user_name.lower() except Exception as e:
user_watched = {} logger(f"Jellyfin: Get users failed {e}", 2)
user_watched[user_name] = {} raise Exception(e)
logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0)
# Movies async def get_user_watched(self, user_name, user_id, library_type, library_id, library_title):
if library_type == "Movie": try:
user_watched[user_name][library_title] = [] user_name = user_name.lower()
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get") user_watched = {}
for movie in watched["Items"]: user_watched[user_name] = {}
if movie["UserData"]["Played"] == True:
movie_guids = {} logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0)
movie_guids["title"] = movie["Name"] # Movies
if movie["ProviderIds"]: async with aiohttp.ClientSession() as session:
# Lowercase movie["ProviderIds"] keys if library_type == "Movie":
movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()} user_watched[user_name][library_title] = []
if movie["MediaSources"]: watched = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get", session)
movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]]) for movie in watched["Items"]:
user_watched[user_name][library_title].append(movie_guids) if movie["UserData"]["Played"] == True:
movie_guids = {}
# TV Shows movie_guids["title"] = movie["Name"]
if library_type == "Episode": if "ProviderIds" in movie:
user_watched[user_name][library_title] = {} # Lowercase movie["ProviderIds"] keys
watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds,Path", "get") movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()}
watched_shows = [x for x in watched["Items"] if x["Type"] == "Series"] if "MediaSources" in movie:
movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]])
for show in watched_shows: user_watched[user_name][library_title].append(movie_guids)
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
show_guids["title"] = show["Name"] # TV Shows
show_guids["locations"] = tuple([show["Path"].split("/")[-1]]) if library_type == "Series":
show_guids = frozenset(show_guids.items()) user_watched[user_name][library_title] = {}
seasons = self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") watched_shows = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount", "get", session)
if len(seasons["Items"]) > 0: watched_shows_filtered = []
for season in seasons["Items"]: for show in watched_shows["Items"]:
episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") if "PlayedPercentage" in show["UserData"]:
if len(episodes["Items"]) > 0: if show["UserData"]["PlayedPercentage"] > 0:
for episode in episodes["Items"]: watched_shows_filtered.append(show)
if episode["UserData"]["Played"] == True: seasons_tasks = []
if episode["ProviderIds"] or episode["MediaSources"]: for show in watched_shows_filtered:
if show_guids not in user_watched[user_name][library_title]: show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
user_watched[user_name][library_title][show_guids] = {} show_guids["title"] = show["Name"]
if season["Name"] not in user_watched[user_name][library_title][show_guids]: show_guids["locations"] = tuple([show["Path"].split("/")[-1]])
user_watched[user_name][library_title][show_guids][season["Name"]] = [] show_guids = frozenset(show_guids.items())
identifiers = {"show_guids": show_guids, "show_id": show["Id"]}
# Lowercase episode["ProviderIds"] keys task = asyncio.ensure_future(self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount", "get", session, frozenset(identifiers.items())))
episode_guids = {} seasons_tasks.append(task)
if episode["ProviderIds"]:
episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()} seasons_watched = await asyncio.gather(*seasons_tasks)
if episode["MediaSources"]: seasons_watched_filtered = []
episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]])
user_watched[user_name][library_title][show_guids][season["Name"]].append(episode_guids) for seasons in seasons_watched:
seasons_watched_filtered_dict = {}
return user_watched seasons_watched_filtered_dict["Identifiers"] = seasons["Identifiers"]
except Exception as e: seasons_watched_filtered_dict["Items"] = []
logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2) for season in seasons["Items"]:
raise Exception(e) if "PlayedPercentage" in season["UserData"]:
if season["UserData"]["PlayedPercentage"] > 0:
seasons_watched_filtered_dict["Items"].append(season)
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None):
try: if seasons_watched_filtered_dict["Items"]:
users_watched = {} seasons_watched_filtered.append(seasons_watched_filtered_dict)
args = []
episodes_tasks = []
for user_name, user_id in users.items(): for seasons in seasons_watched_filtered:
# Get all libraries if len(seasons["Items"]) > 0:
user_name = user_name.lower() for season in seasons["Items"]:
season_identifiers = dict(seasons["Identifiers"])
libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] season_identifiers["season_id"] = season["Id"]
season_identifiers["season_name"] = season["Name"]
for library in libraries: 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())))
library_title = library["Name"] episodes_tasks.append(task)
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") watched_episodes = await asyncio.gather(*episodes_tasks)
for episodes in watched_episodes:
if len(watched["Items"]) == 0: if len(episodes["Items"]) > 0:
logger(f"Jellyfin: No watched items found in library {library_title}", 1) for episode in episodes["Items"]:
continue if episode["UserData"]["Played"] == True:
else: if "ProviderIds" in episode or "MediaSources" in episode:
library_type = watched["Items"][0]["Type"] episode_identifiers = dict(episodes["Identifiers"])
show_guids = episode_identifiers["show_guids"]
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) if show_guids not in user_watched[user_name][library_title]:
user_watched[user_name][library_title][show_guids] = {}
if skip_reason: if episode_identifiers["season_name"] not in user_watched[user_name][library_title][show_guids]:
logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]] = []
continue
episode_guids = {}
args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) if "ProviderIds" in episode:
episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()}
for user_watched in future_thread_executor(args): if "MediaSources" in episode:
for user, user_watched_temp in user_watched.items(): episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]])
if user not in users_watched: user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]].append(episode_guids)
users_watched[user] = {}
users_watched[user].update(user_watched_temp) return user_watched
except Exception as e:
return users_watched logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2)
except Exception as e: raise Exception(e)
logger(f"Jellyfin: Failed to get watched, 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):
def update_user_watched(self, user_name, user_id, library, library_id, videos, dryrun): try:
try: # Get all libraries
logger(f"Jellyfin: Updating watched for {user_name} in library {library}", 1) user_name = user_name.lower()
videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos) tasks_watched = []
logger(f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1) tasks_libraries = []
async with aiohttp.ClientSession() as session:
if videos_movies_ids: libraries = await self.query(f"/Users/{user_id}/Views", "get", session)
jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", "get") for library in libraries["Items"]:
for jellyfin_video in jellyfin_search["Items"]: library_id = library["Id"]
movie_found = False library_title = library["Name"]
identifiers = {"library_id": library_id, "library_title": library_title}
if "MediaSources" in jellyfin_video: task = asyncio.ensure_future(self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&limit=1", "get", session, identifiers=identifiers))
for movie_location in jellyfin_video["MediaSources"]: tasks_libraries.append(task)
if movie_location["Path"].split("/")[-1] in videos_movies_ids["locations"]:
movie_found = True libraries = await asyncio.gather(*tasks_libraries, return_exceptions=True)
break
for watched in libraries:
if not movie_found: if len(watched["Items"]) == 0:
for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items(): continue
if movie_provider_source.lower() in videos_movies_ids:
if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]: library_id = watched["Identifiers"]["library_id"]
movie_found = True library_title = watched["Identifiers"]["library_title"]
break library_type = watched["Items"][0]["Type"]
if movie_found: skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
jellyfin_video_id = jellyfin_video["Id"]
msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin" if skip_reason:
if not dryrun: logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1)
logger(f"Marking {msg}", 0) continue
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post")
else: # Get watched for user
logger(f"Dryrun {msg}", 0) task = asyncio.ensure_future(self.get_user_watched(user_name, user_id, library_type, library_id, library_title))
else: tasks_watched.append(task)
logger(f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 1)
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
return watched
except Exception as e:
# TV Shows logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
if videos_shows_ids and videos_episodes_ids: 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,Path", "get")
jellyfin_shows = [x for x in jellyfin_search["Items"]]
async def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None):
for jellyfin_show in jellyfin_shows: try:
show_found = False users_watched = {}
watched = []
if "Path" in jellyfin_show:
if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]: for user_name, user_id in users.items():
show_found = True watched.append(await self.get_users_watched(user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping))
if not show_found: for user_watched in watched:
for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items(): user_watched_temp = combine_watched_dicts(user_watched)
if show_provider_source.lower() in videos_shows_ids: for user, user_watched_temp in user_watched_temp.items():
if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]: if user not in users_watched:
show_found = True users_watched[user] = {}
break users_watched[user].update(user_watched_temp)
if show_found: return users_watched
logger(f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", 1) except Exception as e:
jellyfin_show_id = jellyfin_show["Id"] logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") raise Exception(e)
for jellyfin_episode in jellyfin_episodes["Items"]:
episode_found = False async def update_user_watched(self, user_name, user_id, library, library_id, videos, dryrun):
try:
if "MediaSources" in jellyfin_episode: logger(f"Jellyfin: Updating watched for {user_name} in library {library}", 1)
for episode_location in jellyfin_episode["MediaSources"]: videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos)
if episode_location["Path"].split("/")[-1] in videos_episodes_ids["locations"]:
episode_found = True logger(f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1)
break async with aiohttp.ClientSession() as session:
if videos_movies_ids:
if not episode_found: 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 episode_provider_source, episode_provider_id in jellyfin_episode["ProviderIds"].items(): for jellyfin_video in jellyfin_search["Items"]:
if episode_provider_source.lower() in videos_episodes_ids: movie_found = False
if episode_provider_id.lower() in videos_episodes_ids[episode_provider_source.lower()]:
episode_found = True if "MediaSources" in jellyfin_video:
break for movie_location in jellyfin_video["MediaSources"]:
if movie_location["Path"].split("/")[-1] in videos_movies_ids["locations"]:
if episode_found: movie_found = True
jellyfin_episode_id = jellyfin_episode["Id"] break
msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']} as watched for {user_name} in {library} for Jellyfin"
if not dryrun: if not movie_found:
logger(f"Marked {msg}", 0) for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items():
self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post") if movie_provider_source.lower() in videos_movies_ids:
else: if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]:
logger(f"Dryrun {msg}", 0) movie_found = True
else: break
logger(f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 1)
else: if movie_found:
logger(f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", 1) jellyfin_video_id = jellyfin_video["Id"]
msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin"
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids: if not dryrun:
logger(f"Jellyfin: No videos to mark as watched for {user_name} in library {library}", 1) logger(f"Marking {msg}", 0)
await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post", session)
except Exception as e: else:
logger(f"Jellyfin: Error updating watched for {user_name} in library {library}", 2) logger(f"Dryrun {msg}", 0)
raise Exception(e) else:
logger(f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 1)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try:
args = [] # TV Shows
for user, libraries in watched_list.items(): if videos_shows_ids and videos_episodes_ids:
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1) 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)
user_other = None jellyfin_shows = [x for x in jellyfin_search["Items"]]
user_name = None
if user_mapping: for jellyfin_show in jellyfin_shows:
if user in user_mapping.keys(): show_found = False
user_other = user_mapping[user]
elif user in user_mapping.values(): if "Path" in jellyfin_show:
user_other = search_mapping(user_mapping, user) if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]:
show_found = True
user_id = None
for key in self.users.keys(): if not show_found:
if user.lower() == key.lower(): for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items():
user_id = self.users[key] if show_provider_source.lower() in videos_shows_ids:
user_name = key if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]:
break show_found = True
elif user_other and user_other.lower() == key.lower(): break
user_id = self.users[key]
user_name = key if show_found:
break logger(f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", 1)
jellyfin_show_id = jellyfin_show["Id"]
if not user_id: jellyfin_episodes = await self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get", session)
logger(f"{user} {user_other} not found in Jellyfin", 2)
continue for jellyfin_episode in jellyfin_episodes["Items"]:
episode_found = False
jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"]
if "MediaSources" in jellyfin_episode:
for library, videos in libraries.items(): for episode_location in jellyfin_episode["MediaSources"]:
library_other = None if episode_location["Path"].split("/")[-1] in videos_episodes_ids["locations"]:
if library_mapping: episode_found = True
if library in library_mapping.keys(): break
library_other = library_mapping[library]
elif library in library_mapping.values(): if not episode_found:
library_other = search_mapping(library_mapping, library) 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()]:
if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]: episode_found = True
if library_other: break
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) if episode_found:
library = library_other jellyfin_episode_id = jellyfin_episode["Id"]
else: msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']} as watched for {user_name} in {library} for Jellyfin"
logger(f"Jellyfin: Library {library} or {library_other} not found in library list", 2) if not dryrun:
continue logger(f"Marked {msg}", 0)
else: await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post", session)
logger(f"Jellyfin: Library {library} not found in library list", 2) else:
continue logger(f"Dryrun {msg}", 0)
else:
library_id = None logger(f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 1)
for jellyfin_library in jellyfin_libraries: else:
if jellyfin_library["Name"] == library: logger(f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", 1)
library_id = jellyfin_library["Id"]
continue 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)
if library_id:
args.append([self.update_user_watched, user_name, user_id, library, library_id, videos, dryrun]) except Exception as e:
logger(f"Jellyfin: Error updating watched for {user_name} in library {library}", 2)
future_thread_executor(args) raise Exception(e)
except Exception as e:
logger(f"Jellyfin: Error updating watched", 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():
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
user_other = None
user_name = None
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)
user_id = None
for key in self.users.keys():
if user.lower() == key.lower():
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
if not user_id:
logger(f"{user} {user_other} not found in Jellyfin", 2)
continue
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():
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.lower() not in [x["Name"].lower() for x in jellyfin_libraries]:
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
library_id = None
for jellyfin_library in jellyfin_libraries:
if jellyfin_library["Name"] == library:
library_id = jellyfin_library["Id"]
continue
if library_id:
task = self.update_user_watched(user_name, user_id, library, library_id, videos, dryrun)
tasks.append(task)
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e:
logger(f"Jellyfin: Error updating watched", 2)
raise Exception(e)

View File

@@ -1,441 +1,450 @@
import copy, os, traceback, json import copy, os, traceback, json, asyncio
from dotenv import load_dotenv from dotenv import load_dotenv
from time import sleep from time import sleep, perf_counter
from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor
from src.plex import Plex from src.plex import Plex
from src.jellyfin import Jellyfin from src.jellyfin import Jellyfin
load_dotenv(override=True) load_dotenv(override=True)
def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None): def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None):
modified_watched_list_1 = copy.deepcopy(watched_list_1) modified_watched_list_1 = copy.deepcopy(watched_list_1)
# remove entries from plex_watched that are in jellyfin_watched # remove entries from plex_watched that are in jellyfin_watched
for user_1 in watched_list_1: for user_1 in watched_list_1:
user_other = None user_other = None
if user_mapping: if user_mapping:
user_other = search_mapping(user_mapping, user_1) user_other = search_mapping(user_mapping, user_1)
if user_1 in modified_watched_list_1: if user_1 in modified_watched_list_1:
if user_1 in watched_list_2: if user_1 in watched_list_2:
user_2 = user_1 user_2 = user_1
elif user_other in watched_list_2: elif user_other in watched_list_2:
user_2 = user_other user_2 = user_other
else: else:
logger(f"User {user_1} and {user_other} not found in watched list 2", 1) logger(f"User {user_1} and {user_other} not found in watched list 2", 1)
continue continue
for library_1 in watched_list_1[user_1]: for library_1 in watched_list_1[user_1]:
library_other = None library_other = None
if library_mapping: if library_mapping:
library_other = search_mapping(library_mapping, library_1) library_other = search_mapping(library_mapping, library_1)
if library_1 in modified_watched_list_1[user_1]: if library_1 in modified_watched_list_1[user_1]:
if library_1 in watched_list_2[user_2]: if library_1 in watched_list_2[user_2]:
library_2 = library_1 library_2 = library_1
elif library_other in watched_list_2[user_2]: elif library_other in watched_list_2[user_2]:
library_2 = library_other library_2 = library_other
else: else:
logger(f"library {library_1} and {library_other} not found in watched list 2", 1) logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
continue continue
# Movies _, episode_watched_list_2_keys_dict, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2])
if isinstance(watched_list_1[user_1][library_1], list):
_, _, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2]) # Movies
for movie in watched_list_1[user_1][library_1]: if isinstance(watched_list_1[user_1][library_1], list):
movie_found = False for movie in watched_list_1[user_1][library_1]:
for movie_key, movie_value in movie.items(): movie_found = False
if movie_key == "locations": for movie_key, movie_value in movie.items():
for location in movie_value: if movie_key == "locations":
if location in movies_watched_list_2_keys_dict["locations"]: if "locations" in movies_watched_list_2_keys_dict.keys():
movie_found = True for location in movie_value:
break if location in movies_watched_list_2_keys_dict["locations"]:
else: movie_found = True
if movie_key in movies_watched_list_2_keys_dict.keys(): break
if movie_value in movies_watched_list_2_keys_dict[movie_key]: else:
movie_found = True if movie_key in movies_watched_list_2_keys_dict.keys():
if movie_value in movies_watched_list_2_keys_dict[movie_key]:
if movie_found: movie_found = True
logger(f"Removing {movie} from {library_1}", 3)
modified_watched_list_1[user_1][library_1].remove(movie) if movie_found:
break 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 # TV Shows
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2]) 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 show_key_1 in watched_list_1[user_1][library_1].keys():
for season in watched_list_1[user_1][library_1][show_key_1]: show_key_dict = dict(show_key_1)
for episode in watched_list_1[user_1][library_1][show_key_1][season]: for season in watched_list_1[user_1][library_1][show_key_1]:
episode_found = False for episode in watched_list_1[user_1][library_1][show_key_1][season]:
for episode_key, episode_value in episode.items(): episode_found = False
# If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1 for episode_key, episode_value in episode.items():
if episode_key == "locations": # If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
for location in episode_value: if episode_key == "locations":
if location in episode_watched_list_2_keys_dict["locations"]: if "locations" in episode_watched_list_2_keys_dict.keys():
episode_found = True for location in episode_value:
break if location in episode_watched_list_2_keys_dict["locations"]:
episode_found = True
else: break
if episode_key in episode_watched_list_2_keys_dict.keys():
if episode_value in episode_watched_list_2_keys_dict[episode_key]: else:
episode_found = True if episode_key in episode_watched_list_2_keys_dict.keys():
if episode_value in episode_watched_list_2_keys_dict[episode_key]:
if episode_found: episode_found = True
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) if episode_found:
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode) if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
break logger(f"Removing {episode} from {show_key_dict['title']}", 3)
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
# Remove empty seasons break
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]: # Remove empty seasons
logger(f"Removing {season} from {show_key_dict['title']} because it is empty", 3) if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
del modified_watched_list_1[user_1][library_1][show_key_1][season] 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)
# If the show is empty, remove the show del modified_watched_list_1[user_1][library_1][show_key_1][season]
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]: # If the show is empty, remove the show
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1) if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
del modified_watched_list_1[user_1][library_1][show_key_1] 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)
for user_1 in watched_list_1: del modified_watched_list_1[user_1][library_1][show_key_1]
for library_1 in watched_list_1[user_1]:
if library_1 in modified_watched_list_1[user_1]: for user_1 in watched_list_1:
# If library is empty then remove it for library_1 in watched_list_1[user_1]:
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) # If library is empty then remove it
del modified_watched_list_1[user_1][library_1] if len(modified_watched_list_1[user_1][library_1]) == 0:
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
if user_1 in modified_watched_list_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: if user_1 in modified_watched_list_1:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1) # If user is empty delete user
del modified_watched_list_1[user_1] if len(modified_watched_list_1[user_1]) == 0:
logger(f"Removing {user_1} from watched list 1 because it is empty", 1)
return modified_watched_list_1 del modified_watched_list_1[user_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): return modified_watched_list_1
if blacklist_library:
if len(blacklist_library) > 0: 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):
blacklist_library = blacklist_library.split(",") if blacklist_library:
blacklist_library = [x.strip() for x in blacklist_library] if len(blacklist_library) > 0:
if library_mapping: blacklist_library = blacklist_library.split(",")
temp_library = [] blacklist_library = [x.strip() for x in blacklist_library]
for library in blacklist_library: if library_mapping:
library_other = search_mapping(library_mapping, library) temp_library = []
if library_other: for library in blacklist_library:
temp_library.append(library_other) library_other = search_mapping(library_mapping, library)
if library_other:
blacklist_library = blacklist_library + temp_library temp_library.append(library_other)
else:
blacklist_library = [] blacklist_library = blacklist_library + temp_library
logger(f"Blacklist Library: {blacklist_library}", 1) else:
blacklist_library = []
if whitelist_library: logger(f"Blacklist Library: {blacklist_library}", 1)
if len(whitelist_library) > 0:
whitelist_library = whitelist_library.split(",") if whitelist_library:
whitelist_library = [x.strip() for x in whitelist_library] if len(whitelist_library) > 0:
if library_mapping: whitelist_library = whitelist_library.split(",")
temp_library = [] whitelist_library = [x.strip() for x in whitelist_library]
for library in whitelist_library: if library_mapping:
library_other = search_mapping(library_mapping, library) temp_library = []
if library_other: for library in whitelist_library:
temp_library.append(library_other) library_other = search_mapping(library_mapping, library)
if library_other:
whitelist_library = whitelist_library + temp_library temp_library.append(library_other)
else:
whitelist_library = [] whitelist_library = whitelist_library + temp_library
logger(f"Whitelist Library: {whitelist_library}", 1) else:
whitelist_library = []
if blacklist_library_type: logger(f"Whitelist Library: {whitelist_library}", 1)
if len(blacklist_library_type) > 0:
blacklist_library_type = blacklist_library_type.split(",") if blacklist_library_type:
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type] if len(blacklist_library_type) > 0:
else: blacklist_library_type = blacklist_library_type.split(",")
blacklist_library_type = [] blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
logger(f"Blacklist Library Type: {blacklist_library_type}", 1) else:
blacklist_library_type = []
if whitelist_library_type: logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
if len(whitelist_library_type) > 0:
whitelist_library_type = whitelist_library_type.split(",") if whitelist_library_type:
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type] if len(whitelist_library_type) > 0:
else: whitelist_library_type = whitelist_library_type.split(",")
whitelist_library_type = [] whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
logger(f"Whitelist Library Type: {whitelist_library_type}", 1) else:
whitelist_library_type = []
if blacklist_users: logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
if len(blacklist_users) > 0:
blacklist_users = blacklist_users.split(",") if blacklist_users:
blacklist_users = [x.lower().strip() for x in blacklist_users] if len(blacklist_users) > 0:
if user_mapping: blacklist_users = blacklist_users.split(",")
temp_users = [] blacklist_users = [x.lower().strip() for x in blacklist_users]
for user in blacklist_users: if user_mapping:
user_other = search_mapping(user_mapping, user) temp_users = []
if user_other: for user in blacklist_users:
temp_users.append(user_other) user_other = search_mapping(user_mapping, user)
if user_other:
blacklist_users = blacklist_users + temp_users temp_users.append(user_other)
else:
blacklist_users = [] blacklist_users = blacklist_users + temp_users
logger(f"Blacklist Users: {blacklist_users}", 1) else:
blacklist_users = []
if whitelist_users: logger(f"Blacklist Users: {blacklist_users}", 1)
if len(whitelist_users) > 0:
whitelist_users = whitelist_users.split(",") if whitelist_users:
whitelist_users = [x.lower().strip() for x in whitelist_users] if len(whitelist_users) > 0:
if user_mapping: whitelist_users = whitelist_users.split(",")
temp_users = [] whitelist_users = [x.lower().strip() for x in whitelist_users]
for user in whitelist_users: if user_mapping:
user_other = search_mapping(user_mapping, user) temp_users = []
if user_other: for user in whitelist_users:
temp_users.append(user_other) user_other = search_mapping(user_mapping, user)
if user_other:
whitelist_users = whitelist_users + temp_users temp_users.append(user_other)
else:
whitelist_users = [] whitelist_users = whitelist_users + temp_users
else: else:
whitelist_users = [] whitelist_users = []
logger(f"Whitelist Users: {whitelist_users}", 1) else:
whitelist_users = []
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users logger(f"Whitelist Users: {whitelist_users}", 1)
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None): return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
# generate list of users from server 1 and server 2 def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
server_1_type = server_1[0]
server_1_connection = server_1[1] # generate list of users from server 1 and server 2
server_2_type = server_2[0] server_1_type = server_1[0]
server_2_connection = server_2[1] server_1_connection = server_1[1]
server_2_type = server_2[0]
server_1_users = [] server_2_connection = server_2[1]
if server_1_type == "plex": print(f"Server 1: {server_1_type} {server_1_connection}")
server_1_users = [ x.title.lower() for x in server_1_connection.users ] print(f"Server 2: {server_2_type} {server_2_connection}")
elif server_1_type == "jellyfin":
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ] server_1_users = []
if server_1_type == "plex":
server_2_users = [] server_1_users = [ x.title.lower() for x in server_1_connection.users ]
if server_2_type == "plex": elif server_1_type == "jellyfin":
server_2_users = [ x.title.lower() for x in server_2_connection.users ] server_1_users = [ key.lower() for key in server_1_connection.users.keys() ]
elif server_2_type == "jellyfin":
server_2_users = [ key.lower() for key in server_2_connection.users.keys() ] server_2_users = []
if server_2_type == "plex":
server_2_users = [ x.title.lower() for x in server_2_connection.users ]
# combined list of overlapping users from plex and jellyfin elif server_2_type == "jellyfin":
users = {} server_2_users = [ key.lower() for key in server_2_connection.users.keys() ]
for server_1_user in server_1_users:
if user_mapping: # combined list of overlapping users from plex and jellyfin
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user) users = {}
if jellyfin_plex_mapped_user:
users[server_1_user] = jellyfin_plex_mapped_user for server_1_user in server_1_users:
continue if user_mapping:
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
if server_1_user in server_2_users: if jellyfin_plex_mapped_user:
users[server_1_user] = server_1_user users[server_1_user] = jellyfin_plex_mapped_user
continue
for server_2_user in server_2_users:
if user_mapping: if server_1_user in server_2_users:
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) users[server_1_user] = server_1_user
if plex_jellyfin_mapped_user:
users[plex_jellyfin_mapped_user] = server_2_user for server_2_user in server_2_users:
continue if user_mapping:
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
if server_2_user in server_1_users: if plex_jellyfin_mapped_user:
users[server_2_user] = server_2_user users[plex_jellyfin_mapped_user] = server_2_user
continue
logger(f"User list that exist on both servers {users}", 1)
if server_2_user in server_1_users:
users_filtered = {} users[server_2_user] = server_2_user
for user in users:
# whitelist_user is not empty and user lowercase is not in whitelist lowercase logger(f"User list that exist on both servers {users}", 1)
if len(whitelist_users) > 0:
if user not in whitelist_users and users[user] not in whitelist_users: users_filtered = {}
logger(f"{user} or {users[user]} is not in whitelist", 1) for user in users:
continue # whitelist_user is not empty and user lowercase is not in whitelist lowercase
if len(whitelist_users) > 0:
if user not in blacklist_users and users[user] not in blacklist_users: if user not in whitelist_users and users[user] not in whitelist_users:
users_filtered[user] = users[user] logger(f"{user} or {users[user]} is not in whitelist", 1)
continue
logger(f"Filtered user list {users_filtered}", 1)
if user not in blacklist_users and users[user] not in blacklist_users:
if server_1_type == "plex": users_filtered[user] = users[user]
output_server_1_users = []
for plex_user in server_1_connection.users: logger(f"Filtered user list {users_filtered}", 1)
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) if server_1_type == "plex":
elif server_1_type == "jellyfin": output_server_1_users = []
output_server_1_users = {} for plex_user in server_1_connection.users:
for jellyfin_user, jellyfin_id in server_1_connection.users.items(): if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): output_server_1_users.append(plex_user)
output_server_1_users[jellyfin_user] = jellyfin_id elif server_1_type == "jellyfin":
output_server_1_users = {}
if server_2_type == "plex": for jellyfin_user, jellyfin_id in server_1_connection.users.items():
output_server_2_users = [] if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
for plex_user in server_2_connection.users: output_server_1_users[jellyfin_user] = jellyfin_id
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) if server_2_type == "plex":
elif server_2_type == "jellyfin": output_server_2_users = []
output_server_2_users = {} for plex_user in server_2_connection.users:
for jellyfin_user, jellyfin_id in server_2_connection.users.items(): if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): output_server_2_users.append(plex_user)
output_server_2_users[jellyfin_user] = jellyfin_id elif server_2_type == "jellyfin":
output_server_2_users = {}
if len(output_server_1_users) == 0: for jellyfin_user, jellyfin_id in server_2_connection.users.items():
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}") 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_2_users) == 0:
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}") if len(output_server_1_users) == 0:
raise Exception(f"No users found for server 1, 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) if len(output_server_2_users) == 0:
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}")
return output_server_1_users, output_server_2_users
logger(f"Server 1 users: {output_server_1_users}", 1)
def generate_server_connections(): logger(f"Server 2 users: {output_server_2_users}", 1)
servers = []
return output_server_1_users, output_server_2_users
plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None) def generate_server_connections():
plex_username = os.getenv("PLEX_USERNAME", None) servers = []
plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None) plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None)
if plex_baseurl and plex_token: plex_username = os.getenv("PLEX_USERNAME", None)
plex_baseurl = plex_baseurl.split(",") plex_password = os.getenv("PLEX_PASSWORD", None)
plex_token = plex_token.split(",") plex_servername = os.getenv("PLEX_SERVERNAME", None)
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
if len(plex_baseurl) != len(plex_token):
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",")
for i, url in enumerate(plex_baseurl): plex_token = plex_token.split(",")
servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None)))
if len(plex_baseurl) != len(plex_token):
if plex_username and plex_password and plex_servername: raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries")
plex_username = plex_username.split(",")
plex_password = plex_password.split(",") for i, url in enumerate(plex_baseurl):
plex_servername = plex_servername.split(",") servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None, ssl_bypass=ssl_bypass)))
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername): if plex_username and plex_password and plex_servername:
raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries") plex_username = plex_username.split(",")
plex_password = plex_password.split(",")
for i, username in enumerate(plex_username): plex_servername = plex_servername.split(",")
servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip())))
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername):
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None) raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries")
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
for i, username in enumerate(plex_username):
if jellyfin_baseurl and jellyfin_token: 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 = jellyfin_baseurl.split(",")
jellyfin_token = jellyfin_token.split(",") jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
if len(jellyfin_baseurl) != len(jellyfin_token):
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries") if jellyfin_baseurl and jellyfin_token:
jellyfin_baseurl = jellyfin_baseurl.split(",")
for i, baseurl in enumerate(jellyfin_baseurl): jellyfin_token = jellyfin_token.split(",")
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
if len(jellyfin_baseurl) != len(jellyfin_token):
return servers raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries")
def main_loop(): for i, baseurl in enumerate(jellyfin_baseurl):
logfile = os.getenv("LOGFILE","log.log") servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
# Delete logfile if it exists
if os.path.exists(logfile): return servers
os.remove(logfile)
dryrun = str_to_bool(os.getenv("DRYRUN", "False")) def main_loop():
logger(f"Dryrun: {dryrun}", 1) logfile = os.getenv("LOGFILE","log.log")
# Delete logfile if it exists
user_mapping = os.getenv("USER_MAPPING") if os.path.exists(logfile):
if user_mapping: os.remove(logfile)
user_mapping = json.loads(user_mapping.lower())
logger(f"User Mapping: {user_mapping}", 1) dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
logger(f"Dryrun: {dryrun}", 1)
library_mapping = os.getenv("LIBRARY_MAPPING")
if library_mapping: user_mapping = os.getenv("USER_MAPPING")
library_mapping = json.loads(library_mapping) if user_mapping:
logger(f"Library Mapping: {library_mapping}", 1) user_mapping = json.loads(user_mapping.lower())
logger(f"User Mapping: {user_mapping}", 1)
# Create (black/white)lists
logger("Creating (black/white)lists", 1) library_mapping = os.getenv("LIBRARY_MAPPING")
blacklist_library = os.getenv("BLACKLIST_LIBRARY", None) if library_mapping:
whitelist_library = os.getenv("WHITELIST_LIBRARY", None) library_mapping = json.loads(library_mapping)
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None) logger(f"Library Mapping: {library_mapping}", 1)
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None)
blacklist_users = os.getenv("BLACKLIST_USERS", None) # Create (black/white)lists
whitelist_users = os.getenv("WHITELIST_USERS", None) logger("Creating (black/white)lists", 1)
blacklist_library = os.getenv("BLACKLIST_LIBRARY", 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) whitelist_library = os.getenv("WHITELIST_LIBRARY", None)
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None)
# Create server connections whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None)
logger("Creating server connections", 1) blacklist_users = os.getenv("BLACKLIST_USERS", None)
servers = generate_server_connections() whitelist_users = os.getenv("WHITELIST_USERS", None)
for server_1 in servers: 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)
# If server is the final server in the list, then we are done with the loop
if server_1 == servers[-1]: # Create server connections
break logger("Creating server connections", 1)
servers = generate_server_connections()
# Start server_2 at the next server in the list
for server_2 in servers[servers.index(server_1) + 1:]: for server_1 in servers:
# If server is the final server in the list, then we are done with the loop
server_1_connection = server_1[1] if server_1 == servers[-1]:
server_2_connection = server_2[1] break
# Create users list # Start server_2 at the next server in the list
logger("Creating users list", 1) for server_2 in servers[servers.index(server_1) + 1:]:
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
server_1_connection = server_1[1]
logger("Creating watched lists", 1) server_2_connection = server_2[1]
args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]
, [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]] # Create users list
logger("Creating users list", 1)
results = future_thread_executor(args) server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
server_1_watched = results[0]
server_2_watched = results[1] logger("Creating watched lists", 1)
logger(f"Server 1 watched: {server_1_watched}", 3) server_1_watched = server_1_connection.get_watched(server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
logger(f"Server 2 watched: {server_2_watched}", 3) 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))
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed logger("Finished creating watched list server 2", 1)
server_1_watched_filtered = copy.deepcopy(server_1_watched) logger(f"Server 1 watched: {server_1_watched}", 3)
server_2_watched_filtered = copy.deepcopy(server_2_watched) logger(f"Server 2 watched: {server_2_watched}", 3)
logger("Cleaning Server 1 Watched", 1) # clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping) server_1_watched_filtered = copy.deepcopy(server_1_watched)
server_2_watched_filtered = copy.deepcopy(server_2_watched)
logger("Cleaning Server 2 Watched", 1)
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping) logger("Cleaning Server 1 Watched", 1)
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_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) logger("Cleaning Server 2 Watched", 1)
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping)
args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun]
, [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]] 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)
future_thread_executor(args)
server_1_connection.update_watched(server_2_watched_filtered, user_mapping, library_mapping, dryrun)
def main(): asyncio.run(server_2_connection.update_watched(server_1_watched_filtered, user_mapping, library_mapping, dryrun))
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
def main():
while(True): sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
try: times = []
main_loop() while(True):
logger(f"Looping in {sleep_duration}") try:
sleep(sleep_duration) start = perf_counter()
except Exception as error: main_loop()
if isinstance(error, list): end = perf_counter()
for message in error: times.append(end - start)
logger(message, log_type=2)
else: logger(f"Looping in {sleep_duration}")
logger(error, log_type=2) sleep(sleep_duration)
except Exception as error:
logger(traceback.format_exc(), 2) if isinstance(error, list):
logger(f"Retrying in {sleep_duration}", log_type=0) for message in error:
sleep(sleep_duration) logger(message, log_type=2)
else:
except KeyboardInterrupt: logger(error, log_type=2)
logger("Exiting", log_type=0)
os._exit(0)
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)

View File

@@ -1,312 +1,318 @@
import re, requests import re, requests
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor
# class plex accept base url and token and username and password but default with none # class plex accept base url and token and username and password but default with none
class Plex: class Plex:
def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None, ssl_bypass=False): def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None, ssl_bypass=False):
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
self.username = username self.username = username
self.password = password self.password = password
self.servername = servername self.servername = servername
self.plex = self.login() self.plex = self.login(ssl_bypass)
self.admin_user = self.plex.myPlexAccount() self.admin_user = self.plex.myPlexAccount()
self.users = self.get_users() self.users = self.get_users()
def login(self): def login(self, ssl_bypass=False):
try: try:
if self.baseurl and self.token: if self.baseurl and self.token:
# Login via token # Login via token
plex = PlexServer(self.baseurl, self.token) if ssl_bypass:
elif self.username and self.password and self.servername: session = requests.Session()
# Login via plex account session.verify = False
account = MyPlexAccount(self.username, self.password) plex = PlexServer(self.baseurl, self.token, session=session)
plex = account.resource(self.servername).connect() else:
else: plex = PlexServer(self.baseurl, self.token)
raise Exception("No complete plex credentials provided") elif self.username and self.password and self.servername:
# Login via plex account
return plex account = MyPlexAccount(self.username, self.password)
except Exception as e: plex = account.resource(self.servername).connect()
if self.username or self.password: else:
msg = f"Failed to login via plex account {self.username}" raise Exception("No complete plex credentials provided")
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
else: return plex
logger(f"Plex: Failed to login, Error: {e}", 2) except Exception as e:
raise Exception(e) if self.username or self.password:
msg = f"Failed to login via plex account {self.username}"
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
def get_users(self): else:
try: logger(f"Plex: Failed to login, Error: {e}", 2)
users = self.plex.myPlexAccount().users() raise Exception(e)
# append self to users
users.append(self.plex.myPlexAccount()) def get_users(self):
try:
return users users = self.plex.myPlexAccount().users()
except Exception as e:
logger(f"Plex: Failed to get users, Error: {e}", 2) # append self to users
raise Exception(e) users.append(self.plex.myPlexAccount())
def get_user_watched(self, user, user_plex, library): return users
try: except Exception as e:
user_name = user.title.lower() logger(f"Plex: Failed to get users, Error: {e}", 2)
user_watched = {} raise Exception(e)
user_watched[user_name] = {}
logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0) def get_user_watched(self, user, user_plex, library):
try:
if library.type == "movie": user_name = user.title.lower()
user_watched[user_name][library.title] = [] user_watched = {}
user_watched[user_name] = {}
library_videos = user_plex.library.section(library.title)
for video in library_videos.search(unwatched=False): logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0)
movie_guids = {}
for guid in video.guids: if library.type == "movie":
guid_source = re.search(r'(.*)://', guid.id).group(1).lower() user_watched[user_name][library.title] = []
guid_id = re.search(r'://(.*)', guid.id).group(1)
movie_guids[guid_source] = guid_id library_videos = user_plex.library.section(library.title)
for video in library_videos.search(unwatched=False):
movie_guids["title"] = video.title movie_guids = {}
movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations]) for guid in video.guids:
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
user_watched[user_name][library.title].append(movie_guids) guid_id = re.search(r'://(.*)', guid.id).group(1)
movie_guids[guid_source] = guid_id
elif library.type == "show":
user_watched[user_name][library.title] = {} movie_guids["title"] = video.title
movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations])
library_videos = user_plex.library.section(library.title)
for show in library_videos.search(unwatched=False): user_watched[user_name][library.title].append(movie_guids)
show_guids = {}
for show_guid in show.guids: elif library.type == "show":
# Extract after :// from guid.id user_watched[user_name][library.title] = {}
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) library_videos = user_plex.library.section(library.title)
show_guids[show_guid_source] = show_guid_id for show in library_videos.search(unwatched=False):
show_guids = {}
show_guids["title"] = show.title for show_guid in show.guids:
show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations]) # Extract after :// from guid.id
show_guids = frozenset(show_guids.items()) show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
for season in show.seasons(): show_guids[show_guid_source] = show_guid_id
episode_guids = []
for episode in season.episodes(): show_guids["title"] = show.title
if episode.viewCount > 0: show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations])
episode_guids_temp = {} show_guids = frozenset(show_guids.items())
for guid in episode.guids:
# Extract after :// from guid.id for season in show.seasons():
guid_source = re.search(r'(.*)://', guid.id).group(1).lower() episode_guids = []
guid_id = re.search(r'://(.*)', guid.id).group(1) for episode in season.episodes():
episode_guids_temp[guid_source] = guid_id if episode.viewCount > 0:
episode_guids_temp = {}
episode_guids_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations]) for guid in episode.guids:
episode_guids.append(episode_guids_temp) # Extract after :// from guid.id
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
if episode_guids: guid_id = re.search(r'://(.*)', guid.id).group(1)
# append show, season, episode episode_guids_temp[guid_source] = guid_id
if show_guids not in user_watched[user_name][library.title]:
user_watched[user_name][library.title][show_guids] = {} episode_guids_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations])
if season.title not in user_watched[user_name][library.title][show_guids]: episode_guids.append(episode_guids_temp)
user_watched[user_name][library.title][show_guids][season.title] = {}
user_watched[user_name][library.title][show_guids][season.title] = episode_guids if episode_guids:
# append show, season, episode
if show_guids not in user_watched[user_name][library.title]:
return user_watched user_watched[user_name][library.title][show_guids] = {}
except Exception as e: if season.title not in user_watched[user_name][library.title][show_guids]:
logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2) user_watched[user_name][library.title][show_guids][season.title] = {}
raise Exception(e) user_watched[user_name][library.title][show_guids][season.title] = episode_guids
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): return user_watched
try: except Exception as e:
# Get all libraries logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2)
users_watched = {} raise Exception(e)
args = []
for user in users: def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
if self.admin_user == user: try:
user_plex = self.plex # Get all libraries
else: users_watched = {}
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) args = []
libraries = user_plex.library.sections() for user in users:
if self.admin_user == user:
for library in libraries: user_plex = self.plex
library_title = library.title else:
library_type = library.type user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) libraries = user_plex.library.sections()
if skip_reason: for library in libraries:
logger(f"Plex: Skipping library {library_title} {skip_reason}", 1) library_title = library.title
continue library_type = library.type
args.append([self.get_user_watched, user, user_plex, library]) skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
for user_watched in future_thread_executor(args): if skip_reason:
for user, user_watched_temp in user_watched.items(): logger(f"Plex: Skipping library {library_title} {skip_reason}", 1)
if user not in users_watched: continue
users_watched[user] = {}
users_watched[user].update(user_watched_temp) args.append([self.get_user_watched, user, user_plex, library])
return users_watched for user_watched in future_thread_executor(args):
except Exception as e: for user, user_watched_temp in user_watched.items():
logger(f"Plex: Failed to get watched, Error: {e}", 2) if user not in users_watched:
raise Exception(e) users_watched[user] = {}
users_watched[user].update(user_watched_temp)
def update_user_watched (self, user, user_plex, library, videos, dryrun): return users_watched
try: except Exception as e:
logger(f"Plex: Updating watched for {user.title} in library {library}", 1) logger(f"Plex: Failed to get watched, Error: {e}", 2)
videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos) raise Exception(e)
logger(f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1)
library_videos = user_plex.library.section(library) def update_user_watched (self, user, user_plex, library, videos, dryrun):
if videos_movies_ids: try:
for movies_search in library_videos.search(unwatched=True): logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
movie_found = False videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos)
for movie_location in movies_search.locations: logger(f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1)
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
movie_found = True library_videos = user_plex.library.section(library)
break if videos_movies_ids:
for movies_search in library_videos.search(unwatched=True):
if not movie_found: movie_found = False
for movie_guid in movies_search.guids: for movie_location in movies_search.locations:
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower() if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1) movie_found = True
break
# 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 not movie_found:
if movie_guid_id in videos_movies_ids[movie_guid_source]: for movie_guid in movies_search.guids:
movie_found = True movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
break movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
if movie_found: # If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex" if movie_guid_source in videos_movies_ids.keys():
if not dryrun: if movie_guid_id in videos_movies_ids[movie_guid_source]:
logger(f"Marked {msg}", 0) movie_found = True
movies_search.markWatched() break
else:
logger(f"Dryrun {msg}", 0) if movie_found:
else: msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
logger(f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 1) if not dryrun:
logger(f"Marked {msg}", 0)
movies_search.markWatched()
if videos_shows_ids and videos_episodes_ids: else:
for show_search in library_videos.search(unwatched=True): logger(f"Dryrun {msg}", 0)
show_found = False else:
for show_location in show_search.locations: logger(f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 1)
if show_location.split("/")[-1] in videos_shows_ids["locations"]:
show_found = True
break if videos_shows_ids and videos_episodes_ids:
for show_search in library_videos.search(unwatched=True):
if not show_found: show_found = False
for show_guid in show_search.guids: for show_location in show_search.locations:
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower() if show_location.split("/")[-1] in videos_shows_ids["locations"]:
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) show_found = True
break
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
if show_guid_source in videos_shows_ids.keys(): if not show_found:
if show_guid_id in videos_shows_ids[show_guid_source]: for show_guid in show_search.guids:
show_found = True show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
break show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
if show_found: # If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
for episode_search in show_search.episodes(): if show_guid_source in videos_shows_ids.keys():
episode_found = False if show_guid_id in videos_shows_ids[show_guid_source]:
show_found = True
for episode_location in episode_search.locations: break
if episode_location.split("/")[-1] in videos_episodes_ids["locations"]:
episode_found = True if show_found:
break for episode_search in show_search.episodes():
episode_found = False
if not episode_found:
for episode_guid in episode_search.guids: for episode_location in episode_search.locations:
episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower() if episode_location.split("/")[-1] in videos_episodes_ids["locations"]:
episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1) episode_found = True
break
# If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
if episode_guid_source in videos_episodes_ids.keys(): if not episode_found:
if episode_guid_id in videos_episodes_ids[episode_guid_source]: for episode_guid in episode_search.guids:
episode_found = True episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower()
break episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1)
if episode_found: # If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex" if episode_guid_source in videos_episodes_ids.keys():
if not dryrun: if episode_guid_id in videos_episodes_ids[episode_guid_source]:
logger(f"Marked {msg}", 0) episode_found = True
episode_search.markWatched() break
else:
logger(f"Dryrun {msg}", 0) if episode_found:
else: msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
logger(f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", 1) if not dryrun:
else: logger(f"Marked {msg}", 0)
logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1) episode_search.markWatched()
else:
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids: logger(f"Dryrun {msg}", 0)
logger(f"Jellyfin: No videos to mark as watched for {user.title} in library {library}", 1) else:
logger(f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", 1)
except Exception as e: else:
logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2) logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1)
raise Exception(e)
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)
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
try: except Exception as e:
args = [] logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2)
raise Exception(e)
for user, libraries in watched_list.items():
user_other = None
# If type of user is dict def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
if user_mapping: try:
if user in user_mapping.keys(): args = []
user_other = user_mapping[user]
elif user in user_mapping.values(): for user, libraries in watched_list.items():
user_other = search_mapping(user_mapping, user) user_other = None
# If type of user is dict
for index, value in enumerate(self.users): if user_mapping:
if user.lower() == value.title.lower(): if user in user_mapping.keys():
user = self.users[index] user_other = user_mapping[user]
break elif user in user_mapping.values():
elif user_other and user_other.lower() == value.title.lower(): user_other = search_mapping(user_mapping, user)
user = self.users[index]
break for index, value in enumerate(self.users):
if user.lower() == value.title.lower():
if self.admin_user == user: user = self.users[index]
user_plex = self.plex break
else: elif user_other and user_other.lower() == value.title.lower():
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) user = self.users[index]
break
for library, videos in libraries.items():
library_other = None if self.admin_user == user:
if library_mapping: user_plex = self.plex
if library in library_mapping.keys(): else:
library_other = library_mapping[library] user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library) for library, videos in libraries.items():
library_other = None
# if library in plex library list if library_mapping:
library_list = user_plex.library.sections() if library in library_mapping.keys():
if library.lower() not in [x.title.lower() for x in library_list]: library_other = library_mapping[library]
if library_other: elif library in library_mapping.values():
if library_other.lower() in [x.title.lower() for x in library_list]: library_other = search_mapping(library_mapping, library)
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
library = library_other # if library in plex library list
else: library_list = user_plex.library.sections()
logger(f"Plex: Library {library} or {library_other} not found in library list", 2) if library.lower() not in [x.title.lower() for x in library_list]:
continue if library_other:
else: if library_other.lower() in [x.title.lower() for x in library_list]:
logger(f"Plex: Library {library} not found in library list", 2) logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
continue library = library_other
else:
logger(f"Plex: Library {library} or {library_other} not found in library list", 2)
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) continue
else:
future_thread_executor(args) logger(f"Plex: Library {library} not found in library list", 2)
except Exception as e: continue
logger(f"Plex: Failed to update watched, Error: {e}", 2)
raise Exception(e)
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)

View File

@@ -1 +1 @@
pytest pytest

View File

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

View File

@@ -1,176 +1,176 @@
import sys import sys
import os import os
# getting the name of the directory # getting the name of the directory
# where the this file is present. # where the this file is present.
current = os.path.dirname(os.path.realpath(__file__)) current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name # Getting the parent directory name
# where the current directory is present. # where the current directory is present.
parent = os.path.dirname(current) parent = os.path.dirname(current)
# adding the parent directory to # adding the parent directory to
# the sys.path. # the sys.path.
sys.path.append(parent) sys.path.append(parent)
from src.main import cleanup_watched from src.main import cleanup_watched
tv_shows_watched_list_1 = { tv_shows_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)},
{'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)} {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E01.mkv',)}, {'locations': ('Test S01E01.mkv',)},
{'locations': ('Test S01E02.mkv',)} {'locations': ('Test S01E02.mkv',)}
] ]
} }
} }
movies_watched_list_1 = [ movies_watched_list_1 = [
{"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, {"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',)}, {"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)},
] ]
tv_shows_watched_list_2 = { tv_shows_watched_list_2 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)}, {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)},
{'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E02.mkv',)}, {'locations': ('Test S01E02.mkv',)},
{'locations': ('Test S01E03.mkv',)} {'locations': ('Test S01E03.mkv',)}
] ]
} }
} }
movies_watched_list_2 = [ movies_watched_list_2 = [
{"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, {"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)},
{'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} {'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)}
] ]
# Test to see if objects get deleted all the way up to the root. # Test to see if objects get deleted all the way up to the root.
tv_shows_2_watched_list_1 = { tv_shows_2_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)},
] ]
} }
} }
expected_tv_show_watched_list_1 = { expected_tv_show_watched_list_1 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)} {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E01.mkv',)} {'locations': ('Test S01E01.mkv',)}
] ]
} }
} }
expected_movie_watched_list_1 = [ expected_movie_watched_list_1 = [
{"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)} {"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)}
] ]
expected_tv_show_watched_list_2 = { expected_tv_show_watched_list_2 = {
frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): {
"Season 1": [ "Season 1": [
{'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)}
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "Season 1": [
{'locations': ('Test S01E03.mkv',)} {'locations': ('Test S01E03.mkv',)}
] ]
} }
} }
expected_movie_watched_list_2 = [ expected_movie_watched_list_2 = [
{'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} {'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)}
] ]
def test_simple_cleanup_watched(): def test_simple_cleanup_watched():
user_watched_list_1 = { user_watched_list_1 = {
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_1, "TV Shows": tv_shows_watched_list_1,
"Movies": movies_watched_list_1, "Movies": movies_watched_list_1,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1
}, },
} }
user_watched_list_2 = { user_watched_list_2 = {
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_2, "TV Shows": tv_shows_watched_list_2,
"Movies": movies_watched_list_2, "Movies": movies_watched_list_2,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1
} }
} }
expected_watched_list_1 = { expected_watched_list_1 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_1 "TV Shows": expected_tv_show_watched_list_1
, "Movies": expected_movie_watched_list_1 , "Movies": expected_movie_watched_list_1
} }
} }
expected_watched_list_2 = { expected_watched_list_2 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_2 "TV Shows": expected_tv_show_watched_list_2
, "Movies": expected_movie_watched_list_2 , "Movies": expected_movie_watched_list_2
} }
} }
return_watched_list_1 = cleanup_watched(user_watched_list_1, user_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) 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_1 == expected_watched_list_1
assert return_watched_list_2 == expected_watched_list_2 assert return_watched_list_2 == expected_watched_list_2
def test_mapping_cleanup_watched(): def test_mapping_cleanup_watched():
user_watched_list_1 = { user_watched_list_1 = {
"user1": { "user1": {
"TV Shows": tv_shows_watched_list_1, "TV Shows": tv_shows_watched_list_1,
"Movies": movies_watched_list_1, "Movies": movies_watched_list_1,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1
}, },
} }
user_watched_list_2 = { user_watched_list_2 = {
"user2": { "user2": {
"Shows": tv_shows_watched_list_2, "Shows": tv_shows_watched_list_2,
"Movies": movies_watched_list_2, "Movies": movies_watched_list_2,
"Other Shows": tv_shows_2_watched_list_1 "Other Shows": tv_shows_2_watched_list_1
} }
} }
expected_watched_list_1 = { expected_watched_list_1 = {
"user1": { "user1": {
"TV Shows": expected_tv_show_watched_list_1 "TV Shows": expected_tv_show_watched_list_1
, "Movies": expected_movie_watched_list_1 , "Movies": expected_movie_watched_list_1
} }
} }
expected_watched_list_2 = { expected_watched_list_2 = {
"user2": { "user2": {
"Shows": expected_tv_show_watched_list_2 "Shows": expected_tv_show_watched_list_2
, "Movies": expected_movie_watched_list_2 , "Movies": expected_movie_watched_list_2
} }
} }
user_mapping = { "user1": "user2" } user_mapping = { "user1": "user2" }
library_mapping = { "TV Shows": "Shows" } library_mapping = { "TV Shows": "Shows" }
return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2, user_mapping=user_mapping, library_mapping=library_mapping) return_watched_list_1 = cleanup_watched(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) return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1, user_mapping=user_mapping, library_mapping=library_mapping)
assert return_watched_list_1 == expected_watched_list_1 assert return_watched_list_1 == expected_watched_list_1
assert return_watched_list_2 == expected_watched_list_2 assert return_watched_list_2 == expected_watched_list_2