134 Commits

Author SHA1 Message Date
Luigi311
87b4a950f1 Merge pull request #75 from luigi311/dev
Variants, Pin versions, CI, Plex usernames
2023-05-17 13:38:25 -06:00
Luigi311
9f61c7338d Plex: Cleanup username_title 2023-05-17 13:22:00 -06:00
Luigi311
ffc81dad69 CI: Add back in dev based on alpine 2023-05-15 15:12:25 -06:00
Luigi311
7eba46b5cb plex: Fix username/title 2023-05-15 14:57:46 -06:00
Luigi311
aa177666a5 Plex: Fix username/title selection 2023-05-15 11:17:28 -06:00
Luigi311
7de7b42fd2 Users: Default to username and fall back to title 2023-05-15 11:10:03 -06:00
Luigi311
03d1fd8019 Log both servers users instead of exiting immediately 2023-05-15 10:44:30 -06:00
Luigi311
485ec5fe2d Add docker-compose file 2023-04-29 20:31:24 -06:00
Luigi311
59bfbd9811 Merge pull request #71 from luigi311/fix-docker-build/push
Do not publish on PR, fix condition check on build
2023-04-13 13:02:55 -06:00
Luigi311
1e485b37f8 Do not publish on PR, fix condition check on build 2023-04-13 12:56:52 -06:00
Luigi311
4adf94f24b Update ci.yml
Action: Use github.repository and github.actor instead
2023-04-13 10:28:01 -06:00
Luigi311
1a0fab36d3 Merge pull request #66 from Nicba1010/main
General build improvements
2023-04-13 09:50:59 -06:00
Roberto Banić
a1ef3b5a8d Add conditional to DockerHub login 2023-04-13 16:45:05 +02:00
Luigi311
0c47ee7119 Merge pull request #68 from Nicba1010/refactor-black-white
Refactor black/whitelist processing
2023-04-13 08:37:38 -06:00
Roberto Banić
e51cf6e482 Refactor black/whitelist processing 2023-04-13 12:56:28 +02:00
Roberto Banić
24d5de813d Remove DOCKER_USERNAME environment variable from docker_meta step 2023-04-13 11:23:32 +02:00
Roberto Banić
9921b2a355 Change is_default_branch to other default branch check 2023-04-13 11:21:28 +02:00
Roberto Banić
faa378c75e Add is_default_branch conditional to latest tag 2023-04-13 11:20:19 +02:00
Roberto Banić
26199100dc Update tags 2023-04-13 11:19:56 +02:00
Roberto Banić
bee854f059 Exclude DockerHub in case there is no username set 2023-04-13 10:48:03 +02:00
Roberto Banić
73c1ebf3ed Pin pytest version 2023-04-13 02:26:12 +02:00
Roberto Banić
397dd17429 Specify Python version 2023-04-13 02:26:11 +02:00
Roberto Banić
73d18dad92 Rename Dockerfile to Dockerfile.alpine 2023-04-13 02:26:10 +02:00
Roberto Banić
94d63a3fdb Add ghcr.io image name to the docker metadata action step 2023-04-13 02:26:09 +02:00
Roberto Banić
120d89e8be Add dashes to tags 2023-04-13 02:26:08 +02:00
Roberto Banić
eb5534c61c Add ghcr.io registry 2023-04-13 02:26:07 +02:00
Roberto Banić
99d217e8f1 Update ci.yml to perform a multi-variant build 2023-04-13 02:26:05 +02:00
Roberto Banić
f7e3f8ae2a Update Dockerfile to use the alpine Python 3 base image 2023-04-13 02:26:04 +02:00
Roberto Banić
2cebd2d73d Pin dependency versions to enable reproducible builds 2023-04-13 02:25:13 +02:00
Luigi311
18df322c41 Merge pull request #65 from luigi311/dev
Dev
2023-04-11 09:29:08 -06:00
Luigi311
fc80f50560 Fix codeql issues 2023-04-11 08:57:49 -06:00
Luigi311
4870ff9e7a Cleanup 2023-04-11 08:48:30 -06:00
Luigi311
58337bd38c Test: Use is None 2023-04-10 23:05:22 -06:00
Luigi311
e6d1e0933a Merge pull request #64 from luigi311/fix_indexing
Fix indexing with check_remove_entry
2023-04-10 17:20:36 -06:00
Luigi311
68e3f25ba4 Fix indexing 2023-04-10 16:59:54 -06:00
Luigi311
c981426db6 Merge pull request #62 from agustinmorantes/dev
Add "RUN_ONLY_ONCE" option
2023-04-10 11:54:17 -06:00
Agustín Morantes
916b16b12c Add "RUN_ONLY_ONCE" option 2023-04-10 14:39:28 -03:00
Luigi311
a178d230de Jellfyfin: Fix more issues with ids 2023-04-07 17:31:25 -06:00
Luigi311
fffb04728a Jellfyin: Fix issue with ids. Do not show marked for partial 2023-04-07 15:17:00 -06:00
Luigi311
658361383a Update README.md 2023-04-07 13:41:39 -06:00
Luigi311
3330026de6 Merge pull request #57 from luigi311/partial_watch
Partially implement in progress syncing
2023-03-31 12:14:53 -06:00
Luigi311
25fe426720 Plex: Implement partial play syncing 2023-03-26 23:55:56 -06:00
Luigi311
8d53b5b8c0 Take into account comparing two partially watched/one watched video 2023-03-23 22:50:13 -06:00
Luigi311
0774735f0f Plex: Add title to episode_guids 2023-03-23 22:49:14 -06:00
Luigi311
a5540b94d5 Gather partially watched movie/episodes with todo for processing. 2023-03-22 19:48:19 -06:00
Luigi311
c69d59858d Merge pull request #54 from luigi311/dev
Fix variable overwrites, Fix errors when plex user has no access
2023-03-22 11:29:36 -06:00
Luigi311
962b1149ad Plex: Use token, Check for token on mark 2023-03-18 12:15:59 -06:00
Luigi311
a8edee0354 Jellyfin: Fix user_watched_temp overwrite issues. 2023-03-18 12:12:12 -06:00
Luigi311
3627dde64d Plex: Do not error if user has no access 2023-03-18 11:56:56 -06:00
Luigi311
80ec0e42c2 Dockerfile: Add sync directions to ENV 2023-03-16 14:57:57 -06:00
Luigi311
fd64088bde Merge pull request #51 from luigi311/dev
Add sync direction flags, seperate out functions, better logging for jellyfin queries
2023-03-09 12:52:24 -07:00
Luigi311
7832e41a3b Add sync from to to readme 2023-03-09 01:32:27 -07:00
Luigi311
cadd65d69b Update issue templates (#50)
* Update issue templates
2023-03-09 01:29:11 -07:00
Luigi311
9f004797fc Force format on save in vscode 2023-03-09 00:53:07 -07:00
Luigi311
9041fee7ad Format 2023-03-09 00:48:29 -07:00
Luigi311
9af6c9057c Simplify plex update_user_watched 2023-03-09 00:36:55 -07:00
Luigi311
757ce91138 Merge pull request #49 from luigi311/seperate_functions
Seperate functions
2023-03-08 23:55:53 -07:00
Luigi311
98f96ed5c7 Fix user being added when shouldnt. Add test_users 2023-03-08 23:48:54 -07:00
Luigi311
3e15120e2a Fix library whitelist, add library tests 2023-03-08 23:17:54 -07:00
Luigi311
5824e6c0cc cleanup 2023-03-08 22:21:40 -07:00
Luigi311
7087d75efb Fix exception 2023-03-08 22:15:03 -07:00
Luigi311
b2a06b8fd3 Add tests for black_white and watched 2023-03-08 22:05:32 -07:00
Luigi311
1ee055faf5 format 2023-03-08 22:05:32 -07:00
Luigi311
404089dfca Seperate generate_library_guids_dict 2023-03-08 22:05:32 -07:00
Luigi311
ed24948dee Better logging on library skip 2023-03-08 22:05:32 -07:00
Luigi311
1f16fcb8eb Seperate check_skip_logic, append reasons 2023-03-08 22:05:32 -07:00
Luigi311
03de3affd7 Cleanup, seperate black/white lists setup 2023-03-08 22:05:32 -07:00
Luigi311
2bad887659 Seperate out functions to seperate scripts. 2023-03-08 22:04:48 -07:00
Luigi311
796be47a63 Move lots of setup_users to functions 2023-03-08 22:03:48 -07:00
Luigi311
dc1fe11590 Check for response status 200 on jellyfin query 2023-03-08 21:49:56 -07:00
Luigi311
13b4ff3215 Merge pull request #48 from JChris246/main
[Feature] Add flags to control the direction of syncing between the servers
2023-03-08 20:46:57 -07:00
JChris246
dca54cf4fb feat:add flags to control the direction of syncing 2023-03-08 21:30:28 -04:00
Luigi311
a4365e59f3 Merge pull request #44 from luigi311/dev
Fix issues with certain libraries failing
2023-02-26 13:32:26 -07:00
Luigi311
b960bccb86 Plex: Fix guids error on mark 2023-02-25 18:42:07 -07:00
Luigi311
218037200c Jellyfin: Fix tv show searching for watched 2023-02-25 18:27:01 -07:00
Luigi311
4ac670e837 Plex: Do not error if guids can not be gathered. Parallelize show processing for get watched. 2023-02-25 16:58:57 -07:00
Luigi311
96eff65c3e Do not error if failed to get library watched 2023-02-25 15:03:27 -07:00
Luigi311
45471607c8 Merge pull request #43 from JChris246/chore/spelling
Correct some spelling issues
2023-02-22 09:51:42 -07:00
JChris246
14885744b1 fix: correct some spelling issues 2023-02-22 00:09:30 -04:00
Luigi311
d1fd61f1d1 Merge pull request #38 from luigi311/dev
Fix issue with nested folders
2023-01-31 16:27:54 -07:00
Luigi311
6c1ee4a7dc Log server users 2023-01-30 11:56:27 -07:00
Luigi311
9a8e799e68 Recursive all the things. Use includeItemType 2023-01-30 11:46:12 -07:00
Luigi311
ffec4e2f28 Support multiple library types 2023-01-28 16:33:36 -07:00
Luigi311
00102891a5 Catch None for types 2023-01-27 23:45:03 -07:00
Luigi311
aa76b83428 Use isinstance instead of type 2023-01-27 12:21:38 -07:00
Luigi311
a644189ea5 Use isinstance instead of type 2023-01-27 12:18:15 -07:00
Luigi311
c5d987a8c9 Update .env.sample and README 2023-01-27 11:23:58 -07:00
Luigi311
bdd68ad68d If user is type str get plex object 2023-01-27 11:02:15 -07:00
Luigi311
2d86bca781 Update github actions 2023-01-27 10:48:52 -07:00
Luigi311
1b01ff6ec2 Log if multiple types and continue instead of error 2023-01-27 10:45:46 -07:00
Luigi311
f08ec43507 Skip library before erroring for multiple types. 2023-01-27 10:43:50 -07:00
Luigi311
7f9424260a Format 2023-01-26 14:03:13 -07:00
Luigi311
5f21943353 Exclude folders, use recursive. 2023-01-26 13:55:50 -07:00
Luigi311
a5a795f43c Exclude Folders from list 2023-01-26 13:42:35 -07:00
Luigi311
fcb6d7625f Fix invalid library types, raise mixed types 2023-01-26 13:31:57 -07:00
Luigi311
fd2179998f Fix ssl_bypass for plex 2023-01-26 11:23:47 -07:00
Luigi311
654e7f20e1 Merge pull request #33 from luigi311/dev
Lots of fixes and simplification
2022-12-23 23:13:22 -07:00
Luigi311
1eb92cf7c1 black formatting 2022-12-23 23:11:38 -07:00
Luigi311
111e284cc8 Cleanup 2022-12-23 23:10:51 -07:00
Luigi311
1a4e3f4ec4 Move setup_black_white_list to functions. Fix trailing slash on jellyfin baseurl 2022-12-23 23:02:53 -07:00
Luigi311
4066228e57 Add more debug logging. Do not enable debug by default 2022-12-19 14:07:56 -07:00
Luigi311
59c6d278e3 Add more logging to debug 2022-12-19 13:57:20 -07:00
Luigi311
39b33f3d43 Fix missing logging when using debug level 2022-12-19 13:22:42 -07:00
Luigi311
e8faf52b2b Do not mark shows/movies that do not exist 2022-12-19 01:35:16 -07:00
Luigi311
370e9bac63 change get user watched name to avoid mistakes 2022-12-18 22:39:03 -07:00
Luigi311
d0746cec5a Fix server 2 always running async runner. Speedup plex get watched 2022-12-18 22:27:42 -07:00
Luigi311
251937431b Move cleanup_watched to functions and simplify 2022-12-18 01:50:45 -07:00
Luigi311
50faf061af Remove dockerfile defaults 2022-11-21 18:21:29 -07:00
Luigi311
9ffbc49ad3 Merge pull request #30 from luigi311/dev
Add ssl_bypass to skip hostname validation.
2022-11-21 17:39:00 -07:00
Luigi311
644dc8e3af Merge pull request #29 from lgtm-migrator/codeql
Add CodeQL workflow for GitHub code scanning
2022-11-21 17:38:45 -07:00
Luigi311
47bc4e94dc Fix dockerfile 2022-11-21 17:31:47 -07:00
LGTM Migrator
f17d39fe17 Add CodeQL workflow for GitHub code scanning 2022-11-10 14:41:07 +00:00
Luigi311
966dcacf8d Add ssl_bypass to skip hostname validation. 2022-09-25 14:16:01 -06:00
Luigi311
9afc00443c Merge pull request #27 from luigi311/dev
Cleanup issues
2022-08-18 00:46:00 -06:00
Luigi311
3ec177ea64 rename test_main 2022-08-18 00:17:32 -06:00
Luigi311
b360c9fd0b Remove unnecessary deepcopy 2022-08-18 00:15:42 -06:00
Luigi311
1ed791b1ed Fix jellyfin 2022-08-17 23:49:05 -06:00
Luigi311
f19b1a3063 Cleanup length and functions instead of methods 2022-08-17 23:34:45 -06:00
Luigi311
190a72bd3c Cleanup 2022-08-17 22:53:27 -06:00
Luigi311
c848106ce7 Black cleanup 2022-08-17 22:31:23 -06:00
Luigi311
dd319271bd Cleanup 2022-08-17 22:09:11 -06:00
Luigi311
16879cc728 Merge pull request #26 from luigi311/dev
Use async for jellyfin
2022-08-17 21:49:34 -06:00
Luigi311
942ec3533f Cleanup log file on runs 2022-08-17 21:43:51 -06:00
Luigi311
9f6edfc91a Merge branch 'main' into dev 2022-08-17 21:40:25 -06:00
Luigi311
827ace2e97 cleanup 2022-08-17 21:20:28 -06:00
Luigi311
f6b57a1b4d Update README.md 2022-07-10 01:38:42 -06:00
Luigi311
88a7526721 Use async for jellyfin (#23)
* Use async

* Massive jellyfin watched speedup

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

View File

@@ -1 +1,15 @@
.env .dockerignore
.env
.env.sample
.git
.github
.gitignore
.idea
.vscode
Dockerfile*
README.md
test
venv

View File

@@ -1,42 +1,77 @@
## 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" } ## If set to true then the script will only run once and then exit
## Map libraries between plex and jellyfin in the even that they are different, order does not matter RUN_ONLY_ONCE = "False"
#LIBRARY_MAPPING = { "Shows": "TV Shows" }
## How often to run the script in seconds
SLEEP_DURATION = "3600"
## 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 ## Log file where all output will be written to
## Comma seperated list for multiple servers LOGFILE = "log.log"
PLEX_BASEURL = "http://localhost:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ ## Map usernames between servers in the event that they are different, order does not matter
PLEX_TOKEN = "SuperSecretToken" ## Comma separated for multiple options
## If not using plex token then use username and password of the server admin along with the servername #USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
#PLEX_USERNAME = ""
#PLEX_PASSWORD = "" ## Map libraries between servers in the even that they are different, order does not matter
#PLEX_SERVERNAME = "Plex Server" ## Comma separated for multiple options
#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## 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 list for multiple servers ## Comma separated for multiple options
JELLYFIN_BASEURL = "http://localhost:8096" #BLACKLIST_LIBRARY = ""
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key #WHITELIST_LIBRARY = ""
JELLYFIN_TOKEN = "SuperSecretToken" #BLACKLIST_LIBRARY_TYPE = ""
#WHITELIST_LIBRARY_TYPE = ""
#BLACKLIST_USERS = ""
## 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. WHITELIST_USERS = "testuser1,testuser2"
#BLACKLIST_LIBRARY = ""
#WHITELIST_LIBRARY = ""
#BLACKLIST_LIBRARY_TYPE = ""
#WHITELIST_LIBRARY_TYPE = "" # Plex
#BLACKLIST_USERS = ""
WHITELIST_USERS = "testuser1,testuser2" ## 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
## Comma separated list for multiple servers
PLEX_BASEURL = "http://localhost:32400, https://nas:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma separated 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 separated 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"
## control the direction of syncing. e.g. SYNC_FROM_PLEX_TO_JELLYFIN set to true will cause the updates from plex
## to be updated in jellyfin. SYNC_FROM_PLEX_TO_PLEX set to true will sync updates between multiple plex servers
SYNC_FROM_PLEX_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
# Jellyfin
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma separated 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 separated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If applicable, add logs to help explain your problem ideally with DEBUG set to true, be sure to remove sensitive information
**Type:**
- [ ] Docker
- [ ] Native
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request]"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,86 +1,105 @@
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: strategy:
- name: Checkout matrix:
uses: actions/checkout@v2 include:
- dockerfile: Dockerfile.alpine
- name: Docker meta variant: alpine
id: docker_meta - dockerfile: Dockerfile.slim
env: variant: slim
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} steps:
if: "${{ env.DOCKER_USERNAME != '' }}" - name: Checkout
uses: docker/metadata-action@v4 uses: actions/checkout@v3
with:
images: ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched # list of Docker images to use as base name for tags - name: Docker meta
tags: | id: docker_meta
type=raw,value=latest,enable={{is_default_branch}} uses: docker/metadata-action@v4
type=ref,event=branch with:
type=ref,event=pr images: |
type=semver,pattern={{version}} ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }}
type=semver,pattern={{major}}.{{minor}} # Do not push to ghcr.io on PRs due to permission issues
type=sha ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }}
tags: |
- name: Set up QEMU type=raw,value=latest,enable=${{ matrix.variant == 'alpine' && github.ref_name == github.event.repository.default_branch }}
uses: docker/setup-qemu-action@v1 type=raw,value=dev,enable=${{ matrix.variant == 'alpine' && github.ref_name == 'dev' }}
type=raw,value=latest,suffix=-${{ matrix.variant }},enable={{ is_default_branch }}
- name: Set up Docker Buildx type=ref,event=branch,suffix=-${{ matrix.variant }}
uses: docker/setup-buildx-action@v1 type=ref,event=pr,suffix=-${{ matrix.variant }}
type=semver,pattern={{ version }},suffix=-${{ matrix.variant }}
- name: Login to DockerHub type=semver,pattern={{ major }}.{{ minor }},suffix=-${{ matrix.variant }}
if: "${{ steps.docker_meta.outcome == 'success' }}" type=sha,suffix=-${{ matrix.variant }}
uses: docker/login-action@v1
with: - name: Set up QEMU
username: ${{ secrets.DOCKER_USERNAME }} uses: docker/setup-qemu-action@v2
password: ${{ secrets.DOCKER_TOKEN }}
- name: Set up Docker Buildx
- name: Build uses: docker/setup-buildx-action@v2
id: build
if: "${{ steps.docker_meta.outcome == 'skipped' }}" - name: Login to DockerHub
uses: docker/build-push-action@v2 env:
with: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
context: . if: "${{ env.DOCKER_USERNAME != '' }}"
file: ./Dockerfile uses: docker/login-action@v2
platforms: linux/amd64,linux/arm64 with:
push: false username: ${{ secrets.DOCKER_USERNAME }}
tags: jellyplex-watched:action password: ${{ secrets.DOCKER_TOKEN }}
- name: Build Push - name: Login to GitHub Container Registry
id: build_push if: "${{ steps.docker_meta.outcome == 'success' }}"
if: "${{ steps.docker_meta.outcome == 'success' }}" uses: docker/login-action@v2
uses: docker/build-push-action@v2 with:
with: registry: ghcr.io
context: . username: ${{ github.actor }}
file: ./Dockerfile password: ${{ secrets.GITHUB_TOKEN }}
platforms: linux/amd64,linux/arm64
push: true - name: Build
tags: ${{ steps.docker_meta.outputs.tags }} id: build
labels: ${{ steps.docker_meta.outputs.labels }} if: "${{ steps.docker_meta.outputs.tags == '' }}"
uses: docker/build-push-action@v3
# Echo digest so users can validate their image with:
- name: Image digest context: .
if: "${{ steps.docker_meta.outcome == 'success' }}" file: ${{ matrix.dockerfile }}
run: echo "${{ steps.build_push.outputs.digest }}" platforms: linux/amd64,linux/arm64
push: false
tags: jellyplex-watched:action
- name: Build Push
id: build_push
if: "${{ steps.docker_meta.outputs.tags != '' }}"
uses: docker/build-push-action@v3
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
# Echo digest so users can validate their image
- name: Image digest
if: "${{ steps.docker_meta.outcome == 'success' }}"
run: echo "${{ steps.build_push.outputs.digest }}"

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

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

