55 Commits

Author SHA1 Message Date
Luigi311
d1fd61f1d1 Merge pull request #38 from luigi311/dev
Fix issue with nested folders
2023-01-31 16:27:54 -07:00
Luigi311
6c1ee4a7dc Log server users 2023-01-30 11:56:27 -07:00
Luigi311
9a8e799e68 Recursive all the things. Use includeItemType 2023-01-30 11:46:12 -07:00
Luigi311
ffec4e2f28 Support multiple library types 2023-01-28 16:33:36 -07:00
Luigi311
00102891a5 Catch None for types 2023-01-27 23:45:03 -07:00
Luigi311
aa76b83428 Use isinstance instead of type 2023-01-27 12:21:38 -07:00
Luigi311
a644189ea5 Use isinstance instead of type 2023-01-27 12:18:15 -07:00
Luigi311
c5d987a8c9 Update .env.sample and README 2023-01-27 11:23:58 -07:00
Luigi311
bdd68ad68d If user is type str get plex object 2023-01-27 11:02:15 -07:00
Luigi311
2d86bca781 Update github actions 2023-01-27 10:48:52 -07:00
Luigi311
1b01ff6ec2 Log if multiple types and continue instead of error 2023-01-27 10:45:46 -07:00
Luigi311
f08ec43507 Skip library before erroring for multiple types. 2023-01-27 10:43:50 -07:00
Luigi311
7f9424260a Format 2023-01-26 14:03:13 -07:00
Luigi311
5f21943353 Exclude folders, use recursive. 2023-01-26 13:55:50 -07:00
Luigi311
a5a795f43c Exclude Folders from list 2023-01-26 13:42:35 -07:00
Luigi311
fcb6d7625f Fix invalid library types, raise mixed types 2023-01-26 13:31:57 -07:00
Luigi311
fd2179998f Fix ssl_bypass for plex 2023-01-26 11:23:47 -07:00
Luigi311
654e7f20e1 Merge pull request #33 from luigi311/dev
Lots of fixes and simplification
2022-12-23 23:13:22 -07:00
Luigi311
1eb92cf7c1 black formatting 2022-12-23 23:11:38 -07:00
Luigi311
111e284cc8 Cleanup 2022-12-23 23:10:51 -07:00
Luigi311
1a4e3f4ec4 Move setup_black_white_list to functions. Fix trailing slash on jellyfin baseurl 2022-12-23 23:02:53 -07:00
Luigi311
4066228e57 Add more debug logging. Do not enable debug by default 2022-12-19 14:07:56 -07:00
Luigi311
59c6d278e3 Add more logging to debug 2022-12-19 13:57:20 -07:00
Luigi311
39b33f3d43 Fix missing logging when using debug level 2022-12-19 13:22:42 -07:00
Luigi311
e8faf52b2b Do not mark shows/movies that do not exist 2022-12-19 01:35:16 -07:00
Luigi311
370e9bac63 change get user watched name to avoid mistakes 2022-12-18 22:39:03 -07:00
Luigi311
d0746cec5a Fix server 2 always running async runner. Speedup plex get watched 2022-12-18 22:27:42 -07:00
Luigi311
251937431b Move cleanup_watched to functions and simplify 2022-12-18 01:50:45 -07:00
Luigi311
50faf061af Remove dockerfile defaults 2022-11-21 18:21:29 -07:00
Luigi311
9ffbc49ad3 Merge pull request #30 from luigi311/dev
Add ssl_bypass to skip hostname validation.
2022-11-21 17:39:00 -07:00
Luigi311
644dc8e3af Merge pull request #29 from lgtm-migrator/codeql
Add CodeQL workflow for GitHub code scanning
2022-11-21 17:38:45 -07:00
Luigi311
47bc4e94dc Fix dockerfile 2022-11-21 17:31:47 -07:00
LGTM Migrator
f17d39fe17 Add CodeQL workflow for GitHub code scanning 2022-11-10 14:41:07 +00:00
Luigi311
966dcacf8d Add ssl_bypass to skip hostname validation. 2022-09-25 14:16:01 -06:00
Luigi311
9afc00443c Merge pull request #27 from luigi311/dev
Cleanup issues
2022-08-18 00:46:00 -06:00
Luigi311
3ec177ea64 rename test_main 2022-08-18 00:17:32 -06:00
Luigi311
b360c9fd0b Remove unnecessary deepcopy 2022-08-18 00:15:42 -06:00
Luigi311
1ed791b1ed Fix jellyfin 2022-08-17 23:49:05 -06:00
Luigi311
f19b1a3063 Cleanup length and functions instead of methods 2022-08-17 23:34:45 -06:00
Luigi311
190a72bd3c Cleanup 2022-08-17 22:53:27 -06:00
Luigi311
c848106ce7 Black cleanup 2022-08-17 22:31:23 -06:00
Luigi311
dd319271bd Cleanup 2022-08-17 22:09:11 -06:00
Luigi311
16879cc728 Merge pull request #26 from luigi311/dev
Use async for jellyfin
2022-08-17 21:49:34 -06:00
Luigi311
942ec3533f Cleanup log file on runs 2022-08-17 21:43:51 -06:00
Luigi311
9f6edfc91a Merge branch 'main' into dev 2022-08-17 21:40:25 -06:00
Luigi311
827ace2e97 cleanup 2022-08-17 21:20:28 -06:00
Luigi311
f6b57a1b4d Update README.md 2022-07-10 01:38:42 -06:00
Luigi311
88a7526721 Use async for jellyfin (#23)
* Use async

* Massive jellyfin watched speedup

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

View File

@@ -1,42 +1,68 @@
## Do not mark any shows/movies as played and instead just output to log if they would of been marked. # Global Settings
DRYRUN = "True"
## Additional logging information ## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
DEBUG = "True" DRYRUN = "True"
## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" ## Additional logging information
## How often to run the script in seconds DEBUG = "False"
SLEEP_DURATION = "3600"
## Log file where all output will be written to ## Debugging level, "info" is default, "debug" is more verbose
LOGFILE = "log.log" DEBUG_LEVEL = "info"
## Map usernames between plex and jellyfin in the event that they are different, order does not matter
#USER_MAPPING = { "testuser2": "testuser3" } ## How often to run the script in seconds
## Map libraries between plex and jellyfin in the even that they are different, order does not matter SLEEP_DURATION = "3600"
#LIBRARY_MAPPING = { "Shows": "TV Shows" }
## Log file where all output will be written to
LOGFILE = "log.log"
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly ## Map usernames between servers in the event that they are different, order does not matter
## Comma seperated list for multiple servers ## Comma seperated for multiple options
PLEX_BASEURL = "http://localhost:32400" #USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
PLEX_TOKEN = "SuperSecretToken" ## Map libraries between servers in the even that they are different, order does not matter
## If not using plex token then use username and password of the server admin along with the servername ## Comma seperated for multiple options
#PLEX_USERNAME = "" #LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
#PLEX_PASSWORD = ""
#PLEX_SERVERNAME = "Plex Server" ## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
## Comma seperated for multiple options
#BLACKLIST_LIBRARY = ""
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly #WHITELIST_LIBRARY = ""
## Comma seperated list for multiple servers #BLACKLIST_LIBRARY_TYPE = ""
JELLYFIN_BASEURL = "http://localhost:8096" #WHITELIST_LIBRARY_TYPE = ""
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key #BLACKLIST_USERS = ""
JELLYFIN_TOKEN = "SuperSecretToken" WHITELIST_USERS = "testuser1,testuser2"
## 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 = "" # Plex
#WHITELIST_LIBRARY = ""
#BLACKLIST_LIBRARY_TYPE = "" ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
#WHITELIST_LIBRARY_TYPE = "" ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
#BLACKLIST_USERS = "" ## Comma seperated list for multiple servers
WHITELIST_USERS = "testuser1,testuser2" PLEX_BASEURL = "http://localhost:32400, https://nas:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers
PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2"
## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options
#PLEX_USERNAME = "PlexUser, PlexUser2"
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
## Skip hostname validation for ssl certificates.
## Set to True if running into ssl certificate errors
SSL_BYPASS = "False"
# Jellyfin
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"

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@v3
- 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@v3
- 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@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- 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@v2
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@v3
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@v3
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 }}"

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

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

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
} }
] ]
} }

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.formatting.provider": "black"
}

