Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1fd61f1d1 | ||
|
|
6c1ee4a7dc | ||
|
|
9a8e799e68 | ||
|
|
ffec4e2f28 | ||
|
|
00102891a5 | ||
|
|
aa76b83428 | ||
|
|
a644189ea5 | ||
|
|
c5d987a8c9 | ||
|
|
bdd68ad68d | ||
|
|
2d86bca781 | ||
|
|
1b01ff6ec2 | ||
|
|
f08ec43507 | ||
|
|
7f9424260a | ||
|
|
5f21943353 | ||
|
|
a5a795f43c | ||
|
|
fcb6d7625f | ||
|
|
fd2179998f | ||
|
|
654e7f20e1 | ||
|
|
1eb92cf7c1 | ||
|
|
111e284cc8 | ||
|
|
1a4e3f4ec4 | ||
|
|
4066228e57 | ||
|
|
59c6d278e3 | ||
|
|
39b33f3d43 | ||
|
|
e8faf52b2b | ||
|
|
370e9bac63 | ||
|
|
d0746cec5a | ||
|
|
251937431b | ||
|
|
50faf061af | ||
|
|
9ffbc49ad3 | ||
|
|
644dc8e3af | ||
|
|
47bc4e94dc | ||
|
|
f17d39fe17 | ||
|
|
966dcacf8d | ||
|
|
9afc00443c | ||
|
|
3ec177ea64 | ||
|
|
b360c9fd0b | ||
|
|
1ed791b1ed | ||
|
|
f19b1a3063 | ||
|
|
190a72bd3c | ||
|
|
c848106ce7 | ||
|
|
dd319271bd | ||
|
|
16879cc728 | ||
|
|
942ec3533f | ||
|
|
9f6edfc91a | ||
|
|
827ace2e97 | ||
|
|
f6b57a1b4d | ||
|
|
88a7526721 | ||
|
|
1efb4d8543 |
110
.env.sample
110
.env.sample
@@ -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"
|
||||||
|
|||||||
172
.github/workflows/ci.yml
vendored
172
.github/workflows/ci.yml
vendored
@@ -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
41
.github/workflows/codeql.yml
vendored
Normal 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
264
.gitignore
vendored
@@ -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
32
.vscode/launch.json
vendored
@@ -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
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python.formatting.provider": "black"
|
||||||
|
}
|
||||||
76
Dockerfile
76
Dockerfile
@@ -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"]
|
||||||
|
|||||||
216
README.md
216
README.md
@@ -1,73 +1,143 @@
|
|||||||
# JellyPlex-Watched
|
# JellyPlex-Watched
|
||||||
|
|
||||||
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&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
21
main.py
@@ -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()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
plexapi
|
plexapi
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
aiohttp
|
||||||
|
|||||||
632
src/functions.py
632
src/functions.py
@@ -1,155 +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):
|
|
||||||
show_output_dict = {}
|
if whitelist_library:
|
||||||
episode_output_dict = {}
|
if len(whitelist_library) > 0:
|
||||||
movies_output_dict = {}
|
whitelist_library = whitelist_library.split(",")
|
||||||
|
whitelist_library = [x.strip() for x in whitelist_library]
|
||||||
try:
|
if library_mapping:
|
||||||
show_output_keys = user_list.keys()
|
temp_library = []
|
||||||
show_output_keys = ([ dict(x) for x in list(show_output_keys) ])
|
for library in whitelist_library:
|
||||||
for show_key in show_output_keys:
|
library_other = search_mapping(library_mapping, library)
|
||||||
for provider_key, provider_value in show_key.items():
|
if library_other:
|
||||||
# Skip title
|
temp_library.append(library_other)
|
||||||
if provider_key.lower() == "title":
|
|
||||||
continue
|
whitelist_library = whitelist_library + temp_library
|
||||||
if provider_key.lower() not in show_output_dict:
|
else:
|
||||||
show_output_dict[provider_key.lower()] = []
|
whitelist_library = []
|
||||||
if provider_key.lower() == "locations":
|
logger(f"Whitelist Library: {whitelist_library}", 1)
|
||||||
for show_location in provider_value:
|
|
||||||
show_output_dict[provider_key.lower()].append(show_location)
|
if blacklist_library_type:
|
||||||
else:
|
if len(blacklist_library_type) > 0:
|
||||||
show_output_dict[provider_key.lower()].append(provider_value.lower())
|
blacklist_library_type = blacklist_library_type.split(",")
|
||||||
except:
|
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
|
||||||
pass
|
else:
|
||||||
|
blacklist_library_type = []
|
||||||
try:
|
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
|
||||||
for show in user_list:
|
|
||||||
for season in user_list[show]:
|
if whitelist_library_type:
|
||||||
for episode in user_list[show][season]:
|
if len(whitelist_library_type) > 0:
|
||||||
for episode_key, episode_value in episode.items():
|
whitelist_library_type = whitelist_library_type.split(",")
|
||||||
if episode_key.lower() not in episode_output_dict:
|
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
|
||||||
episode_output_dict[episode_key.lower()] = []
|
else:
|
||||||
if episode_key == "locations":
|
whitelist_library_type = []
|
||||||
for episode_location in episode_value:
|
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
|
||||||
episode_output_dict[episode_key.lower()].append(episode_location)
|
|
||||||
else:
|
if blacklist_users:
|
||||||
episode_output_dict[episode_key.lower()].append(episode_value.lower())
|
if len(blacklist_users) > 0:
|
||||||
except:
|
blacklist_users = blacklist_users.split(",")
|
||||||
pass
|
blacklist_users = [x.lower().strip() for x in blacklist_users]
|
||||||
|
if user_mapping:
|
||||||
try:
|
temp_users = []
|
||||||
for movie in user_list:
|
for user in blacklist_users:
|
||||||
for movie_key, movie_value in movie.items():
|
user_other = search_mapping(user_mapping, user)
|
||||||
if movie_key.lower() not in movies_output_dict:
|
if user_other:
|
||||||
movies_output_dict[movie_key.lower()] = []
|
temp_users.append(user_other)
|
||||||
if movie_key == "locations":
|
|
||||||
for movie_location in movie_value:
|
blacklist_users = blacklist_users + temp_users
|
||||||
movies_output_dict[movie_key.lower()].append(movie_location)
|
else:
|
||||||
else:
|
blacklist_users = []
|
||||||
movies_output_dict[movie_key.lower()].append(movie_value.lower())
|
logger(f"Blacklist Users: {blacklist_users}", 1)
|
||||||
except:
|
|
||||||
pass
|
if whitelist_users:
|
||||||
|
if len(whitelist_users) > 0:
|
||||||
return show_output_dict, episode_output_dict, movies_output_dict
|
whitelist_users = whitelist_users.split(",")
|
||||||
|
whitelist_users = [x.lower().strip() for x in whitelist_users]
|
||||||
def future_thread_executor(args: list, workers: int = -1):
|
if user_mapping:
|
||||||
futures_list = []
|
temp_users = []
|
||||||
results = []
|
for user in whitelist_users:
|
||||||
|
user_other = search_mapping(user_mapping, user)
|
||||||
if workers == -1:
|
if user_other:
|
||||||
workers = min(32, os.cpu_count()*1.25)
|
temp_users.append(user_other)
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
whitelist_users = whitelist_users + temp_users
|
||||||
for arg in args:
|
else:
|
||||||
# * arg unpacks the list into actual arguments
|
whitelist_users = []
|
||||||
futures_list.append(executor.submit(*arg))
|
else:
|
||||||
|
whitelist_users = []
|
||||||
for future in futures_list:
|
logger(f"Whitelist Users: {whitelist_users}", 1)
|
||||||
try:
|
|
||||||
result = future.result()
|
return (
|
||||||
results.append(result)
|
blacklist_library,
|
||||||
except Exception as e:
|
whitelist_library,
|
||||||
raise Exception(e)
|
blacklist_library_type,
|
||||||
|
whitelist_library_type,
|
||||||
return results
|
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
|
||||||
|
|||||||
1058
src/jellyfin.py
1058
src/jellyfin.py
File diff suppressed because it is too large
Load Diff
861
src/main.py
861
src/main.py
@@ -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])
|
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)
|
||||||
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2])
|
|
||||||
|
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 {episode} from {show_key_dict['title']}", 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 {show_key_dict['title']} 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)
|
|
||||||
|
|||||||
771
src/plex.py
771
src/plex.py
@@ -1,312 +1,459 @@
|
|||||||
import re, requests
|
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, ssl_bypass=False):
|
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:
|
||||||
videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos)
|
logger(
|
||||||
logger(f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1)
|
f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}",
|
||||||
|
1,
|
||||||
library_videos = user_plex.library.section(library)
|
)
|
||||||
if videos_movies_ids:
|
|
||||||
for movies_search in library_videos.search(unwatched=True):
|
if videos_shows_ids and videos_episodes_ids:
|
||||||
movie_found = False
|
for show_search in library_videos.search(unwatched=True):
|
||||||
for movie_location in movies_search.locations:
|
show_found = False
|
||||||
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
|
for show_location in show_search.locations:
|
||||||
movie_found = True
|
if show_location.split("/")[-1] in videos_shows_ids["locations"]:
|
||||||
break
|
show_found = True
|
||||||
|
break
|
||||||
if not movie_found:
|
|
||||||
for movie_guid in movies_search.guids:
|
if not show_found:
|
||||||
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
|
for show_guid in show_search.guids:
|
||||||
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
|
show_guid_source = (
|
||||||
|
re.search(r"(.*)://", show_guid.id).group(1).lower()
|
||||||
# If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
|
)
|
||||||
if movie_guid_source in videos_movies_ids.keys():
|
show_guid_id = re.search(r"://(.*)", show_guid.id).group(1)
|
||||||
if movie_guid_id in videos_movies_ids[movie_guid_source]:
|
|
||||||
movie_found = True
|
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
|
||||||
break
|
if show_guid_source in videos_shows_ids.keys():
|
||||||
|
if show_guid_id in videos_shows_ids[show_guid_source]:
|
||||||
if movie_found:
|
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)
|
|
||||||
else:
|
for episode_location in episode_search.locations:
|
||||||
logger(f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 1)
|
if (
|
||||||
|
episode_location.split("/")[-1]
|
||||||
|
in videos_episodes_ids["locations"]
|
||||||
if videos_shows_ids and videos_episodes_ids:
|
):
|
||||||
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_episodes_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_episodes_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_episodes_ids.keys():
|
3,
|
||||||
if episode_guid_id in videos_episodes_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,
|
||||||
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
)
|
||||||
if not dryrun:
|
|
||||||
logger(f"Marked {msg}", 0)
|
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids:
|
||||||
episode_search.markWatched()
|
logger(
|
||||||
else:
|
f"Jellyfin: No videos to mark as watched for {user.title} in library {library}",
|
||||||
logger(f"Dryrun {msg}", 0)
|
1,
|
||||||
else:
|
)
|
||||||
logger(f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", 1)
|
|
||||||
else:
|
except Exception as e:
|
||||||
logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1)
|
logger(
|
||||||
|
f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}",
|
||||||
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids:
|
2,
|
||||||
logger(f"Jellyfin: No videos to mark as watched for {user.title} in library {library}", 1)
|
)
|
||||||
|
raise Exception(e)
|
||||||
except Exception as e:
|
|
||||||
logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2)
|
|
||||||
raise Exception(e)
|
# class plex accept base url and token and username and password but default with none
|
||||||
|
class Plex:
|
||||||
|
def __init__(
|
||||||
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
|
self,
|
||||||
try:
|
baseurl=None,
|
||||||
args = []
|
token=None,
|
||||||
|
username=None,
|
||||||
for user, libraries in watched_list.items():
|
password=None,
|
||||||
user_other = None
|
servername=None,
|
||||||
# If type of user is dict
|
ssl_bypass=False,
|
||||||
if user_mapping:
|
session=None,
|
||||||
if user in user_mapping.keys():
|
):
|
||||||
user_other = user_mapping[user]
|
self.baseurl = baseurl
|
||||||
elif user in user_mapping.values():
|
self.token = token
|
||||||
user_other = search_mapping(user_mapping, user)
|
self.username = username
|
||||||
|
self.password = password
|
||||||
for index, value in enumerate(self.users):
|
self.servername = servername
|
||||||
if user.lower() == value.title.lower():
|
self.ssl_bypass = ssl_bypass
|
||||||
user = self.users[index]
|
if ssl_bypass:
|
||||||
break
|
# Session for ssl bypass
|
||||||
elif user_other and user_other.lower() == value.title.lower():
|
session = requests.Session()
|
||||||
user = self.users[index]
|
# By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
||||||
break
|
session.mount("https://", HostNameIgnoringAdapter())
|
||||||
|
self.session = session
|
||||||
if self.admin_user == user:
|
self.plex = self.login(self.baseurl, self.token)
|
||||||
user_plex = self.plex
|
self.admin_user = self.plex.myPlexAccount()
|
||||||
else:
|
self.users = self.get_users()
|
||||||
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
|
|
||||||
|
def login(self, baseurl, token):
|
||||||
for library, videos in libraries.items():
|
try:
|
||||||
library_other = None
|
if baseurl and token:
|
||||||
if library_mapping:
|
plex = PlexServer(baseurl, token, session=self.session)
|
||||||
if library in library_mapping.keys():
|
elif self.username and self.password and self.servername:
|
||||||
library_other = library_mapping[library]
|
# Login via plex account
|
||||||
elif library in library_mapping.values():
|
account = MyPlexAccount(self.username, self.password)
|
||||||
library_other = search_mapping(library_mapping, library)
|
plex = account.resource(self.servername).connect()
|
||||||
|
else:
|
||||||
# if library in plex library list
|
raise Exception("No complete plex credentials provided")
|
||||||
library_list = user_plex.library.sections()
|
|
||||||
if library.lower() not in [x.title.lower() for x in library_list]:
|
return plex
|
||||||
if library_other:
|
except Exception as e:
|
||||||
if library_other.lower() in [x.title.lower() for x in library_list]:
|
if self.username or self.password:
|
||||||
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
|
msg = f"Failed to login via plex account {self.username}"
|
||||||
library = library_other
|
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
|
||||||
else:
|
else:
|
||||||
logger(f"Plex: Library {library} or {library_other} not found in library list", 2)
|
logger(f"Plex: Failed to login, Error: {e}", 2)
|
||||||
continue
|
raise Exception(e)
|
||||||
else:
|
|
||||||
logger(f"Plex: Library {library} not found in library list", 2)
|
def get_users(self):
|
||||||
continue
|
try:
|
||||||
|
users = self.plex.myPlexAccount().users()
|
||||||
|
|
||||||
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun])
|
# append self to users
|
||||||
|
users.append(self.plex.myPlexAccount())
|
||||||
future_thread_executor(args)
|
|
||||||
except Exception as e:
|
return users
|
||||||
logger(f"Plex: Failed to update watched, Error: {e}", 2)
|
except Exception as e:
|
||||||
raise Exception(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)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
pytest
|
pytest
|
||||||
|
|||||||
78
test/test_main.py
Normal file
78
test/test_main.py
Normal 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"]
|
||||||
@@ -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']
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user