264
.gitignore vendored
View File

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

32
.vscode/launch.json vendored
View File

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

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

@@ -0,0 +1,7 @@
{
"[python]" : {
"editor.formatOnSave": true,
},
"python.formatting.provider": "black",
}

41
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,41 @@
FROM python:3-alpine
ENV DRYRUN 'True'
ENV DEBUG 'True'
ENV DEBUG_LEVEL 'INFO'
ENV SLEEP_DURATION '3600'
ENV LOGFILE 'log.log'
ENV USER_MAPPING ''
ENV LIBRARY_MAPPING ''
ENV PLEX_BASEURL ''
ENV PLEX_TOKEN ''
ENV PLEX_USERNAME ''
ENV PLEX_PASSWORD ''
ENV PLEX_SERVERNAME ''
ENV JELLYFIN_BASEURL ''
ENV JELLYFIN_TOKEN ''
ENV SYNC_FROM_PLEX_TO_JELLYFIN 'True'
ENV SYNC_FROM_JELLYFIN_TO_PLEX 'True'
ENV SYNC_FROM_PLEX_TO_PLEX 'True'
ENV SYNC_FROM_JELLYFIN_TO_JELLYFIN 'True'
ENV BLACKLIST_LIBRARY ''
ENV WHITELIST_LIBRARY ''
ENV BLACKLIST_LIBRARY_TYPE ''
ENV WHITELIST_LIBRARY_TYPE ''
ENV BLACKLIST_USERS ''
ENV WHITELIST_USERS ''
WORKDIR /app
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-u", "main.py"]