View File

@@ -1,35 +1,41 @@
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 ''
ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }' ENV LIBRARY_MAPPING ''
ENV PLEX_BASEURL 'http://localhost:32400' ENV PLEX_BASEURL ''
ENV PLEX_TOKEN '' ENV PLEX_TOKEN ''
ENV PLEX_USERNAME '' ENV PLEX_USERNAME ''
ENV PLEX_PASSWORD '' ENV PLEX_PASSWORD ''
ENV PLEX_SERVERNAME '' ENV PLEX_SERVERNAME ''
ENV JELLYFIN_BASEURL 'http://localhost:8096' ENV JELLYFIN_BASEURL ''
ENV JELLYFIN_TOKEN '' ENV JELLYFIN_TOKEN ''
ENV BLACKLIST_LIBRARY '' ENV BLACKLIST_LIBRARY ''
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 ./ RUN apt-get update && \
RUN pip install --no-cache-dir -r requirements.txt apt-get install -y --no-install-recommends \
build-essential && \
COPY . . apt-get clean && \
rm -rf /var/lib/apt/lists/*
CMD ["python", "-u", "main.py"]
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-u", "main.py"]

1348
LICENSE

File diff suppressed because it is too large Load Diff

216
README.md
View File

@@ -1,73 +1,143 @@
# 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 locally
## 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 file names and provider ids 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
```bash
# Global Settings
## Installation
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
### Baremetal DRYRUN = "True"
- Setup virtualenv of your choice ## Additional logging information
DEBUG = "False"
- Install dependencies
## Debugging level, "info" is default, "debug" is more verbose
```bash DEBUG_LEVEL = "info"
pip install -r requirements.txt
``` ## How often to run the script in seconds
SLEEP_DURATION = "3600"
- Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens
## Log file where all output will be written to
- Run LOGFILE = "log.log"
```bash ## Map usernames between servers in the event that they are different, order does not matter
python main.py ## Comma seperated for multiple options
``` USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
### Docker ## Map libraries between servers in the even that they are different, order does not matter
## Comma seperated for multiple options
- Build docker image LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
```bash ## 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.
docker build -t jellyplex-watched . ## Comma seperated for multiple options
``` BLACKLIST_LIBRARY = ""
WHITELIST_LIBRARY = ""
- or use pre-built image BLACKLIST_LIBRARY_TYPE = ""
WHITELIST_LIBRARY_TYPE = ""
```bash BLACKLIST_USERS = ""
docker pull luigi311/jellyplex-watched:latest WHITELIST_USERS = "testuser1,testuser2"
```
#### With variables
# Plex
- Run
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
```bash ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest ## Comma seperated list for multiple servers
``` PLEX_BASEURL = "http://localhost:32400, https://nas:32400"
#### With .env ## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers
- Create a .env file similar to .env.sample and set the MNEMONIC variable to your seed phrase PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2"
- Run ## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options
```bash #PLEX_USERNAME = "PlexUser, PlexUser2"
docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest #PLEX_PASSWORD = "SuperSecret, SuperSecret2"
``` #PLEX_SERVERNAME = "Plex Server1, Plex Server2"
## Contributing ## Skip hostname validation for ssl certificates.
## Set to True if running into ssl certificate errors
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. SSL_BYPASS = "False"
## License
This is currently under the GNU General Public License v3.0. # Jellyfin
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
```
## Installation
### Baremetal
- Setup virtualenv of your choice
- Install dependencies
```bash
pip install -r requirements.txt
```
- Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens
- Run
```bash
python main.py
```
### Docker
- Build docker image
```bash
docker build -t jellyplex-watched .
```
- or use pre-built image
```bash
docker pull luigi311/jellyplex-watched:latest
```
#### With variables
- Run
```bash
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest
```
#### With .env
- Create a .env file similar to .env.sample and set the variables to match your setup
- Run
```bash
docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest
```
## 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.
## License
This is currently under the GNU General Public License v3.0.

21
main.py
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

78
test/test_main.py Normal file
View File

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

View File

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

View File

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