View File

@@ -6,18 +6,23 @@ 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 SYNC_FROM_PLEX_TO_JELLYFIN 'True'
ENV SYNC_FROM_JELLYFIN_TO_PLEX 'True'
ENV SYNC_FROM_PLEX_TO_PLEX 'True'
ENV SYNC_FROM_JELLYFIN_TO_JELLYFIN 'True'
ENV BLACKLIST_LIBRARY '' ENV BLACKLIST_LIBRARY ''
ENV WHITELIST_LIBRARY '' ENV WHITELIST_LIBRARY ''
ENV BLACKLIST_LIBRARY_TYPE '' ENV BLACKLIST_LIBRARY_TYPE ''

1348
LICENSE

File diff suppressed because it is too large Load Diff

137
README.md
View File

@@ -1,32 +1,143 @@
# JellyPlex-Watched # JellyPlex-Watched
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=luigi311/JellyPlex-Watched&amp;utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com\&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 entering multiple options in the .env plex/jellyfin section separated by commas.
## Features
### Plex
* \[x] Match via Filenames
* \[x] Match via provider ids
* \[x] Map usersnames
* \[x] Use single login
* \[x] One Way/Multi Way sync
* \[x] Sync Watched
* \[x] Sync Inprogress
### Jellyfin
* \[x] Match via Filenames
* \[x] Match via provider ids
* \[x] Map usersnames
* \[x] Use single login
* \[x] One Way/Multi Way sync
* \[x] Sync Watched
* \[ ] Sync Inprogress
### Emby
* \[ ] Match via Filenames
* \[ ] Match via provider ids
* \[ ] Map usersnames
* \[ ] Use single login
* \[ ] One Way/Multi Way sync
* \[ ] Sync Watched
* \[ ] Sync Inprogress
## Configuration ## Configuration
```bash
# Global Settings
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
DRYRUN = "True"
## Additional logging information
DEBUG = "False"
## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info"
## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "False"
## How often to run the script in seconds
SLEEP_DURATION = "3600"
## Log file where all output will be written to
LOGFILE = "log.log"
## Map usernames between servers in the event that they are different, order does not matter
## Comma separated for multiple options
USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
## Map libraries between servers in the even that they are different, order does not matter
## Comma separated for multiple options
LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
## 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 separated for multiple options
BLACKLIST_LIBRARY = ""
WHITELIST_LIBRARY = ""
BLACKLIST_LIBRARY_TYPE = ""
WHITELIST_LIBRARY_TYPE = ""
BLACKLIST_USERS = ""
WHITELIST_USERS = "testuser1,testuser2"
# Plex
## 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
## Comma separated list for multiple servers
PLEX_BASEURL = "http://localhost:32400, https://nas:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma separated 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 separated 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"
## control the direction of syncing. e.g. SYNC_FROM_PLEX_TO_JELLYFIN set to true will cause the updates from plex
## to be updated in jellyfin. SYNC_FROM_PLEX_TO_PLEX set to true will sync updates between multiple plex servers
SYNC_FROM_PLEX_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
# Jellyfin
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma separated 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 separated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
```
## Installation ## Installation
### Baremetal ### Baremetal
- Setup virtualenv of your choice * Setup virtualenv of your choice
- Install dependencies * Install dependencies
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
- Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens * Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens
- Run * Run
```bash ```bash
python main.py python main.py
@@ -34,13 +145,13 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
### Docker ### Docker
- Build docker image * Build docker image
```bash ```bash
docker build -t jellyplex-watched . docker build -t jellyplex-watched .
``` ```
- or use pre-built image * or use pre-built image
```bash ```bash
docker pull luigi311/jellyplex-watched:latest docker pull luigi311/jellyplex-watched:latest
@@ -48,7 +159,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
#### With variables #### With variables
- Run * Run
```bash ```bash
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest
@@ -56,9 +167,9 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
#### With .env #### With .env
- Create a .env file similar to .env.sample and set the MNEMONIC variable to your seed phrase * Create a .env file similar to .env.sample and set the variables to match your setup
- Run * Run
```bash ```bash
docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest docker run --rm -it -v "$(pwd)/.env:/app/.env" luigi311/jellyplex-watched:latest
@@ -66,7 +177,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
## Contributing ## Contributing
I am open to recieving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches. I am open to receiving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches.
## License ## License

31
docker-compose.yml Normal file
View File

@@ -0,0 +1,31 @@
version: '3'
services:
jellyplex-watched:
image: luigi311/jellyplex-watched:latest
container_name: jellyplex-watched
restart: always
environment:
- DRYRUN=True
- DEBUG=True
- DEBUG_LEVEL=info
- RUN_ONLY_ONCE=False
- SLEEP_DURATION=3600
- LOGFILE=/tmp/log.log
- USER_MAPPING=
- LIBRARY_MAPPING={"TV Shows":"Shows"}
- BLACKLIST_LIBRARY=
- WHITELIST_LIBRARY=
- BLACKLIST_LIBRARY_TYPE=
- WHITELIST_LIBRARY_TYPE=
- BLACKLIST_USERS=
- WHITELIST_USERS=
- PLEX_BASEURL=
- PLEX_TOKEN=
- JELLYFIN_BASEURL=
- JELLYFIN_TOKEN=
- SSL_BYPASS=True
- SYNC_FROM_PLEX_TO_JELLYFIN=True
- SYNC_FROM_JELLYFIN_TO_PLEX=True
- SYNC_FROM_PLEX_TO_PLEX=True
- SYNC_FROM_JELLYFIN_TO_JELLYFIN=True

21
main.py
View File

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

View File

@@ -1,3 +1,4 @@
plexapi PlexAPI==4.13.4
requests requests==2.28.2
python-dotenv python-dotenv==1.0.0
aiohttp==3.8.4

130
src/black_white.py Normal file
View File

@@ -0,0 +1,130 @@
from src.functions import logger, search_mapping
def setup_black_white_lists(
blacklist_library: str,
whitelist_library: str,
blacklist_library_type: str,
whitelist_library_type: str,
blacklist_users: str,
whitelist_users: str,
library_mapping=None,
user_mapping=None,
):
blacklist_library, blacklist_library_type, blacklist_users = setup_x_lists(
blacklist_library,
blacklist_library_type,
blacklist_users,
"White",
library_mapping,
user_mapping,
)
whitelist_library, whitelist_library_type, whitelist_users = setup_x_lists(
whitelist_library,
whitelist_library_type,
whitelist_users,
"Black",
library_mapping,
user_mapping,
)
return (
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
blacklist_users,
whitelist_users,
)
def setup_x_lists(
xlist_library,
xlist_library_type,
xlist_users,
xlist_type,
library_mapping=None,
user_mapping=None,
):
if xlist_library:
if len(xlist_library) > 0:
xlist_library = xlist_library.split(",")
xlist_library = [x.strip() for x in xlist_library]
if library_mapping:
temp_library = []
for library in xlist_library:
library_other = search_mapping(library_mapping, library)
if library_other:
temp_library.append(library_other)
xlist_library = xlist_library + temp_library
else:
xlist_library = []
logger(f"{xlist_type}list Library: {xlist_library}", 1)
if xlist_library_type:
if len(xlist_library_type) > 0:
xlist_library_type = xlist_library_type.split(",")
xlist_library_type = [x.lower().strip() for x in xlist_library_type]
else:
xlist_library_type = []
logger(f"{xlist_type}list Library Type: {xlist_library_type}", 1)
if xlist_users:
if len(xlist_users) > 0:
xlist_users = xlist_users.split(",")
xlist_users = [x.lower().strip() for x in xlist_users]
if user_mapping:
temp_users = []
for user in xlist_users:
user_other = search_mapping(user_mapping, user)
if user_other:
temp_users.append(user_other)
xlist_users = xlist_users + temp_users
else:
xlist_users = []
else:
xlist_users = []
logger(f"{xlist_type}list Users: {xlist_users}", 1)
return xlist_library, xlist_library_type, xlist_users

View File

@@ -1,150 +1,85 @@
import os import os
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv(override=True) load_dotenv(override=True)
logfile = os.getenv("LOGFILE","log.log") logfile = os.getenv("LOGFILE", "log.log")
def logger(message: str, log_type=0):
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()] # Search for nested element in list
elif key_value in dictionary.values(): def contains_nested(element, lst):
return list(dictionary.keys())[list(dictionary.values()).index(key_value)] for i, item in enumerate(lst):
elif key_value.lower() in dictionary.values(): if element in item:
return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())] return i
else: return None
return None
# Get mapped value
def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): def search_mapping(dictionary: dict, key_value: str):
skip_reason = None if key_value in dictionary.keys():
return dictionary[key_value]
if library_type.lower() in blacklist_library_type: elif key_value.lower() in dictionary.keys():
skip_reason = "is blacklist_library_type" return dictionary[key_value.lower()]
elif key_value in dictionary.values():
if library_title.lower() in [x.lower() for x in blacklist_library]: return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
skip_reason = "is blacklist_library" elif key_value.lower() in dictionary.values():
return list(dictionary.keys())[
library_other = None list(dictionary.values()).index(key_value.lower())
if library_mapping: ]
library_other = search_mapping(library_mapping, library_title) else:
if library_other: return None
if library_other.lower() in [x.lower() for x in blacklist_library]:
skip_reason = "is blacklist_library"
def future_thread_executor(args: list, workers: int = -1):
if len(whitelist_library_type) > 0: futures_list = []
if library_type.lower() not in whitelist_library_type: results = []
skip_reason = "is not whitelist_library_type"
if workers == -1:
# if whitelist is not empty and library is not in whitelist workers = min(32, os.cpu_count() * 2)
if len(whitelist_library) > 0:
if library_title.lower() not in [x.lower() for x in whitelist_library]: with ThreadPoolExecutor(max_workers=workers) as executor:
skip_reason = "is not whitelist_library" for arg in args:
# * arg unpacks the list into actual arguments
if library_other: futures_list.append(executor.submit(*arg))
if library_other.lower() not in [x.lower() for x in whitelist_library]:
skip_reason = "is not whitelist_library" for future in futures_list:
try:
return skip_reason result = future.result()
results.append(result)
except Exception as e:
def generate_library_guids_dict(user_list: dict, generate_output: int): raise Exception(e)
# if generate_output is 0 then only generate shows, if 1 then only generate episodes, if 2 then generate movies, if 3 then generate shows and episodes
show_output_dict = {} return results
episode_output_dict = {}
movies_output_dict = {}
if generate_output in (0, 3):
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())
if generate_output in (1, 3):
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())
if generate_output == 2:
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())
return show_output_dict, episode_output_dict, movies_output_dict
def future_thread_executor(args: list, workers: int = -1):
futures_list = []
results = []
if workers == -1:
workers = min(32, os.cpu_count()*1.25)
with ThreadPoolExecutor(max_workers=workers) as executor:
for arg in args:
# * arg unpacks the list into actual arguments
futures_list.append(executor.submit(*arg))
for future in futures_list:
try:
result = future.result()
results.append(result)
except Exception as e:
raise Exception(e)
return results

File diff suppressed because it is too large Load Diff

262
src/library.py Normal file
View File

@@ -0,0 +1,262 @@
from src.functions import (
logger,
search_mapping,
)
def check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping=None,
):
skip_reason = None
library_other = None
if library_mapping:
library_other = search_mapping(library_mapping, library_title)
skip_reason_black = check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other,
)
skip_reason_white = check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other,
)
# Combine skip reasons
if skip_reason_black:
skip_reason = skip_reason_black
if skip_reason_white:
if skip_reason:
skip_reason = skip_reason + " and " + skip_reason_white
else:
skip_reason = skip_reason_white
return skip_reason
def check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other=None,
):
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 = f"{library_type_item} is in blacklist_library_type"
else:
if library_type.lower() in blacklist_library_type:
skip_reason = f"{library_type} is in blacklist_library_type"
if library_title.lower() in [x.lower() for x in blacklist_library]:
if skip_reason:
skip_reason = (
skip_reason + " and " + f"{library_title} is in blacklist_library"
)
else:
skip_reason = f"{library_title} is in blacklist_library"
if library_other:
if library_other.lower() in [x.lower() for x in blacklist_library]:
if skip_reason:
skip_reason = (
skip_reason + " and " + f"{library_other} is in blacklist_library"
)
else:
skip_reason = f"{library_other} is in blacklist_library"
return skip_reason
def check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other=None,
):
skip_reason = None
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 = (
f"{library_type_item} is not in whitelist_library_type"
)
else:
if library_type.lower() not in whitelist_library_type:
skip_reason = f"{library_type} is not in whitelist_library_type"
# if whitelist is not empty and library is not in whitelist
if len(whitelist_library) > 0:
if library_other:
if library_title.lower() not in [
x.lower() for x in whitelist_library
] and library_other.lower() not in [x.lower() for x in whitelist_library]:
if skip_reason:
skip_reason = (
skip_reason
+ " and "
+ f"{library_title} is not in whitelist_library"
)
else:
skip_reason = f"{library_title} is not in whitelist_library"
else:
if library_title.lower() not in [x.lower() for x in whitelist_library]:
if skip_reason:
skip_reason = (
skip_reason
+ " and "
+ f"{library_title} is not in whitelist_library"
)
else:
skip_reason = f"{library_title} is not in whitelist_library"
return skip_reason
def show_title_dict(user_list: dict):
try:
show_output_dict = {}
show_output_dict["locations"] = []
show_counter = 0 # Initialize a counter for the current show position
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()] = [None] * show_counter
if provider_key.lower() == "locations":
show_output_dict[provider_key.lower()].append(provider_value)
else:
show_output_dict[provider_key.lower()].append(
provider_value.lower()
)
show_counter += 1
for key in show_output_dict:
if len(show_output_dict[key]) < show_counter:
show_output_dict[key].append(None)
return show_output_dict
except Exception:
logger("Generating show_output_dict failed, skipping", 1)
return {}
def episode_title_dict(user_list: dict):
try:
episode_output_dict = {}
episode_output_dict["completed"] = []
episode_output_dict["time"] = []
episode_output_dict["locations"] = []
episode_counter = 0 # Initialize a counter for the current episode position
# Iterate through the shows, seasons, and episodes in user_list
for show in user_list:
for season in user_list[show]:
for episode in user_list[show][season]:
# Iterate through the keys and values in each episode
for episode_key, episode_value in episode.items():
# If the key is not "status", add the key to episode_output_dict if it doesn't exist
if episode_key != "status":
if episode_key.lower() not in episode_output_dict:
# Initialize the list with None values up to the current episode position
episode_output_dict[episode_key.lower()] = [
None
] * episode_counter
# If the key is "locations", append each location to the list
if episode_key == "locations":
episode_output_dict[episode_key.lower()].append(
episode_value
)
# If the key is "status", append the "completed" and "time" values
elif episode_key == "status":
episode_output_dict["completed"].append(
episode_value["completed"]
)
episode_output_dict["time"].append(episode_value["time"])
# For other keys, append the value to the list
else:
episode_output_dict[episode_key.lower()].append(
episode_value.lower()
)
# Increment the episode_counter
episode_counter += 1
# Extend the lists in episode_output_dict with None values to match the current episode_counter
for key in episode_output_dict:
if len(episode_output_dict[key]) < episode_counter:
episode_output_dict[key].append(None)
return episode_output_dict
except Exception:
logger("Generating episode_output_dict failed, skipping", 1)
return {}
def movies_title_dict(user_list: dict):
try:
movies_output_dict = {}
movies_output_dict["completed"] = []
movies_output_dict["time"] = []
movies_output_dict["locations"] = []
movie_counter = 0 # Initialize a counter for the current movie position
for movie in user_list:
for movie_key, movie_value in movie.items():
if movie_key != "status":
if movie_key.lower() not in movies_output_dict:
movies_output_dict[movie_key.lower()] = []
if movie_key == "locations":
movies_output_dict[movie_key.lower()].append(movie_value)
elif movie_key == "status":
movies_output_dict["completed"].append(movie_value["completed"])
movies_output_dict["time"].append(movie_value["time"])
else:
movies_output_dict[movie_key.lower()].append(movie_value.lower())
movie_counter += 1
for key in movies_output_dict:
if len(movies_output_dict[key]) < movie_counter:
movies_output_dict[key].append(None)
return movies_output_dict
except Exception:
logger("Generating movies_output_dict failed, skipping", 1)
return {}
def generate_library_guids_dict(user_list: 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 = show_title_dict(user_list)
episode_output_dict = episode_title_dict(user_list)
movies_output_dict = movies_title_dict(user_list)
return show_output_dict, episode_output_dict, movies_output_dict

View File

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

View File

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

91
src/users.py Normal file
View File

@@ -0,0 +1,91 @@
from src.functions import (
logger,
search_mapping,
)
def generate_user_list(server):
# generate list of users from server 1 and server 2
server_type = server[0]
server_connection = server[1]
server_users = []
if server_type == "plex":
for user in server_connection.users:
server_users.append(
user.username.lower() if user.username else user.title.lower()
)
elif server_type == "jellyfin":
server_users = [key.lower() for key in server_connection.users.keys()]
return server_users
def combine_user_lists(server_1_users, server_2_users, user_mapping):
# combined list of overlapping users from plex and jellyfin
users = {}
for server_1_user in server_1_users:
if user_mapping:
mapped_user = search_mapping(user_mapping, server_1_user)
if mapped_user in server_2_users:
users[server_1_user] = mapped_user
continue
if server_1_user in server_2_users:
users[server_1_user] = server_1_user
for server_2_user in server_2_users:
if user_mapping:
mapped_user = search_mapping(user_mapping, server_2_user)
if mapped_user in server_1_users:
users[mapped_user] = server_2_user
continue
if server_2_user in server_1_users:
users[server_2_user] = server_2_user
return users
def filter_user_lists(users, blacklist_users, whitelist_users):
users_filtered = {}
for user in users:
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
if len(whitelist_users) > 0:
if user not in whitelist_users and users[user] not in whitelist_users:
logger(f"{user} or {users[user]} is not in whitelist", 1)
continue
if user not in blacklist_users and users[user] not in blacklist_users:
users_filtered[user] = users[user]
return users_filtered
def generate_server_users(server, users):
server_users = None
if server[0] == "plex":
server_users = []
for plex_user in server[1].users:
username_title = (
plex_user.username if plex_user.username else plex_user.title
)
if (
username_title.lower() in users.keys()
or username_title.lower() in users.values()
):
server_users.append(plex_user)
elif server[0] == "jellyfin":
server_users = {}
for jellyfin_user, jellyfin_id in server[1].users.items():
if (
jellyfin_user.lower() in users.keys()
or jellyfin_user.lower() in users.values()
):
server_users[jellyfin_user] = jellyfin_id
return server_users

240
src/watched.py Normal file
View File

@@ -0,0 +1,240 @@
import copy
from src.functions import logger, search_mapping, contains_nested
from src.library import generate_library_guids_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 check_remove_entry(video, library, video_index, library_watched_list_2):
if video_index is not None:
if (
library_watched_list_2["completed"][video_index]
== video["status"]["completed"]
) and (library_watched_list_2["time"][video_index] == video["status"]["time"]):
logger(
f"Removing {video['title']} from {library} due to exact match",
3,
)
return True
elif (
library_watched_list_2["completed"][video_index] == True
and video["status"]["completed"] == False
):
logger(
f"Removing {video['title']} from {library} due to being complete in one library and not the other",
3,
)
return True
elif (
library_watched_list_2["completed"][video_index] == False
and video["status"]["completed"] == False
) and (video["status"]["time"] < library_watched_list_2["time"][video_index]):
logger(
f"Removing {video['title']} from {library} due to more time watched in one library than the other",
3,
)
return True
elif (
library_watched_list_2["completed"][video_index] == True
and video["status"]["completed"] == True
):
logger(
f"Removing {video['title']} from {library} due to being complete in both libraries",
3,
)
return True
return False
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]:
movie_index = get_movie_index_in_dict(
movie, movies_watched_list_2_keys_dict
)
if movie_index is not None:
if check_remove_entry(
movie,
library_1,
movie_index,
movies_watched_list_2_keys_dict,
):
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
]:
episode_index = get_episode_index_in_dict(
episode, episode_watched_list_2_keys_dict
)
if episode_index is not None:
if check_remove_entry(
episode,
library_1,
episode_index,
episode_watched_list_2_keys_dict,
):
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, object_1, object_2):
if object_1 in watched_list:
return object_1
elif object_2 in watched_list:
return object_2
else:
logger(f"{object_1} and {object_2} not found in watched list 2", 1)
return None
def get_movie_index_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 index of the key
return contains_nested(
location, movies_watched_list_2_keys_dict["locations"]
)
# 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 movies_watched_list_2_keys_dict[movie_key].index(movie_value)
# If the loop completes without finding a match, return False
return None
def get_episode_index_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 episode_key in episode_watched_list_2_keys_dict.keys():
if episode_key == "locations":
# 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 index of the key
return contains_nested(
location, episode_watched_list_2_keys_dict["locations"]
)
else:
# 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 episode_watched_list_2_keys_dict[episode_key].index(
episode_value
)
# If the loop completes without finding a match, return False
return None

View File

@@ -1 +1 @@
pytest pytest==7.3.0

78
test/test_black_white.py Normal file
View File

@@ -0,0 +1,78 @@
import sys
import os
# getting the name of the directory
# where the this file is present.
current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name
# where the current directory is present.
parent = os.path.dirname(current)
# adding the parent directory to
# the sys.path.
sys.path.append(parent)
from src.black_white 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"]

317
test/test_library.py Normal file
View File

@@ -0,0 +1,317 @@
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.functions import (
search_mapping,
)
from src.library import (
check_skip_logic,
check_blacklist_logic,
check_whitelist_logic,
show_title_dict,
episode_title_dict,
movies_title_dict,
generate_library_guids_dict,
)
blacklist_library = ["TV Shows"]
whitelist_library = ["Movies"]
blacklist_library_type = ["episodes"]
whitelist_library_type = ["movies"]
library_mapping = {"Shows": "TV Shows", "Movie": "Movies"}
show_list = {
frozenset(
{
("locations", ("The Last of Us",)),
("tmdb", "100088"),
("imdb", "tt3581920"),
("tvdb", "392256"),
("title", "The Last of Us"),
}
): {
"Season 1": [
{
"imdb": "tt11957006",
"tmdb": "2181581",
"tvdb": "8444132",
"locations": (
(
"The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv",
)
),
"status": {"completed": True, "time": 0},
}
]
}
}
movie_list = [
{
"title": "Coco",
"imdb": "tt2380307",
"tmdb": "354912",
"locations": [("Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv")],
"status": {"completed": True, "time": 0},
}
]
show_titles = {
"imdb": ["tt3581920"],
"locations": [("The Last of Us",)],
"tmdb": ["100088"],
"tvdb": ["392256"],
}
episode_titles = {
"imdb": ["tt11957006"],
"locations": [
("The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv",)
],
"tmdb": ["2181581"],
"tvdb": ["8444132"],
"completed": [True],
"time": [0],
}
movie_titles = {
"imdb": ["tt2380307"],
"locations": [
[
(
"Coco (2017) Remux-2160p.mkv",
"Coco (2017) Remux-1080p.mkv",
)
]
],
"title": ["coco"],
"tmdb": ["354912"],
"completed": [True],
"time": [0],
}
def test_check_skip_logic():
# Failes
library_title = "Test"
library_type = "movies"
skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
assert skip_reason == "Test is not in whitelist_library"
library_title = "Shows"
library_type = "episodes"
skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
assert (
skip_reason
== "episodes is in blacklist_library_type and TV Shows is in blacklist_library and "
+ "episodes is not in whitelist_library_type and Shows is not in whitelist_library"
)
# Passes
library_title = "Movie"
library_type = "movies"
skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
assert skip_reason is None
def test_check_blacklist_logic():
# Fails
library_title = "Shows"
library_type = "episodes"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other,
)
assert (
skip_reason
== "episodes is in blacklist_library_type and TV Shows is in blacklist_library"
)
library_title = "TV Shows"
library_type = "episodes"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other,
)
assert (
skip_reason
== "episodes is in blacklist_library_type and TV Shows is in blacklist_library"
)
# Passes
library_title = "Movie"
library_type = "movies"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other,
)
assert skip_reason is None
library_title = "Movies"
library_type = "movies"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_blacklist_logic(
library_title,
library_type,
blacklist_library,
blacklist_library_type,
library_other,
)
assert skip_reason is None
def test_check_whitelist_logic():
# Fails
library_title = "Shows"
library_type = "episodes"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other,
)
assert (
skip_reason
== "episodes is not in whitelist_library_type and Shows is not in whitelist_library"
)
library_title = "TV Shows"
library_type = "episodes"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other,
)
assert (
skip_reason
== "episodes is not in whitelist_library_type and TV Shows is not in whitelist_library"
)
# Passes
library_title = "Movie"
library_type = "movies"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other,
)
assert skip_reason is None
library_title = "Movies"
library_type = "movies"
library_other = search_mapping(library_mapping, library_title)
skip_reason = check_whitelist_logic(
library_title,
library_type,
whitelist_library,
whitelist_library_type,
library_other,
)
assert skip_reason is None
def test_show_title_dict():
show_titles_dict = show_title_dict(show_list)
assert show_titles_dict == show_titles
def test_episode_title_dict():
episode_titles_dict = episode_title_dict(show_list)
assert episode_titles_dict == episode_titles
def test_movies_title_dict():
movies_titles_dict = movies_title_dict(movie_list)
assert movies_titles_dict == movie_titles
def test_generate_library_guids_dict():
# Test with shows
(
show_titles_dict,
episode_titles_dict,
movies_titles_dict,
) = generate_library_guids_dict(show_list)
assert show_titles_dict == show_titles
assert episode_titles_dict == episode_titles
assert movies_titles_dict == {}
# Test with movies
(
show_titles_dict,
episode_titles_dict,
movies_titles_dict,
) = generate_library_guids_dict(movie_list)
assert show_titles_dict == {}
assert episode_titles_dict == {}
assert movies_titles_dict == movie_titles

78
test/test_main.py Normal file
View File

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

View File

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

View File

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

39
test/test_users.py Normal file
View File

@@ -0,0 +1,39 @@
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.users import (
combine_user_lists,
filter_user_lists,
)
def test_combine_user_lists():
server_1_users = ["test", "test3", "luigi311"]
server_2_users = ["luigi311", "test2", "test3"]
user_mapping = {"test2": "test"}
combined = combine_user_lists(server_1_users, server_2_users, user_mapping)
assert combined == {"luigi311": "luigi311", "test": "test2", "test3": "test3"}
def test_filter_user_lists():
users = {"luigi311": "luigi311", "test": "test2", "test3": "test3"}
blacklist_users = ["test3"]
whitelist_users = ["test", "luigi311"]
filtered = filter_user_lists(users, blacklist_users, whitelist_users)
assert filtered == {"test": "test2", "luigi311": "luigi311"}

487
test/test_watched.py Normal file
View File

@@ -0,0 +1,487 @@
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.watched import cleanup_watched, combine_watched_dicts
tv_shows_watched_list_1 = {
frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [
{
"imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"title": "Extreme Aggressor",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
"status": {"completed": True, "time": 0},
},
{
"imdb": "tt0550487",
"tmdb": "282861",
"tvdb": "300385",
"title": "Compulsion",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
"status": {"completed": True, "time": 0},
},
]
},
frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [
{
"title": "S01E01",
"locations": ("Test S01E01.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "S01E02",
"locations": ("Test S01E02.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "S01E04",
"locations": ("Test S01E04.mkv",),
"status": {"completed": False, "time": 5},
},
]
},
}
tv_shows_watched_list_2 = {
frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [
{
"imdb": "tt0550487",
"tmdb": "282861",
"tvdb": "300385",
"title": "Compulsion",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"imdb": "tt0550498",
"tmdb": "282865",
"tvdb": "300474",
"title": "Won't Get Fooled Again",
"locations": (
"Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",
),
"status": {"completed": True, "time": 0},
},
]
},
frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [
{
"title": "S01E02",
"locations": ("Test S01E02.mkv",),
"status": {"completed": False, "time": 10},
},
{
"title": "S01E03",
"locations": ("Test S01E03.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "S01E04",
"locations": ("Test S01E04.mkv",),
"status": {"completed": False, "time": 10},
},
]
},
}
expected_tv_show_watched_list_1 = {
frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [
{
"imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"title": "Extreme Aggressor",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
"status": {"completed": True, "time": 0},
}
]
},
frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [
{
"title": "S01E01",
"locations": ("Test S01E01.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "S01E02",
"locations": ("Test S01E02.mkv",),
"status": {"completed": True, "time": 0},
},
]
},
}
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",
"title": "Won't Get Fooled Again",
"locations": (
"Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",
),
"status": {"completed": True, "time": 0},
}
]
},
frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [
{
"title": "S01E03",
"locations": ("Test S01E03.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "S01E04",
"locations": ("Test S01E04.mkv",),
"status": {"completed": False, "time": 10},
},
]
},
}
movies_watched_list_1 = [
{
"imdb": "tt2380307",
"tmdb": "354912",
"title": "Coco",
"locations": ("Coco (2017) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"tmdbcollection": "448150",
"imdb": "tt1431045",
"tmdb": "293660",
"title": "Deadpool",
"locations": ("Deadpool (2016) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
]
movies_watched_list_2 = [
{
"imdb": "tt2380307",
"tmdb": "354912",
"title": "Coco",
"locations": ("Coco (2017) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"imdb": "tt0384793",
"tmdb": "9788",
"tvdb": "9103",
"title": "Accepted",
"locations": ("Accepted (2006) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
]
expected_movie_watched_list_1 = [
{
"tmdbcollection": "448150",
"imdb": "tt1431045",
"tmdb": "293660",
"title": "Deadpool",
"locations": ("Deadpool (2016) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
}
]
expected_movie_watched_list_2 = [
{
"imdb": "tt0384793",
"tmdb": "9788",
"tvdb": "9103",
"title": "Accepted",
"locations": ("Accepted (2006) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
}
]
# Test to see if objects get deleted all the way up to the root.
tv_shows_2_watched_list_1 = {
frozenset(
{
("tvdb", "75710"),
("title", "Criminal Minds"),
("imdb", "tt0452046"),
("locations", ("Criminal Minds",)),
("tmdb", "4057"),
}
): {
"Season 1": [
{
"imdb": "tt0550489",
"tmdb": "282843",
"tvdb": "176357",
"title": "Extreme Aggressor",
"locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
),
"status": {"completed": True, "time": 0},
},
]
}
}
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
def test_combine_watched_dicts():
input_watched = [
{
"test3": {
"Anime Movies": [
{
"title": "Ponyo",
"tmdb": "12429",
"imdb": "tt0876563",
"locations": ("Ponyo (2008) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Spirited Away",
"tmdb": "129",
"imdb": "tt0245429",
"locations": ("Spirited Away (2001) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Castle in the Sky",
"tmdb": "10515",
"imdb": "tt0092067",
"locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
]
}
},
{"test3": {"Anime Shows": {}}},
{"test3": {"Cartoon Shows": {}}},
{
"test3": {
"Shows": {
frozenset(
{
("tmdb", "64464"),
("tvdb", "301824"),
("tvrage", "45210"),
("title", "11.22.63"),
("locations", ("11.22.63",)),
("imdb", "tt2879552"),
}
): {
"Season 1": [
{
"imdb": "tt4460418",
"title": "The Rabbit Hole",
"locations": (
"11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv",
),
"status": {"completed": True, "time": 0},
}
]
}
}
}
},
{"test3": {"Subbed Anime": {}}},
]
expected = {
"test3": {
"Anime Movies": [
{
"title": "Ponyo",
"tmdb": "12429",
"imdb": "tt0876563",
"locations": ("Ponyo (2008) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Spirited Away",
"tmdb": "129",
"imdb": "tt0245429",
"locations": ("Spirited Away (2001) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Castle in the Sky",
"tmdb": "10515",
"imdb": "tt0092067",
"locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
],
"Anime Shows": {},
"Cartoon Shows": {},
"Shows": {
frozenset(
{
("tmdb", "64464"),
("tvdb", "301824"),
("tvrage", "45210"),
("title", "11.22.63"),
("locations", ("11.22.63",)),
("imdb", "tt2879552"),
}
): {
"Season 1": [
{
"imdb": "tt4460418",
"title": "The Rabbit Hole",
"locations": (
"11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv",
),
"status": {"completed": True, "time": 0},
}
]
}
},
"Subbed Anime": {},
}
}
assert combine_watched_dicts(input_watched) == expected