Compare commits
283 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3175305bd | ||
|
|
5b1933cb08 | ||
|
|
ae71ca0940 | ||
|
|
9b38729b95 | ||
|
|
402c286742 | ||
|
|
dcd4ac1d36 | ||
|
|
e6fbf746d8 | ||
|
|
803d248cb8 | ||
|
|
713be6970c | ||
|
|
62509f16db | ||
|
|
84899aef50 | ||
|
|
86b30e1887 | ||
|
|
033ef76cfe | ||
|
|
815596379c | ||
|
|
bc5e8bc65d | ||
|
|
b32de7259b | ||
|
|
29cb0cebd5 | ||
|
|
6744ebcb5b | ||
|
|
c6b026a82d | ||
|
|
cc706938ce | ||
|
|
84b98db36b | ||
|
|
01ad15e2bd | ||
|
|
54adf0e56f | ||
|
|
025e40b098 | ||
|
|
4534854001 | ||
|
|
362d54b471 | ||
|
|
fa533ff65e | ||
|
|
96fe367562 | ||
|
|
9566ffa384 | ||
|
|
f5835e1e72 | ||
|
|
fe65716706 | ||
|
|
873735900f | ||
|
|
28c166146e | ||
|
|
c6affc3108 | ||
|
|
59b49fd0df | ||
|
|
6ec003f899 | ||
|
|
95f2a9ad30 | ||
|
|
7317e8533d | ||
|
|
f80c20d70c | ||
|
|
01fc13c3e0 | ||
|
|
1edfecae42 | ||
|
|
9dab9a4632 | ||
|
|
98a824bfdc | ||
|
|
8fa9351ef1 | ||
|
|
64b2197844 | ||
|
|
26f1f80be7 | ||
|
|
2e4c2a6817 | ||
|
|
9498335e22 | ||
|
|
26f40110d0 | ||
|
|
9375d482b0 | ||
|
|
de9180a124 | ||
|
|
ba480d2cb7 | ||
|
|
5014748ee1 | ||
|
|
4e25ae5539 | ||
|
|
a2b802a5de | ||
|
|
9739b27718 | ||
|
|
bdf6476689 | ||
|
|
b8b627be1a | ||
|
|
03cad668aa | ||
|
|
2e0ec9aa38 | ||
|
|
4b02aae889 | ||
|
|
c91ba0b1b3 | ||
|
|
6b7f8b04e6 | ||
|
|
5472baab51 | ||
|
|
d5b6859bf8 | ||
|
|
8e23847c79 | ||
|
|
0c1579bae7 | ||
|
|
3dc50fff95 | ||
|
|
b8273f50c2 | ||
|
|
dbea28e9c6 | ||
|
|
a1b11ab039 | ||
|
|
1841b0dea6 | ||
|
|
b311bf2770 | ||
|
|
df13cef760 | ||
|
|
76ac264b25 | ||
|
|
93bc94add5 | ||
|
|
79325b8c61 | ||
|
|
58c1eb7004 | ||
|
|
466f292feb | ||
|
|
4de25a0d4a | ||
|
|
43d6bc0d82 | ||
|
|
b53d7c9ecc | ||
|
|
116d50a75a | ||
|
|
e1fb365096 | ||
|
|
03617dacfc | ||
|
|
e6b33f1bc9 | ||
|
|
d9e6a554f6 | ||
|
|
7ef37fe848 | ||
|
|
dd64617cbd | ||
|
|
a227c01a7f | ||
|
|
da53609385 | ||
|
|
e94a8fb2c3 | ||
|
|
d87542ab78 | ||
|
|
945ffb2fb3 | ||
|
|
da808ba25e | ||
|
|
e4b4c7ba39 | ||
|
|
43ead4bb0f | ||
|
|
c4a2f8af39 | ||
|
|
fd281a50b6 | ||
|
|
f8ef4fe6c9 | ||
|
|
faef0ae246 | ||
|
|
117932e272 | ||
|
|
4297708d3e | ||
|
|
2d00d8cb3e | ||
|
|
0190788658 | ||
|
|
b46d4a7166 | ||
|
|
994d529f59 | ||
|
|
7f347ae186 | ||
|
|
cd4ce186ca | ||
|
|
ca5403f97b | ||
|
|
7bb76f62a5 | ||
|
|
dcdbe44648 | ||
|
|
f91005f0ba | ||
|
|
5baea978ab | ||
|
|
9cc1f96eea | ||
|
|
2a65c4b5ca | ||
|
|
e1ef6615cc | ||
|
|
d607c9c821 | ||
|
|
f6b2186824 | ||
|
|
a3fc53059c | ||
|
|
6afe123947 | ||
|
|
7e9c6bb338 | ||
|
|
89a2768fc9 | ||
|
|
9ff3bdf302 | ||
|
|
2c48e89435 | ||
|
|
6ccb68aeb3 | ||
|
|
032243de0a | ||
|
|
5b1b9ec222 | ||
|
|
375c6b23a5 | ||
|
|
b378dff0dc | ||
|
|
23f2d287d6 | ||
|
|
3cd73e54a1 | ||
|
|
bf5d875079 | ||
|
|
aef884523b | ||
|
|
2a59f38faf | ||
|
|
3a0e60c772 | ||
|
|
fb657d41db | ||
|
|
ac7f389563 | ||
|
|
237e82eceb | ||
|
|
8fab4304a4 | ||
|
|
971c9e9147 | ||
|
|
cacbca5a07 | ||
|
|
e4dbd8adfb | ||
|
|
19f77c89e7 | ||
|
|
ce1b922f71 | ||
|
|
81e967864d | ||
|
|
29f55104bc | ||
|
|
ff2e2deb20 | ||
|
|
3fa55cb41b | ||
|
|
aa5d97a0d5 | ||
|
|
89c4f15ae8 | ||
|
|
1351bfc1cf | ||
|
|
32cc76f043 | ||
|
|
968cb2091d | ||
|
|
8986c1037b | ||
|
|
87b4a950f1 | ||
|
|
9f61c7338d | ||
|
|
ffc81dad69 | ||
|
|
7eba46b5cb | ||
|
|
aa177666a5 | ||
|
|
7de7b42fd2 | ||
|
|
03d1fd8019 | ||
|
|
485ec5fe2d | ||
|
|
59bfbd9811 | ||
|
|
1e485b37f8 | ||
|
|
4adf94f24b | ||
|
|
1a0fab36d3 | ||
|
|
a1ef3b5a8d | ||
|
|
0c47ee7119 | ||
|
|
e51cf6e482 | ||
|
|
24d5de813d | ||
|
|
9921b2a355 | ||
|
|
faa378c75e | ||
|
|
26199100dc | ||
|
|
bee854f059 | ||
|
|
73c1ebf3ed | ||
|
|
397dd17429 | ||
|
|
73d18dad92 | ||
|
|
94d63a3fdb | ||
|
|
120d89e8be | ||
|
|
eb5534c61c | ||
|
|
99d217e8f1 | ||
|
|
f7e3f8ae2a | ||
|
|
2cebd2d73d | ||
|
|
18df322c41 | ||
|
|
fc80f50560 | ||
|
|
4870ff9e7a | ||
|
|
58337bd38c | ||
|
|
e6d1e0933a | ||
|
|
68e3f25ba4 | ||
|
|
c981426db6 | ||
|
|
916b16b12c | ||
|
|
a178d230de | ||
|
|
fffb04728a | ||
|
|
658361383a | ||
|
|
3330026de6 | ||
|
|
25fe426720 | ||
|
|
8d53b5b8c0 | ||
|
|
0774735f0f | ||
|
|
a5540b94d5 | ||
|
|
c69d59858d | ||
|
|
962b1149ad | ||
|
|
a8edee0354 | ||
|
|
3627dde64d | ||
|
|
80ec0e42c2 | ||
|
|
fd64088bde | ||
|
|
7832e41a3b | ||
|
|
cadd65d69b | ||
|
|
9f004797fc | ||
|
|
9041fee7ad | ||
|
|
9af6c9057c | ||
|
|
757ce91138 | ||
|
|
98f96ed5c7 | ||
|
|
3e15120e2a | ||
|
|
5824e6c0cc | ||
|
|
7087d75efb | ||
|
|
b2a06b8fd3 | ||
|
|
1ee055faf5 | ||
|
|
404089dfca | ||
|
|
ed24948dee | ||
|
|
1f16fcb8eb | ||
|
|
03de3affd7 | ||
|
|
2bad887659 | ||
|
|
796be47a63 | ||
|
|
dc1fe11590 | ||
|
|
13b4ff3215 | ||
|
|
dca54cf4fb | ||
|
|
a4365e59f3 | ||
|
|
b960bccb86 | ||
|
|
218037200c | ||
|
|
4ac670e837 | ||
|
|
96eff65c3e | ||
|
|
45471607c8 | ||
|
|
14885744b1 | ||
|
|
d1fd61f1d1 | ||
|
|
6c1ee4a7dc | ||
|
|
9a8e799e68 | ||
|
|
ffec4e2f28 | ||
|
|
00102891a5 | ||
|
|
aa76b83428 | ||
|
|
a644189ea5 | ||
|
|
c5d987a8c9 | ||
|
|
bdd68ad68d | ||
|
|
2d86bca781 | ||
|
|
1b01ff6ec2 | ||
|
|
f08ec43507 | ||
|
|
7f9424260a | ||
|
|
5f21943353 | ||
|
|
a5a795f43c | ||
|
|
fcb6d7625f | ||
|
|
fd2179998f | ||
|
|
654e7f20e1 | ||
|
|
1eb92cf7c1 | ||
|
|
111e284cc8 | ||
|
|
1a4e3f4ec4 | ||
|
|
4066228e57 | ||
|
|
59c6d278e3 | ||
|
|
39b33f3d43 | ||
|
|
e8faf52b2b | ||
|
|
370e9bac63 | ||
|
|
d0746cec5a | ||
|
|
251937431b | ||
|
|
50faf061af | ||
|
|
9ffbc49ad3 | ||
|
|
644dc8e3af | ||
|
|
47bc4e94dc | ||
|
|
f17d39fe17 | ||
|
|
966dcacf8d | ||
|
|
9afc00443c | ||
|
|
3ec177ea64 | ||
|
|
b360c9fd0b | ||
|
|
1ed791b1ed | ||
|
|
f19b1a3063 | ||
|
|
190a72bd3c | ||
|
|
c848106ce7 | ||
|
|
dd319271bd | ||
|
|
16879cc728 | ||
|
|
942ec3533f | ||
|
|
9f6edfc91a | ||
|
|
827ace2e97 | ||
|
|
f6b57a1b4d | ||
|
|
88a7526721 | ||
|
|
1efb4d8543 |
@@ -1 +1,15 @@
|
|||||||
.env
|
.dockerignore
|
||||||
|
.env
|
||||||
|
.env.sample
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
Dockerfile*
|
||||||
|
README.md
|
||||||
|
|
||||||
|
test
|
||||||
|
|
||||||
|
venv
|
||||||
96
.env.sample
96
.env.sample
@@ -1,42 +1,96 @@
|
|||||||
|
# Global Settings
|
||||||
|
|
||||||
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||||
DRYRUN = "True"
|
DRYRUN = "True"
|
||||||
|
|
||||||
## Additional logging information
|
## Additional logging information
|
||||||
DEBUG = "True"
|
DEBUG = "False"
|
||||||
|
|
||||||
## Debugging level, "info" is default, "debug" is more verbose
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
DEBUG_LEVEL = "info"
|
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
|
## How often to run the script in seconds
|
||||||
SLEEP_DURATION = "3600"
|
SLEEP_DURATION = "3600"
|
||||||
|
|
||||||
## Log file where all output will be written to
|
## Log file where all output will be written to
|
||||||
LOGFILE = "log.log"
|
LOGFILE = "log.log"
|
||||||
## Map usernames between plex and jellyfin in the event that they are different, order does not matter
|
|
||||||
#USER_MAPPING = { "testuser2": "testuser3" }
|
|
||||||
## Map libraries between plex and jellyfin in the even that they are different, order does not matter
|
|
||||||
#LIBRARY_MAPPING = { "Shows": "TV Shows" }
|
|
||||||
|
|
||||||
|
## Mark file where all shows/movies that have been marked as played will be written to
|
||||||
|
MARK_FILE = "mark.log"
|
||||||
|
|
||||||
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
|
## Timeout for requests for jellyfin
|
||||||
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
|
REQUEST_TIMEOUT = 300
|
||||||
## Comma seperated list for multiple servers
|
|
||||||
PLEX_BASEURL = "http://localhost:32400"
|
|
||||||
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
|
||||||
PLEX_TOKEN = "SuperSecretToken"
|
|
||||||
## If not using plex token then use username and password of the server admin along with the servername
|
|
||||||
#PLEX_USERNAME = ""
|
|
||||||
#PLEX_PASSWORD = ""
|
|
||||||
#PLEX_SERVERNAME = "Plex Server"
|
|
||||||
|
|
||||||
|
## Generate guids
|
||||||
|
## Generating guids is a slow process, so this is a way to speed up the process
|
||||||
|
## by using the location only, useful when using same files on multiple servers
|
||||||
|
GENERATE_GUIDS = "True"
|
||||||
|
|
||||||
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
|
## Generate locations
|
||||||
## Comma seperated list for multiple servers
|
## Generating locations is a slow process, so this is a way to speed up the process
|
||||||
JELLYFIN_BASEURL = "http://localhost:8096"
|
## by using the guid only, useful when using different files on multiple servers
|
||||||
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
GENERATE_LOCATIONS = "True"
|
||||||
JELLYFIN_TOKEN = "SuperSecretToken"
|
|
||||||
|
|
||||||
|
## Max threads for processing
|
||||||
|
MAX_THREADS = 32
|
||||||
|
|
||||||
|
## 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 event 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.
|
## 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 = ""
|
#BLACKLIST_LIBRARY = ""
|
||||||
#WHITELIST_LIBRARY = ""
|
#WHITELIST_LIBRARY = ""
|
||||||
#BLACKLIST_LIBRARY_TYPE = ""
|
#BLACKLIST_LIBRARY_TYPE = ""
|
||||||
#WHITELIST_LIBRARY_TYPE = ""
|
#WHITELIST_LIBRARY_TYPE = ""
|
||||||
#BLACKLIST_USERS = ""
|
#BLACKLIST_USERS = ""
|
||||||
WHITELIST_USERS = "testuser1,testuser2"
|
WHITELIST_USERS = "testuser1,testuser2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|||||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
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 Compose
|
||||||
|
- [ ] Docker
|
||||||
|
- [ ] Unraid
|
||||||
|
- [ ] Native
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||||
258
.github/workflows/ci.yml
vendored
258
.github/workflows/ci.yml
vendored
@@ -1,86 +1,172 @@
|
|||||||
name: CI
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
paths-ignore:
|
push:
|
||||||
- .gitignore
|
paths-ignore:
|
||||||
- "*.md"
|
- .gitignore
|
||||||
pull_request:
|
- "*.md"
|
||||||
paths-ignore:
|
pull_request:
|
||||||
- .gitignore
|
paths-ignore:
|
||||||
- "*.md"
|
- .gitignore
|
||||||
|
- "*.md"
|
||||||
jobs:
|
|
||||||
pytest:
|
jobs:
|
||||||
runs-on: ubuntu-latest
|
pytest:
|
||||||
steps:
|
runs-on: ubuntu-latest
|
||||||
- uses: actions/checkout@v2
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: "Install dependencies"
|
|
||||||
run: pip install -r requirements.txt && pip install -r test/requirements.txt
|
- name: "Install dependencies"
|
||||||
|
run: pip install -r requirements.txt && pip install -r test/requirements.txt
|
||||||
- name: "Run tests"
|
|
||||||
run: pytest -vvv
|
- name: "Run tests"
|
||||||
|
run: pytest -vvv
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
test:
|
||||||
needs: pytest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
- name: "Install dependencies"
|
||||||
- name: Docker meta
|
run: |
|
||||||
id: docker_meta
|
pip install -r requirements.txt
|
||||||
env:
|
sudo apt update && sudo apt install -y docker-compose
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
if: "${{ env.DOCKER_USERNAME != '' }}"
|
- name: "Checkout JellyPlex-Watched-CI"
|
||||||
uses: docker/metadata-action@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched # list of Docker images to use as base name for tags
|
repository: luigi311/JellyPlex-Watched-CI
|
||||||
tags: |
|
path: JellyPlex-Watched-CI
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
type=ref,event=branch
|
- name: "Start containers"
|
||||||
type=ref,event=pr
|
run: |
|
||||||
type=semver,pattern={{version}}
|
export PGID=$(id -g)
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
export PUID=$(id -u)
|
||||||
type=sha
|
|
||||||
|
sudo chown -R $PUID:$PGID JellyPlex-Watched-CI
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
docker pull lscr.io/linuxserver/plex &
|
||||||
|
docker pull lscr.io/linuxserver/jellyfin &
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
wait
|
||||||
|
|
||||||
- name: Login to DockerHub
|
docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml up -d
|
||||||
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml up -d
|
||||||
uses: docker/login-action@v1
|
# Wait for containers to start
|
||||||
with:
|
sleep 10
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml logs
|
||||||
|
docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml logs
|
||||||
- name: Build
|
|
||||||
id: build
|
- name: "Run tests"
|
||||||
if: "${{ steps.docker_meta.outcome == 'skipped' }}"
|
run: |
|
||||||
uses: docker/build-push-action@v2
|
# Test ci1
|
||||||
with:
|
mv test/ci1.env .env
|
||||||
context: .
|
python main.py
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
# Test ci2
|
||||||
push: false
|
mv test/ci2.env .env
|
||||||
tags: jellyplex-watched:action
|
python main.py
|
||||||
|
|
||||||
- name: Build Push
|
# Test ci3
|
||||||
id: build_push
|
mv test/ci3.env .env
|
||||||
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
python main.py
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
# Test again to test if it can handle existing data
|
||||||
context: .
|
python main.py
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
cat mark.log
|
||||||
push: true
|
python test/validate_ci_marklog.py
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
# Echo digest so users can validate their image
|
needs:
|
||||||
- name: Image digest
|
- pytest
|
||||||
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
- test
|
||||||
run: echo "${{ steps.build_push.outputs.digest }}"
|
env:
|
||||||
|
DEFAULT_VARIANT: alpine
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- dockerfile: Dockerfile.alpine
|
||||||
|
variant: alpine
|
||||||
|
- dockerfile: Dockerfile.slim
|
||||||
|
variant: slim
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: docker_meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }}
|
||||||
|
# Do not push to ghcr.io on PRs due to permission issues, only push if the owner is luigi311 so it doesnt fail on forks
|
||||||
|
ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' && github.repository_owner == 'luigi311'}}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == github.event.repository.default_branch }}
|
||||||
|
type=raw,value=dev,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == 'dev' }}
|
||||||
|
type=raw,value=latest,suffix=-${{ matrix.variant }},enable={{ is_default_branch }}
|
||||||
|
type=ref,event=branch,suffix=-${{ matrix.variant }}
|
||||||
|
type=ref,event=branch,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
|
||||||
|
type=ref,event=pr,suffix=-${{ matrix.variant }}
|
||||||
|
type=ref,event=pr,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
|
||||||
|
type=semver,pattern={{ version }},suffix=-${{ matrix.variant }}
|
||||||
|
type=semver,pattern={{ version }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
|
||||||
|
type=semver,pattern={{ major }}.{{ minor }},suffix=-${{ matrix.variant }}
|
||||||
|
type=semver,pattern={{ major }}.{{ minor }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
|
||||||
|
type=sha,suffix=-${{ matrix.variant }}
|
||||||
|
type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
if: "${{ env.DOCKER_USERNAME != '' }}"
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
if: "${{ steps.docker_meta.outputs.tags == '' }}"
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
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@v5
|
||||||
|
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
41
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: "23 20 * * 6"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ python ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: +security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
264
.gitignore
vendored
264
.gitignore
vendored
@@ -1,132 +1,132 @@
|
|||||||
.env
|
**.env*
|
||||||
*.prof
|
*.prof
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
pip-wheel-metadata/
|
pip-wheel-metadata/
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
*.manifest
|
*.manifest
|
||||||
*.spec
|
*.spec
|
||||||
|
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
.nox/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
local_settings.py
|
local_settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
|
|
||||||
# Flask stuff:
|
# Flask stuff:
|
||||||
instance/
|
instance/
|
||||||
.webassets-cache
|
.webassets-cache
|
||||||
|
|
||||||
# Scrapy stuff:
|
# Scrapy stuff:
|
||||||
.scrapy
|
.scrapy
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
# IPython
|
# IPython
|
||||||
profile_default/
|
profile_default/
|
||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
# Celery stuff
|
# Celery stuff
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
celerybeat.pid
|
celerybeat.pid
|
||||||
|
|
||||||
# SageMath parsed files
|
# SageMath parsed files
|
||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
|
|
||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.dmypy.json
|
.dmypy.json
|
||||||
dmypy.json
|
dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|||||||
43
.vscode/launch.json
vendored
43
.vscode/launch.json
vendored
@@ -1,16 +1,27 @@
|
|||||||
{
|
{
|
||||||
// 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
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
}
|
"name": "Pytest",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "pytest",
|
||||||
|
"args": [
|
||||||
|
"-vv"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"[python]" : {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
},
|
||||||
|
"python.formatting.provider": "black",
|
||||||
|
|
||||||
|
}
|
||||||
35
Dockerfile
35
Dockerfile
@@ -1,35 +0,0 @@
|
|||||||
FROM python:3-slim
|
|
||||||
|
|
||||||
ENV DRYRUN 'True'
|
|
||||||
ENV DEBUG 'True'
|
|
||||||
ENV DEBUG_LEVEL 'INFO'
|
|
||||||
ENV SLEEP_DURATION '3600'
|
|
||||||
ENV LOGFILE 'log.log'
|
|
||||||
|
|
||||||
ENV USER_MAPPING '{ "User Test": "User Test2" }'
|
|
||||||
ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }'
|
|
||||||
|
|
||||||
ENV PLEX_BASEURL 'http://localhost:32400'
|
|
||||||
ENV PLEX_TOKEN ''
|
|
||||||
ENV PLEX_USERNAME ''
|
|
||||||
ENV PLEX_PASSWORD ''
|
|
||||||
ENV PLEX_SERVERNAME ''
|
|
||||||
|
|
||||||
ENV JELLYFIN_BASEURL 'http://localhost:8096'
|
|
||||||
ENV JELLYFIN_TOKEN ''
|
|
||||||
|
|
||||||
ENV BLACKLIST_LIBRARY ''
|
|
||||||
ENV WHITELIST_LIBRARY ''
|
|
||||||
ENV BLACKLIST_LIBRARY_TYPE ''
|
|
||||||
ENV WHITELIST_LIBRARY_TYPE ''
|
|
||||||
ENV BLACKLIST_USERS ''
|
|
||||||
ENV WHITELIST_USERS ''
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ./requirements.txt ./
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["python", "-u", "main.py"]
|
|
||||||
53
Dockerfile.alpine
Normal file
53
Dockerfile.alpine
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
ENV DRYRUN 'True'
|
||||||
|
ENV DEBUG 'True'
|
||||||
|
ENV DEBUG_LEVEL 'INFO'
|
||||||
|
ENV RUN_ONLY_ONCE 'False'
|
||||||
|
ENV SLEEP_DURATION '3600'
|
||||||
|
ENV LOGFILE 'log.log'
|
||||||
|
ENV MARKFILE 'mark.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 ''
|
||||||
|
|
||||||
|
|
||||||
|
RUN apk add --no-cache tini && \
|
||||||
|
addgroup --system jellyplex_user && \
|
||||||
|
adduser --system --no-create-home jellyplex_user --ingroup jellyplex_user && \
|
||||||
|
mkdir -p /app && \
|
||||||
|
chown -R jellyplex_user:jellyplex_user /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY --chown=jellyplex_user:jellyplex_user . .
|
||||||
|
|
||||||
|
USER jellyplex_user
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
CMD ["python", "-u", "main.py"]
|
||||||
56
Dockerfile.slim
Normal file
56
Dockerfile.slim
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV DRYRUN 'True'
|
||||||
|
ENV DEBUG 'True'
|
||||||
|
ENV DEBUG_LEVEL 'INFO'
|
||||||
|
ENV RUN_ONLY_ONCE 'False'
|
||||||
|
ENV SLEEP_DURATION '3600'
|
||||||
|
ENV LOGFILE 'log.log'
|
||||||
|
ENV MARKFILE 'mark.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 ''
|
||||||
|
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install tini --yes --no-install-recommends && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
addgroup --system jellyplex_user && \
|
||||||
|
adduser --system --no-create-home jellyplex_user --ingroup jellyplex_user && \
|
||||||
|
mkdir -p /app && \
|
||||||
|
chown -R jellyplex_user:jellyplex_user /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY --chown=jellyplex_user:jellyplex_user . .
|
||||||
|
|
||||||
|
USER jellyplex_user
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/tini", "--"]
|
||||||
|
CMD ["python", "-u", "main.py"]
|
||||||
69
README.md
69
README.md
@@ -1,32 +1,64 @@
|
|||||||
# JellyPlex-Watched
|
# JellyPlex-Watched
|
||||||
|
|
||||||
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=luigi311/JellyPlex-Watched\&utm_campaign=Badge_Grade)
|
||||||
|
|
||||||
Sync watched between jellyfin and plex
|
Sync watched between jellyfin and plex locally
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by enterying multiple options in the .env plex/jellyfin section seperated by commas.
|
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses file names and provider ids to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by 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 usernames
|
||||||
|
* \[x] Use single login
|
||||||
|
* \[x] One way/multi way sync
|
||||||
|
* \[x] Sync watched
|
||||||
|
* \[x] Sync in progress
|
||||||
|
|
||||||
|
### Jellyfin
|
||||||
|
|
||||||
|
* \[x] Match via filenames
|
||||||
|
* \[x] Match via provider ids
|
||||||
|
* \[x] Map usernames
|
||||||
|
* \[x] Use single login
|
||||||
|
* \[x] One way/multi way sync
|
||||||
|
* \[x] Sync watched
|
||||||
|
* \[ ] Sync in progress
|
||||||
|
|
||||||
|
### Emby
|
||||||
|
|
||||||
|
* \[ ] Match via filenames
|
||||||
|
* \[ ] Match via provider ids
|
||||||
|
* \[ ] Map usernames
|
||||||
|
* \[ ] Use single login
|
||||||
|
* \[ ] One way/multi way sync
|
||||||
|
* \[ ] Sync watched
|
||||||
|
* \[ ] Sync in progress
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
Full list of configuration options can be found in the [.env.sample](.env.sample)
|
||||||
|
|
||||||
## 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 +66,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 +80,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,17 +88,26 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Troubleshooting/Issues
|
||||||
|
|
||||||
|
* Jellyfin
|
||||||
|
* Attempt to decode JSON with unexpected mimetype, make sure you enable remote access or add your docker subnet to lan networks in jellyfin settings
|
||||||
|
|
||||||
|
* Configuration
|
||||||
|
* Do not use quotes around variables in docker compose
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
|||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
|
- MARKFILE=/tmp/mark.log
|
||||||
|
- USER_MAPPING={"user1":"user2"}
|
||||||
|
- LIBRARY_MAPPING={"TV Shows":"Shows"}
|
||||||
|
- BLACKLIST_LIBRARY=
|
||||||
|
- WHITELIST_LIBRARY=
|
||||||
|
- BLACKLIST_LIBRARY_TYPE=
|
||||||
|
- WHITELIST_LIBRARY_TYPE=
|
||||||
|
- BLACKLIST_USERS=
|
||||||
|
- WHITELIST_USERS=
|
||||||
|
- PLEX_BASEURL=https://localhost:32400
|
||||||
|
- PLEX_TOKEN=plex_token
|
||||||
|
- JELLYFIN_BASEURL=http://localhost:8096
|
||||||
|
- JELLYFIN_TOKEN=jelly_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
21
main.py
@@ -1,10 +1,11 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
# Check python version 3.6 or higher
|
# Check python version 3.9 or higher
|
||||||
if not (3, 6) <= tuple(map(int, sys.version_info[:2])):
|
if not (3, 9) <= tuple(map(int, sys.version_info[:2])):
|
||||||
print("This script requires Python 3.6 or higher")
|
print("This script requires Python 3.9 or higher")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
from src.main import main
|
from src.main import main
|
||||||
main()
|
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
plexapi
|
PlexAPI==4.15.7
|
||||||
requests
|
requests==2.31.0
|
||||||
python-dotenv
|
python-dotenv==1.0.0
|
||||||
|
|||||||
92
src/black_white.py
Normal file
92
src/black_white.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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,
|
||||||
|
"Black",
|
||||||
|
library_mapping,
|
||||||
|
user_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
whitelist_library, whitelist_library_type, whitelist_users = setup_x_lists(
|
||||||
|
whitelist_library,
|
||||||
|
whitelist_library_type,
|
||||||
|
whitelist_users,
|
||||||
|
"White",
|
||||||
|
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
|
||||||
283
src/functions.py
283
src/functions.py
@@ -1,155 +1,128 @@
|
|||||||
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")
|
||||||
|
markfile = os.getenv("MARKFILE", "mark.log")
|
||||||
def logger(message: str, log_type=0):
|
|
||||||
debug = str_to_bool(os.getenv("DEBUG", "True"))
|
|
||||||
debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
|
def logger(message: str, log_type=0):
|
||||||
|
debug = str_to_bool(os.getenv("DEBUG", "False"))
|
||||||
output = str(message)
|
debug_level = os.getenv("DEBUG_LEVEL", "info").lower()
|
||||||
if log_type == 0:
|
|
||||||
pass
|
output = str(message)
|
||||||
elif log_type == 1 and (debug or debug_level == "info"):
|
if log_type == 0:
|
||||||
output = f"[INFO]: {output}"
|
pass
|
||||||
elif log_type == 2:
|
elif log_type == 1 and (debug and debug_level in ("info", "debug")):
|
||||||
output = f"[ERROR]: {output}"
|
output = f"[INFO]: {output}"
|
||||||
elif log_type == 3 and (debug and debug_level == "debug"):
|
elif log_type == 2:
|
||||||
output = f"[DEBUG]: {output}"
|
output = f"[ERROR]: {output}"
|
||||||
else:
|
elif log_type == 3 and (debug and debug_level == "debug"):
|
||||||
output = None
|
output = f"[DEBUG]: {output}"
|
||||||
|
elif log_type == 4:
|
||||||
if output is not None:
|
output = f"[WARNING]: {output}"
|
||||||
print(output)
|
elif log_type == 5:
|
||||||
file = open(logfile, "a", encoding="utf-8")
|
output = f"[MARK]: {output}"
|
||||||
file.write(output + "\n")
|
elif log_type == 6:
|
||||||
|
output = f"[DRYRUN]: {output}"
|
||||||
# Reimplementation of distutils.util.strtobool due to it being deprecated
|
else:
|
||||||
# Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
|
output = None
|
||||||
def str_to_bool(value: any) -> bool:
|
|
||||||
if not value:
|
if output is not None:
|
||||||
return False
|
print(output)
|
||||||
return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
|
file = open(logfile, "a", encoding="utf-8")
|
||||||
|
file.write(output + "\n")
|
||||||
# Get mapped value
|
|
||||||
def search_mapping(dictionary: dict, key_value: str):
|
|
||||||
if key_value in dictionary.keys():
|
def log_marked(
|
||||||
return dictionary[key_value]
|
username: str, library: str, movie_show: str, episode: str = None, duration=None
|
||||||
elif key_value.lower() in dictionary.keys():
|
):
|
||||||
return dictionary[key_value.lower()]
|
if markfile is None:
|
||||||
elif key_value in dictionary.values():
|
return
|
||||||
return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
|
|
||||||
elif key_value.lower() in dictionary.values():
|
output = f"{username}/{library}/{movie_show}"
|
||||||
return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())]
|
|
||||||
else:
|
if episode:
|
||||||
return None
|
output += f"/{episode}"
|
||||||
|
|
||||||
|
if duration:
|
||||||
def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
|
output += f"/{duration}"
|
||||||
skip_reason = None
|
|
||||||
|
file = open(f"{markfile}", "a", encoding="utf-8")
|
||||||
if library_type.lower() in blacklist_library_type:
|
file.write(output + "\n")
|
||||||
skip_reason = "is blacklist_library_type"
|
|
||||||
|
|
||||||
if library_title.lower() in [x.lower() for x in blacklist_library]:
|
# Reimplementation of distutils.util.strtobool due to it being deprecated
|
||||||
skip_reason = "is blacklist_library"
|
# Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
|
||||||
|
def str_to_bool(value: any) -> bool:
|
||||||
library_other = None
|
if not value:
|
||||||
if library_mapping:
|
return False
|
||||||
library_other = search_mapping(library_mapping, library_title)
|
return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
|
||||||
if library_other:
|
|
||||||
if library_other.lower() in [x.lower() for x in blacklist_library]:
|
|
||||||
skip_reason = "is blacklist_library"
|
# Search for nested element in list
|
||||||
|
def contains_nested(element, lst):
|
||||||
if len(whitelist_library_type) > 0:
|
if lst is None:
|
||||||
if library_type.lower() not in whitelist_library_type:
|
return None
|
||||||
skip_reason = "is not whitelist_library_type"
|
|
||||||
|
for i, item in enumerate(lst):
|
||||||
# if whitelist is not empty and library is not in whitelist
|
if item is None:
|
||||||
if len(whitelist_library) > 0:
|
continue
|
||||||
if library_title.lower() not in [x.lower() for x in whitelist_library]:
|
if element in item:
|
||||||
skip_reason = "is not whitelist_library"
|
return i
|
||||||
|
elif element == item:
|
||||||
if library_other:
|
return i
|
||||||
if library_other.lower() not in [x.lower() for x in whitelist_library]:
|
return None
|
||||||
skip_reason = "is not whitelist_library"
|
|
||||||
|
|
||||||
return skip_reason
|
# Get mapped value
|
||||||
|
def search_mapping(dictionary: dict, key_value: str):
|
||||||
|
if key_value in dictionary.keys():
|
||||||
def generate_library_guids_dict(user_list: dict):
|
return dictionary[key_value]
|
||||||
show_output_dict = {}
|
elif key_value.lower() in dictionary.keys():
|
||||||
episode_output_dict = {}
|
return dictionary[key_value.lower()]
|
||||||
movies_output_dict = {}
|
elif key_value in dictionary.values():
|
||||||
|
return list(dictionary.keys())[list(dictionary.values()).index(key_value)]
|
||||||
try:
|
elif key_value.lower() in dictionary.values():
|
||||||
show_output_keys = user_list.keys()
|
return list(dictionary.keys())[
|
||||||
show_output_keys = ([ dict(x) for x in list(show_output_keys) ])
|
list(dictionary.values()).index(key_value.lower())
|
||||||
for show_key in show_output_keys:
|
]
|
||||||
for provider_key, provider_value in show_key.items():
|
else:
|
||||||
# Skip title
|
return None
|
||||||
if provider_key.lower() == "title":
|
|
||||||
continue
|
|
||||||
if provider_key.lower() not in show_output_dict:
|
def future_thread_executor(
|
||||||
show_output_dict[provider_key.lower()] = []
|
args: list, threads: int = None, override_threads: bool = False
|
||||||
if provider_key.lower() == "locations":
|
):
|
||||||
for show_location in provider_value:
|
futures_list = []
|
||||||
show_output_dict[provider_key.lower()].append(show_location)
|
results = []
|
||||||
else:
|
|
||||||
show_output_dict[provider_key.lower()].append(provider_value.lower())
|
workers = min(int(os.getenv("MAX_THREADS", 32)), os.cpu_count() * 2)
|
||||||
except:
|
if threads:
|
||||||
pass
|
workers = min(threads, workers)
|
||||||
|
|
||||||
try:
|
if override_threads:
|
||||||
for show in user_list:
|
workers = threads
|
||||||
for season in user_list[show]:
|
|
||||||
for episode in user_list[show][season]:
|
# If only one worker, run in main thread to avoid overhead
|
||||||
for episode_key, episode_value in episode.items():
|
if workers == 1:
|
||||||
if episode_key.lower() not in episode_output_dict:
|
results = []
|
||||||
episode_output_dict[episode_key.lower()] = []
|
for arg in args:
|
||||||
if episode_key == "locations":
|
results.append(arg[0](*arg[1:]))
|
||||||
for episode_location in episode_value:
|
return results
|
||||||
episode_output_dict[episode_key.lower()].append(episode_location)
|
|
||||||
else:
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
episode_output_dict[episode_key.lower()].append(episode_value.lower())
|
for arg in args:
|
||||||
except:
|
# * arg unpacks the list into actual arguments
|
||||||
pass
|
futures_list.append(executor.submit(*arg))
|
||||||
|
|
||||||
try:
|
for future in futures_list:
|
||||||
for movie in user_list:
|
try:
|
||||||
for movie_key, movie_value in movie.items():
|
result = future.result()
|
||||||
if movie_key.lower() not in movies_output_dict:
|
results.append(result)
|
||||||
movies_output_dict[movie_key.lower()] = []
|
except Exception as e:
|
||||||
if movie_key == "locations":
|
raise Exception(e)
|
||||||
for movie_location in movie_value:
|
|
||||||
movies_output_dict[movie_key.lower()].append(movie_location)
|
return results
|
||||||
else:
|
|
||||||
movies_output_dict[movie_key.lower()].append(movie_value.lower())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
1186
src/jellyfin.py
1186
src/jellyfin.py
File diff suppressed because it is too large
Load Diff
278
src/library.py
Normal file
278
src/library.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
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("Skipping show_output_dict ", 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_output_dict["show"] = []
|
||||||
|
episode_output_dict["season"] = []
|
||||||
|
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]:
|
||||||
|
# Add the show title to the episode_output_dict if it doesn't exist
|
||||||
|
if "show" not in episode_output_dict:
|
||||||
|
episode_output_dict["show"] = [None] * episode_counter
|
||||||
|
|
||||||
|
# Add the season number to the episode_output_dict if it doesn't exist
|
||||||
|
if "season" not in episode_output_dict:
|
||||||
|
episode_output_dict["season"] = [None] * episode_counter
|
||||||
|
|
||||||
|
# Add the show title to the episode_output_dict
|
||||||
|
episode_output_dict["show"].append(dict(show))
|
||||||
|
|
||||||
|
# Add the season number to the episode_output_dict
|
||||||
|
episode_output_dict["season"].append(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("Skipping episode_output_dict", 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("Skipping movies_output_dict failed", 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
|
||||||
867
src/main.py
867
src/main.py
@@ -1,441 +1,426 @@
|
|||||||
import copy, os, traceback, json
|
import os, traceback, json
|
||||||
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)
|
logger(f"Server 1 users: {server_1_users}", 1)
|
||||||
if library_1 in modified_watched_list_1[user_1]:
|
logger(f"Server 2 users: {server_2_users}", 1)
|
||||||
if library_1 in watched_list_2[user_2]:
|
|
||||||
library_2 = library_1
|
users = combine_user_lists(server_1_users, server_2_users, user_mapping)
|
||||||
elif library_other in watched_list_2[user_2]:
|
logger(f"User list that exist on both servers {users}", 1)
|
||||||
library_2 = library_other
|
|
||||||
else:
|
users_filtered = filter_user_lists(users, blacklist_users, whitelist_users)
|
||||||
logger(f"library {library_1} and {library_other} not found in watched list 2", 1)
|
logger(f"Filtered user list {users_filtered}", 1)
|
||||||
continue
|
|
||||||
|
output_server_1_users = generate_server_users(server_1, users_filtered)
|
||||||
# Movies
|
output_server_2_users = generate_server_users(server_2, users_filtered)
|
||||||
if isinstance(watched_list_1[user_1][library_1], list):
|
|
||||||
_, _, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2])
|
# Check if users is none or empty
|
||||||
for movie in watched_list_1[user_1][library_1]:
|
if output_server_1_users is None or len(output_server_1_users) == 0:
|
||||||
movie_found = False
|
logger(
|
||||||
for movie_key, movie_value in movie.items():
|
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}"
|
||||||
if movie_key == "locations":
|
)
|
||||||
for location in movie_value:
|
|
||||||
if location in movies_watched_list_2_keys_dict["locations"]:
|
if output_server_2_users is None or len(output_server_2_users) == 0:
|
||||||
movie_found = True
|
logger(
|
||||||
break
|
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}"
|
||||||
else:
|
)
|
||||||
if movie_key in movies_watched_list_2_keys_dict.keys():
|
|
||||||
if movie_value in movies_watched_list_2_keys_dict[movie_key]:
|
if (
|
||||||
movie_found = True
|
output_server_1_users is None
|
||||||
|
or len(output_server_1_users) == 0
|
||||||
if movie_found:
|
or output_server_2_users is None
|
||||||
logger(f"Removing {movie} from {library_1}", 3)
|
or len(output_server_2_users) == 0
|
||||||
modified_watched_list_1[user_1][library_1].remove(movie)
|
):
|
||||||
break
|
raise Exception("No users found for one or both servers")
|
||||||
|
|
||||||
|
logger(f"Server 1 users: {output_server_1_users}", 1)
|
||||||
# TV Shows
|
logger(f"Server 2 users: {output_server_2_users}", 1)
|
||||||
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
|
return output_server_1_users, output_server_2_users
|
||||||
_, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2])
|
|
||||||
|
|
||||||
for show_key_1 in watched_list_1[user_1][library_1].keys():
|
def generate_server_connections():
|
||||||
show_key_dict = dict(show_key_1)
|
servers = []
|
||||||
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]:
|
plex_baseurl = os.getenv("PLEX_BASEURL", None)
|
||||||
episode_found = False
|
plex_token = os.getenv("PLEX_TOKEN", None)
|
||||||
for episode_key, episode_value in episode.items():
|
plex_username = os.getenv("PLEX_USERNAME", None)
|
||||||
# If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1
|
plex_password = os.getenv("PLEX_PASSWORD", None)
|
||||||
if episode_key == "locations":
|
plex_servername = os.getenv("PLEX_SERVERNAME", None)
|
||||||
for location in episode_value:
|
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
|
||||||
if location in episode_watched_list_2_keys_dict["locations"]:
|
|
||||||
episode_found = True
|
if plex_baseurl and plex_token:
|
||||||
break
|
plex_baseurl = plex_baseurl.split(",")
|
||||||
|
plex_token = plex_token.split(",")
|
||||||
else:
|
|
||||||
if episode_key in episode_watched_list_2_keys_dict.keys():
|
if len(plex_baseurl) != len(plex_token):
|
||||||
if episode_value in episode_watched_list_2_keys_dict[episode_key]:
|
raise Exception(
|
||||||
episode_found = True
|
"PLEX_BASEURL and PLEX_TOKEN must have the same number of entries"
|
||||||
|
)
|
||||||
if episode_found:
|
|
||||||
if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]:
|
for i, url in enumerate(plex_baseurl):
|
||||||
logger(f"Removing {episode} from {show_key_dict['title']}", 3)
|
server = Plex(
|
||||||
modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode)
|
baseurl=url.strip(),
|
||||||
break
|
token=plex_token[i].strip(),
|
||||||
|
username=None,
|
||||||
# Remove empty seasons
|
password=None,
|
||||||
if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0:
|
servername=None,
|
||||||
if season in modified_watched_list_1[user_1][library_1][show_key_1]:
|
ssl_bypass=ssl_bypass,
|
||||||
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]
|
|
||||||
|
logger(f"Plex Server {i} info: {server.info()}", 3)
|
||||||
# If the show is empty, remove the show
|
|
||||||
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
|
servers.append(
|
||||||
if show_key_1 in modified_watched_list_1[user_1][library_1]:
|
(
|
||||||
logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1)
|
"plex",
|
||||||
del modified_watched_list_1[user_1][library_1][show_key_1]
|
server,
|
||||||
|
)
|
||||||
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 plex_username and plex_password and plex_servername:
|
||||||
# If library is empty then remove it
|
plex_username = plex_username.split(",")
|
||||||
if len(modified_watched_list_1[user_1][library_1]) == 0:
|
plex_password = plex_password.split(",")
|
||||||
logger(f"Removing {library_1} from {user_1} because it is empty", 1)
|
plex_servername = plex_servername.split(",")
|
||||||
del modified_watched_list_1[user_1][library_1]
|
|
||||||
|
if len(plex_username) != len(plex_password) or len(plex_username) != len(
|
||||||
if user_1 in modified_watched_list_1:
|
plex_servername
|
||||||
# If user is empty delete user
|
):
|
||||||
if len(modified_watched_list_1[user_1]) == 0:
|
raise Exception(
|
||||||
logger(f"Removing {user_1} from watched list 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]
|
)
|
||||||
|
|
||||||
return modified_watched_list_1
|
for i, username in enumerate(plex_username):
|
||||||
|
server = Plex(
|
||||||
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):
|
baseurl=None,
|
||||||
if blacklist_library:
|
token=None,
|
||||||
if len(blacklist_library) > 0:
|
username=username.strip(),
|
||||||
blacklist_library = blacklist_library.split(",")
|
password=plex_password[i].strip(),
|
||||||
blacklist_library = [x.strip() for x in blacklist_library]
|
servername=plex_servername[i].strip(),
|
||||||
if library_mapping:
|
ssl_bypass=ssl_bypass,
|
||||||
temp_library = []
|
)
|
||||||
for library in blacklist_library:
|
|
||||||
library_other = search_mapping(library_mapping, library)
|
logger(f"Plex Server {i} info: {server.info()}", 3)
|
||||||
if library_other:
|
servers.append(
|
||||||
temp_library.append(library_other)
|
(
|
||||||
|
"plex",
|
||||||
blacklist_library = blacklist_library + temp_library
|
server,
|
||||||
else:
|
)
|
||||||
blacklist_library = []
|
)
|
||||||
logger(f"Blacklist Library: {blacklist_library}", 1)
|
|
||||||
|
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
|
||||||
if whitelist_library:
|
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
|
||||||
if len(whitelist_library) > 0:
|
|
||||||
whitelist_library = whitelist_library.split(",")
|
if jellyfin_baseurl and jellyfin_token:
|
||||||
whitelist_library = [x.strip() for x in whitelist_library]
|
jellyfin_baseurl = jellyfin_baseurl.split(",")
|
||||||
if library_mapping:
|
jellyfin_token = jellyfin_token.split(",")
|
||||||
temp_library = []
|
|
||||||
for library in whitelist_library:
|
if len(jellyfin_baseurl) != len(jellyfin_token):
|
||||||
library_other = search_mapping(library_mapping, library)
|
raise Exception(
|
||||||
if library_other:
|
"JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries"
|
||||||
temp_library.append(library_other)
|
)
|
||||||
|
|
||||||
whitelist_library = whitelist_library + temp_library
|
for i, baseurl in enumerate(jellyfin_baseurl):
|
||||||
else:
|
baseurl = baseurl.strip()
|
||||||
whitelist_library = []
|
if baseurl[-1] == "/":
|
||||||
logger(f"Whitelist Library: {whitelist_library}", 1)
|
baseurl = baseurl[:-1]
|
||||||
|
|
||||||
if blacklist_library_type:
|
server = Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip())
|
||||||
if len(blacklist_library_type) > 0:
|
|
||||||
blacklist_library_type = blacklist_library_type.split(",")
|
logger(f"Jellyfin Server {i} info: {server.info()}", 3)
|
||||||
blacklist_library_type = [x.lower().strip() for x in blacklist_library_type]
|
servers.append(
|
||||||
else:
|
(
|
||||||
blacklist_library_type = []
|
"jellyfin",
|
||||||
logger(f"Blacklist Library Type: {blacklist_library_type}", 1)
|
server,
|
||||||
|
)
|
||||||
if whitelist_library_type:
|
)
|
||||||
if len(whitelist_library_type) > 0:
|
|
||||||
whitelist_library_type = whitelist_library_type.split(",")
|
return servers
|
||||||
whitelist_library_type = [x.lower().strip() for x in whitelist_library_type]
|
|
||||||
else:
|
|
||||||
whitelist_library_type = []
|
def get_server_watched(
|
||||||
logger(f"Whitelist Library Type: {whitelist_library_type}", 1)
|
server_connection: list,
|
||||||
|
users: dict,
|
||||||
if blacklist_users:
|
blacklist_library: list,
|
||||||
if len(blacklist_users) > 0:
|
whitelist_library: list,
|
||||||
blacklist_users = blacklist_users.split(",")
|
blacklist_library_type: list,
|
||||||
blacklist_users = [x.lower().strip() for x in blacklist_users]
|
whitelist_library_type: list,
|
||||||
if user_mapping:
|
library_mapping: dict,
|
||||||
temp_users = []
|
):
|
||||||
for user in blacklist_users:
|
if server_connection[0] == "plex":
|
||||||
user_other = search_mapping(user_mapping, user)
|
return server_connection[1].get_watched(
|
||||||
if user_other:
|
users,
|
||||||
temp_users.append(user_other)
|
blacklist_library,
|
||||||
|
whitelist_library,
|
||||||
blacklist_users = blacklist_users + temp_users
|
blacklist_library_type,
|
||||||
else:
|
whitelist_library_type,
|
||||||
blacklist_users = []
|
library_mapping,
|
||||||
logger(f"Blacklist Users: {blacklist_users}", 1)
|
)
|
||||||
|
elif server_connection[0] == "jellyfin":
|
||||||
if whitelist_users:
|
return server_connection[1].get_watched(
|
||||||
if len(whitelist_users) > 0:
|
users,
|
||||||
whitelist_users = whitelist_users.split(",")
|
blacklist_library,
|
||||||
whitelist_users = [x.lower().strip() for x in whitelist_users]
|
whitelist_library,
|
||||||
if user_mapping:
|
blacklist_library_type,
|
||||||
temp_users = []
|
whitelist_library_type,
|
||||||
for user in whitelist_users:
|
library_mapping,
|
||||||
user_other = search_mapping(user_mapping, user)
|
)
|
||||||
if user_other:
|
|
||||||
temp_users.append(user_other)
|
|
||||||
|
def update_server_watched(
|
||||||
whitelist_users = whitelist_users + temp_users
|
server_connection: list,
|
||||||
else:
|
server_watched_filtered: dict,
|
||||||
whitelist_users = []
|
user_mapping: dict,
|
||||||
else:
|
library_mapping: dict,
|
||||||
whitelist_users = []
|
dryrun: bool,
|
||||||
logger(f"Whitelist Users: {whitelist_users}", 1)
|
):
|
||||||
|
if server_connection[0] == "plex":
|
||||||
return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users
|
server_connection[1].update_watched(
|
||||||
|
server_watched_filtered, user_mapping, library_mapping, dryrun
|
||||||
def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None):
|
)
|
||||||
|
elif server_connection[0] == "jellyfin":
|
||||||
# generate list of users from server 1 and server 2
|
server_connection[1].update_watched(
|
||||||
server_1_type = server_1[0]
|
server_watched_filtered, user_mapping, library_mapping, dryrun
|
||||||
server_1_connection = server_1[1]
|
)
|
||||||
server_2_type = server_2[0]
|
|
||||||
server_2_connection = server_2[1]
|
|
||||||
|
def should_sync_server(server_1_type, server_2_type):
|
||||||
server_1_users = []
|
sync_from_plex_to_jellyfin = str_to_bool(
|
||||||
if server_1_type == "plex":
|
os.getenv("SYNC_FROM_PLEX_TO_JELLYFIN", "True")
|
||||||
server_1_users = [ x.title.lower() for x in server_1_connection.users ]
|
)
|
||||||
elif server_1_type == "jellyfin":
|
sync_from_jelly_to_plex = str_to_bool(
|
||||||
server_1_users = [ key.lower() for key in server_1_connection.users.keys() ]
|
os.getenv("SYNC_FROM_JELLYFIN_TO_PLEX", "True")
|
||||||
|
)
|
||||||
server_2_users = []
|
sync_from_plex_to_plex = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_PLEX", "True"))
|
||||||
if server_2_type == "plex":
|
sync_from_jelly_to_jellyfin = str_to_bool(
|
||||||
server_2_users = [ x.title.lower() for x in server_2_connection.users ]
|
os.getenv("SYNC_FROM_JELLYFIN_TO_JELLYFIN", "True")
|
||||||
elif server_2_type == "jellyfin":
|
)
|
||||||
server_2_users = [ key.lower() for key in server_2_connection.users.keys() ]
|
|
||||||
|
if (
|
||||||
|
server_1_type == "plex"
|
||||||
# combined list of overlapping users from plex and jellyfin
|
and server_2_type == "plex"
|
||||||
users = {}
|
and not sync_from_plex_to_plex
|
||||||
|
):
|
||||||
for server_1_user in server_1_users:
|
logger("Sync between plex and plex is disabled", 1)
|
||||||
if user_mapping:
|
return False
|
||||||
jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user)
|
|
||||||
if jellyfin_plex_mapped_user:
|
if (
|
||||||
users[server_1_user] = jellyfin_plex_mapped_user
|
server_1_type == "plex"
|
||||||
continue
|
and server_2_type == "jellyfin"
|
||||||
|
and not sync_from_jelly_to_plex
|
||||||
if server_1_user in server_2_users:
|
):
|
||||||
users[server_1_user] = server_1_user
|
logger("Sync from jellyfin to plex disabled", 1)
|
||||||
|
return False
|
||||||
for server_2_user in server_2_users:
|
|
||||||
if user_mapping:
|
if (
|
||||||
plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user)
|
server_1_type == "jellyfin"
|
||||||
if plex_jellyfin_mapped_user:
|
and server_2_type == "jellyfin"
|
||||||
users[plex_jellyfin_mapped_user] = server_2_user
|
and not sync_from_jelly_to_jellyfin
|
||||||
continue
|
):
|
||||||
|
logger("Sync between jellyfin and jellyfin is disabled", 1)
|
||||||
if server_2_user in server_1_users:
|
return False
|
||||||
users[server_2_user] = server_2_user
|
|
||||||
|
if (
|
||||||
logger(f"User list that exist on both servers {users}", 1)
|
server_1_type == "jellyfin"
|
||||||
|
and server_2_type == "plex"
|
||||||
users_filtered = {}
|
and not sync_from_plex_to_jellyfin
|
||||||
for user in users:
|
):
|
||||||
# whitelist_user is not empty and user lowercase is not in whitelist lowercase
|
logger("Sync from plex to jellyfin is disabled", 1)
|
||||||
if len(whitelist_users) > 0:
|
return False
|
||||||
if user not in whitelist_users and users[user] not in whitelist_users:
|
|
||||||
logger(f"{user} or {users[user]} is not in whitelist", 1)
|
return True
|
||||||
continue
|
|
||||||
|
|
||||||
if user not in blacklist_users and users[user] not in blacklist_users:
|
def main_loop():
|
||||||
users_filtered[user] = users[user]
|
logfile = os.getenv("LOGFILE", "log.log")
|
||||||
|
# Delete logfile if it exists
|
||||||
logger(f"Filtered user list {users_filtered}", 1)
|
if os.path.exists(logfile):
|
||||||
|
os.remove(logfile)
|
||||||
if server_1_type == "plex":
|
|
||||||
output_server_1_users = []
|
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
|
||||||
for plex_user in server_1_connection.users:
|
logger(f"Dryrun: {dryrun}", 1)
|
||||||
if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values():
|
|
||||||
output_server_1_users.append(plex_user)
|
user_mapping = os.getenv("USER_MAPPING")
|
||||||
elif server_1_type == "jellyfin":
|
if user_mapping:
|
||||||
output_server_1_users = {}
|
user_mapping = json.loads(user_mapping.lower())
|
||||||
for jellyfin_user, jellyfin_id in server_1_connection.users.items():
|
logger(f"User Mapping: {user_mapping}", 1)
|
||||||
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
|
|
||||||
output_server_1_users[jellyfin_user] = jellyfin_id
|
library_mapping = os.getenv("LIBRARY_MAPPING")
|
||||||
|
if library_mapping:
|
||||||
if server_2_type == "plex":
|
library_mapping = json.loads(library_mapping)
|
||||||
output_server_2_users = []
|
logger(f"Library Mapping: {library_mapping}", 1)
|
||||||
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():
|
# Create (black/white)lists
|
||||||
output_server_2_users.append(plex_user)
|
logger("Creating (black/white)lists", 1)
|
||||||
elif server_2_type == "jellyfin":
|
blacklist_library = os.getenv("BLACKLIST_LIBRARY", None)
|
||||||
output_server_2_users = {}
|
whitelist_library = os.getenv("WHITELIST_LIBRARY", None)
|
||||||
for jellyfin_user, jellyfin_id in server_2_connection.users.items():
|
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None)
|
||||||
if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values():
|
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None)
|
||||||
output_server_2_users[jellyfin_user] = jellyfin_id
|
blacklist_users = os.getenv("BLACKLIST_USERS", None)
|
||||||
|
whitelist_users = os.getenv("WHITELIST_USERS", None)
|
||||||
if len(output_server_1_users) == 0:
|
|
||||||
raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}")
|
(
|
||||||
|
blacklist_library,
|
||||||
if len(output_server_2_users) == 0:
|
whitelist_library,
|
||||||
raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}")
|
blacklist_library_type,
|
||||||
|
whitelist_library_type,
|
||||||
logger(f"Server 1 users: {output_server_1_users}", 1)
|
blacklist_users,
|
||||||
logger(f"Server 2 users: {output_server_2_users}", 1)
|
whitelist_users,
|
||||||
|
) = setup_black_white_lists(
|
||||||
return output_server_1_users, output_server_2_users
|
blacklist_library,
|
||||||
|
whitelist_library,
|
||||||
def generate_server_connections():
|
blacklist_library_type,
|
||||||
servers = []
|
whitelist_library_type,
|
||||||
|
blacklist_users,
|
||||||
plex_baseurl = os.getenv("PLEX_BASEURL", None)
|
whitelist_users,
|
||||||
plex_token = os.getenv("PLEX_TOKEN", None)
|
library_mapping,
|
||||||
plex_username = os.getenv("PLEX_USERNAME", None)
|
user_mapping,
|
||||||
plex_password = os.getenv("PLEX_PASSWORD", None)
|
)
|
||||||
plex_servername = os.getenv("PLEX_SERVERNAME", None)
|
|
||||||
|
# Create server connections
|
||||||
if plex_baseurl and plex_token:
|
logger("Creating server connections", 1)
|
||||||
plex_baseurl = plex_baseurl.split(",")
|
servers = generate_server_connections()
|
||||||
plex_token = plex_token.split(",")
|
|
||||||
|
for server_1 in servers:
|
||||||
if len(plex_baseurl) != len(plex_token):
|
# If server is the final server in the list, then we are done with the loop
|
||||||
raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries")
|
if server_1 == servers[-1]:
|
||||||
|
break
|
||||||
for i, url in enumerate(plex_baseurl):
|
|
||||||
servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None)))
|
# Start server_2 at the next server in the list
|
||||||
|
for server_2 in servers[servers.index(server_1) + 1 :]:
|
||||||
if plex_username and plex_password and plex_servername:
|
logger(f"Server 1: {server_1[0].capitalize()}: {server_1[1].info()}", 0)
|
||||||
plex_username = plex_username.split(",")
|
logger(f"Server 2: {server_2[0].capitalize()}: {server_2[1].info()}", 0)
|
||||||
plex_password = plex_password.split(",")
|
|
||||||
plex_servername = plex_servername.split(",")
|
# Create users list
|
||||||
|
logger("Creating users list", 1)
|
||||||
if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername):
|
server_1_users, server_2_users = setup_users(
|
||||||
raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries")
|
server_1, server_2, blacklist_users, whitelist_users, user_mapping
|
||||||
|
)
|
||||||
for i, username in enumerate(plex_username):
|
|
||||||
servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip())))
|
logger("Creating watched lists", 1)
|
||||||
|
server_1_watched = get_server_watched(
|
||||||
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
|
server_1,
|
||||||
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
|
server_1_users,
|
||||||
|
blacklist_library,
|
||||||
if jellyfin_baseurl and jellyfin_token:
|
whitelist_library,
|
||||||
jellyfin_baseurl = jellyfin_baseurl.split(",")
|
blacklist_library_type,
|
||||||
jellyfin_token = jellyfin_token.split(",")
|
whitelist_library_type,
|
||||||
|
library_mapping,
|
||||||
if len(jellyfin_baseurl) != len(jellyfin_token):
|
)
|
||||||
raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries")
|
logger("Finished creating watched list server 1", 1)
|
||||||
|
server_2_watched = get_server_watched(
|
||||||
for i, baseurl in enumerate(jellyfin_baseurl):
|
server_2,
|
||||||
servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip())))
|
server_2_users,
|
||||||
|
blacklist_library,
|
||||||
return servers
|
whitelist_library,
|
||||||
|
blacklist_library_type,
|
||||||
def main_loop():
|
whitelist_library_type,
|
||||||
logfile = os.getenv("LOGFILE","log.log")
|
library_mapping,
|
||||||
# Delete logfile if it exists
|
)
|
||||||
if os.path.exists(logfile):
|
logger("Finished creating watched list server 2", 1)
|
||||||
os.remove(logfile)
|
logger(f"Server 1 watched: {server_1_watched}", 3)
|
||||||
|
logger(f"Server 2 watched: {server_2_watched}", 3)
|
||||||
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
|
|
||||||
logger(f"Dryrun: {dryrun}", 1)
|
logger("Cleaning Server 1 Watched", 1)
|
||||||
|
server_1_watched_filtered = cleanup_watched(
|
||||||
user_mapping = os.getenv("USER_MAPPING")
|
server_1_watched, server_2_watched, user_mapping, library_mapping
|
||||||
if user_mapping:
|
)
|
||||||
user_mapping = json.loads(user_mapping.lower())
|
|
||||||
logger(f"User Mapping: {user_mapping}", 1)
|
logger("Cleaning Server 2 Watched", 1)
|
||||||
|
server_2_watched_filtered = cleanup_watched(
|
||||||
library_mapping = os.getenv("LIBRARY_MAPPING")
|
server_2_watched, server_1_watched, user_mapping, library_mapping
|
||||||
if library_mapping:
|
)
|
||||||
library_mapping = json.loads(library_mapping)
|
|
||||||
logger(f"Library Mapping: {library_mapping}", 1)
|
logger(
|
||||||
|
f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}",
|
||||||
# Create (black/white)lists
|
1,
|
||||||
logger("Creating (black/white)lists", 1)
|
)
|
||||||
blacklist_library = os.getenv("BLACKLIST_LIBRARY", None)
|
logger(
|
||||||
whitelist_library = os.getenv("WHITELIST_LIBRARY", None)
|
f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}",
|
||||||
blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None)
|
1,
|
||||||
whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None)
|
)
|
||||||
blacklist_users = os.getenv("BLACKLIST_USERS", None)
|
|
||||||
whitelist_users = os.getenv("WHITELIST_USERS", None)
|
if should_sync_server(server_1[0], server_2[0]):
|
||||||
|
update_server_watched(
|
||||||
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)
|
server_1,
|
||||||
|
server_2_watched_filtered,
|
||||||
# Create server connections
|
user_mapping,
|
||||||
logger("Creating server connections", 1)
|
library_mapping,
|
||||||
servers = generate_server_connections()
|
dryrun,
|
||||||
|
)
|
||||||
for server_1 in servers:
|
|
||||||
# If server is the final server in the list, then we are done with the loop
|
if should_sync_server(server_2[0], server_1[0]):
|
||||||
if server_1 == servers[-1]:
|
update_server_watched(
|
||||||
break
|
server_2,
|
||||||
|
server_1_watched_filtered,
|
||||||
# Start server_2 at the next server in the list
|
user_mapping,
|
||||||
for server_2 in servers[servers.index(server_1) + 1:]:
|
library_mapping,
|
||||||
|
dryrun,
|
||||||
server_1_connection = server_1[1]
|
)
|
||||||
server_2_connection = server_2[1]
|
|
||||||
|
|
||||||
# Create users list
|
def main():
|
||||||
logger("Creating users list", 1)
|
run_only_once = str_to_bool(os.getenv("RUN_ONLY_ONCE", "False"))
|
||||||
server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping)
|
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
|
||||||
|
times = []
|
||||||
logger("Creating watched lists", 1)
|
while True:
|
||||||
args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]
|
try:
|
||||||
, [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]]
|
start = perf_counter()
|
||||||
|
main_loop()
|
||||||
results = future_thread_executor(args)
|
end = perf_counter()
|
||||||
server_1_watched = results[0]
|
times.append(end - start)
|
||||||
server_2_watched = results[1]
|
|
||||||
logger(f"Server 1 watched: {server_1_watched}", 3)
|
if len(times) > 0:
|
||||||
logger(f"Server 2 watched: {server_2_watched}", 3)
|
logger(f"Average time: {sum(times) / len(times)}", 0)
|
||||||
|
|
||||||
# clone watched so it isnt modified in the cleanup function so all duplicates are actually removed
|
if run_only_once:
|
||||||
server_1_watched_filtered = copy.deepcopy(server_1_watched)
|
break
|
||||||
server_2_watched_filtered = copy.deepcopy(server_2_watched)
|
|
||||||
|
logger(f"Looping in {sleep_duration}")
|
||||||
logger("Cleaning Server 1 Watched", 1)
|
sleep(sleep_duration)
|
||||||
server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping)
|
|
||||||
|
except Exception as error:
|
||||||
logger("Cleaning Server 2 Watched", 1)
|
if isinstance(error, list):
|
||||||
server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping)
|
for message in error:
|
||||||
|
logger(message, log_type=2)
|
||||||
logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1)
|
else:
|
||||||
logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1)
|
logger(error, log_type=2)
|
||||||
|
|
||||||
args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun]
|
logger(traceback.format_exc(), 2)
|
||||||
, [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]]
|
|
||||||
|
if run_only_once:
|
||||||
future_thread_executor(args)
|
break
|
||||||
|
|
||||||
def main():
|
logger(f"Retrying in {sleep_duration}", log_type=0)
|
||||||
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
|
sleep(sleep_duration)
|
||||||
|
|
||||||
while(True):
|
except KeyboardInterrupt:
|
||||||
try:
|
if len(times) > 0:
|
||||||
main_loop()
|
logger(f"Average time: {sum(times) / len(times)}", 0)
|
||||||
logger(f"Looping in {sleep_duration}")
|
logger("Exiting", log_type=0)
|
||||||
sleep(sleep_duration)
|
os._exit(0)
|
||||||
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)
|
|
||||||
|
|||||||
950
src/plex.py
950
src/plex.py
@@ -1,312 +1,638 @@
|
|||||||
import re, requests
|
import os, requests, traceback
|
||||||
|
from dotenv import load_dotenv
|
||||||
from plexapi.server import PlexServer
|
from typing import Dict, Union, FrozenSet
|
||||||
from plexapi.myplex import MyPlexAccount
|
|
||||||
|
from urllib3.poolmanager import PoolManager
|
||||||
from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor
|
from math import floor
|
||||||
|
|
||||||
|
from requests.adapters import HTTPAdapter as RequestsHTTPAdapter
|
||||||
# class plex accept base url and token and username and password but default with none
|
|
||||||
class Plex:
|
from plexapi.video import Show, Episode, Movie
|
||||||
def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None, ssl_bypass=False):
|
from plexapi.server import PlexServer
|
||||||
self.baseurl = baseurl
|
from plexapi.myplex import MyPlexAccount
|
||||||
self.token = token
|
|
||||||
self.username = username
|
from src.functions import (
|
||||||
self.password = password
|
logger,
|
||||||
self.servername = servername
|
search_mapping,
|
||||||
self.plex = self.login()
|
future_thread_executor,
|
||||||
self.admin_user = self.plex.myPlexAccount()
|
contains_nested,
|
||||||
self.users = self.get_users()
|
log_marked,
|
||||||
|
str_to_bool,
|
||||||
def login(self):
|
)
|
||||||
try:
|
from src.library import (
|
||||||
if self.baseurl and self.token:
|
check_skip_logic,
|
||||||
# Login via token
|
generate_library_guids_dict,
|
||||||
plex = PlexServer(self.baseurl, self.token)
|
)
|
||||||
elif self.username and self.password and self.servername:
|
|
||||||
# Login via plex account
|
|
||||||
account = MyPlexAccount(self.username, self.password)
|
load_dotenv(override=True)
|
||||||
plex = account.resource(self.servername).connect()
|
|
||||||
else:
|
generate_guids = str_to_bool(os.getenv("GENERATE_GUIDS", "True"))
|
||||||
raise Exception("No complete plex credentials provided")
|
generate_locations = str_to_bool(os.getenv("GENERATE_LOCATIONS", "True"))
|
||||||
|
|
||||||
return plex
|
|
||||||
except Exception as e:
|
# Bypass hostname validation for ssl. Taken from https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
||||||
if self.username or self.password:
|
class HostNameIgnoringAdapter(RequestsHTTPAdapter):
|
||||||
msg = f"Failed to login via plex account {self.username}"
|
def init_poolmanager(self, connections, maxsize, block=..., **pool_kwargs):
|
||||||
logger(f"Plex: Failed to login, {msg}, Error: {e}", 2)
|
self.poolmanager = PoolManager(
|
||||||
else:
|
num_pools=connections,
|
||||||
logger(f"Plex: Failed to login, Error: {e}", 2)
|
maxsize=maxsize,
|
||||||
raise Exception(e)
|
block=block,
|
||||||
|
assert_hostname=False,
|
||||||
|
**pool_kwargs,
|
||||||
def get_users(self):
|
)
|
||||||
try:
|
|
||||||
users = self.plex.myPlexAccount().users()
|
|
||||||
|
def extract_guids_from_item(item: Union[Movie, Show, Episode]) -> Dict[str, str]:
|
||||||
# append self to users
|
# If GENERATE_GUIDS is set to False, then return an empty dict
|
||||||
users.append(self.plex.myPlexAccount())
|
if not generate_guids:
|
||||||
|
return {}
|
||||||
return users
|
|
||||||
except Exception as e:
|
guids: Dict[str, str] = dict(
|
||||||
logger(f"Plex: Failed to get users, Error: {e}", 2)
|
guid.id.split("://")
|
||||||
raise Exception(e)
|
for guid in item.guids
|
||||||
|
if guid.id is not None and len(guid.id.strip()) > 0
|
||||||
def get_user_watched(self, user, user_plex, library):
|
)
|
||||||
try:
|
|
||||||
user_name = user.title.lower()
|
if len(guids) == 0:
|
||||||
user_watched = {}
|
logger(
|
||||||
user_watched[user_name] = {}
|
f"Plex: Failed to get any guids for {item.title}",
|
||||||
|
1,
|
||||||
logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0)
|
)
|
||||||
|
|
||||||
if library.type == "movie":
|
return guids
|
||||||
user_watched[user_name][library.title] = []
|
|
||||||
|
|
||||||
library_videos = user_plex.library.section(library.title)
|
def get_guids(item: Union[Movie, Episode], completed=True):
|
||||||
for video in library_videos.search(unwatched=False):
|
if not item.locations:
|
||||||
movie_guids = {}
|
logger(
|
||||||
for guid in video.guids:
|
f"Plex: {item.title} has no locations",
|
||||||
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
|
1,
|
||||||
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
)
|
||||||
movie_guids[guid_source] = guid_id
|
|
||||||
|
if not item.guids:
|
||||||
movie_guids["title"] = video.title
|
logger(
|
||||||
movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations])
|
f"Plex: {item.title} has no guids",
|
||||||
|
1,
|
||||||
user_watched[user_name][library.title].append(movie_guids)
|
)
|
||||||
|
|
||||||
elif library.type == "show":
|
return {
|
||||||
user_watched[user_name][library.title] = {}
|
"title": item.title,
|
||||||
|
"locations": (
|
||||||
library_videos = user_plex.library.section(library.title)
|
tuple([location.split("/")[-1] for location in item.locations])
|
||||||
for show in library_videos.search(unwatched=False):
|
if generate_locations
|
||||||
show_guids = {}
|
else tuple()
|
||||||
for show_guid in show.guids:
|
),
|
||||||
# Extract after :// from guid.id
|
"status": {
|
||||||
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
"completed": completed,
|
||||||
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
"time": item.viewOffset,
|
||||||
show_guids[show_guid_source] = show_guid_id
|
},
|
||||||
|
} | extract_guids_from_item(
|
||||||
show_guids["title"] = show.title
|
item
|
||||||
show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations])
|
) # Merge the metadata and guid dictionaries
|
||||||
show_guids = frozenset(show_guids.items())
|
|
||||||
|
|
||||||
for season in show.seasons():
|
def get_user_library_watched_show(show, process_episodes, threads=None):
|
||||||
episode_guids = []
|
try:
|
||||||
for episode in season.episodes():
|
show_guids: FrozenSet = frozenset(
|
||||||
if episode.viewCount > 0:
|
(
|
||||||
episode_guids_temp = {}
|
{
|
||||||
for guid in episode.guids:
|
"title": show.title,
|
||||||
# Extract after :// from guid.id
|
"locations": (
|
||||||
guid_source = re.search(r'(.*)://', guid.id).group(1).lower()
|
tuple([location.split("/")[-1] for location in show.locations])
|
||||||
guid_id = re.search(r'://(.*)', guid.id).group(1)
|
if generate_locations
|
||||||
episode_guids_temp[guid_source] = guid_id
|
else tuple()
|
||||||
|
),
|
||||||
episode_guids_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations])
|
}
|
||||||
episode_guids.append(episode_guids_temp)
|
| extract_guids_from_item(show)
|
||||||
|
).items() # Merge the metadata and guid dictionaries
|
||||||
if episode_guids:
|
)
|
||||||
# append show, season, episode
|
|
||||||
if show_guids not in user_watched[user_name][library.title]:
|
episode_guids_args = []
|
||||||
user_watched[user_name][library.title][show_guids] = {}
|
|
||||||
if season.title not in user_watched[user_name][library.title][show_guids]:
|
for episode in process_episodes:
|
||||||
user_watched[user_name][library.title][show_guids][season.title] = {}
|
episode_guids_args.append([get_guids, episode, episode.isWatched])
|
||||||
user_watched[user_name][library.title][show_guids][season.title] = episode_guids
|
|
||||||
|
episode_guids_results = future_thread_executor(
|
||||||
|
episode_guids_args, threads=threads
|
||||||
return user_watched
|
)
|
||||||
except Exception as e:
|
|
||||||
logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2)
|
episode_guids = {}
|
||||||
raise Exception(e)
|
for index, episode in enumerate(process_episodes):
|
||||||
|
if episode.parentIndex not in episode_guids:
|
||||||
|
episode_guids[episode.parentIndex] = []
|
||||||
def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping):
|
episode_guids[episode.parentIndex].append(episode_guids_results[index])
|
||||||
try:
|
|
||||||
# Get all libraries
|
return show_guids, episode_guids
|
||||||
users_watched = {}
|
except Exception:
|
||||||
args = []
|
return {}, {}
|
||||||
|
|
||||||
for user in users:
|
|
||||||
if self.admin_user == user:
|
def get_user_library_watched(user, user_plex, library):
|
||||||
user_plex = self.plex
|
user_name: str = user.username.lower() if user.username else user.title.lower()
|
||||||
else:
|
try:
|
||||||
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
|
logger(
|
||||||
|
f"Plex: Generating watched for {user_name} in library {library.title}",
|
||||||
libraries = user_plex.library.sections()
|
0,
|
||||||
|
)
|
||||||
for library in libraries:
|
|
||||||
library_title = library.title
|
library_videos = user_plex.library.section(library.title)
|
||||||
library_type = library.type
|
|
||||||
|
if library.type == "movie":
|
||||||
skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)
|
watched = []
|
||||||
|
|
||||||
if skip_reason:
|
args = [
|
||||||
logger(f"Plex: Skipping library {library_title} {skip_reason}", 1)
|
[get_guids, video, video.isWatched]
|
||||||
continue
|
for video in library_videos.search(unwatched=False)
|
||||||
|
+ library_videos.search(inProgress=True)
|
||||||
args.append([self.get_user_watched, user, user_plex, library])
|
if video.isWatched or video.viewOffset >= 60000
|
||||||
|
]
|
||||||
for user_watched in future_thread_executor(args):
|
|
||||||
for user, user_watched_temp in user_watched.items():
|
for guid in future_thread_executor(args, threads=len(args)):
|
||||||
if user not in users_watched:
|
logger(f"Plex: Adding {guid['title']} to {user_name} watched list", 3)
|
||||||
users_watched[user] = {}
|
watched.append(guid)
|
||||||
users_watched[user].update(user_watched_temp)
|
elif library.type == "show":
|
||||||
|
watched = {}
|
||||||
return users_watched
|
|
||||||
except Exception as e:
|
# Get all watched shows and partially watched shows
|
||||||
logger(f"Plex: Failed to get watched, Error: {e}", 2)
|
parallel_show_task = []
|
||||||
raise Exception(e)
|
parallel_episodes_task = []
|
||||||
|
|
||||||
|
for show in library_videos.search(unwatched=False) + library_videos.search(
|
||||||
def update_user_watched (self, user, user_plex, library, videos, dryrun):
|
inProgress=True
|
||||||
try:
|
):
|
||||||
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
|
process_episodes = []
|
||||||
videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos)
|
for episode in show.episodes():
|
||||||
logger(f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1)
|
if episode.isWatched or episode.viewOffset >= 60000:
|
||||||
|
process_episodes.append(episode)
|
||||||
library_videos = user_plex.library.section(library)
|
|
||||||
if videos_movies_ids:
|
# Shows with more than 24 episodes has its episodes processed in parallel
|
||||||
for movies_search in library_videos.search(unwatched=True):
|
# Shows with less than 24 episodes has its episodes processed in serial but the shows are processed in parallel
|
||||||
movie_found = False
|
if len(process_episodes) >= 24:
|
||||||
for movie_location in movies_search.locations:
|
parallel_episodes_task.append(
|
||||||
if movie_location.split("/")[-1] in videos_movies_ids["locations"]:
|
[
|
||||||
movie_found = True
|
get_user_library_watched_show,
|
||||||
break
|
show,
|
||||||
|
process_episodes,
|
||||||
if not movie_found:
|
len(process_episodes),
|
||||||
for movie_guid in movies_search.guids:
|
]
|
||||||
movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower()
|
)
|
||||||
movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1)
|
else:
|
||||||
|
parallel_show_task.append(
|
||||||
# If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list
|
[get_user_library_watched_show, show, process_episodes, 1]
|
||||||
if movie_guid_source in videos_movies_ids.keys():
|
)
|
||||||
if movie_guid_id in videos_movies_ids[movie_guid_source]:
|
|
||||||
movie_found = True
|
for show_guids, episode_guids in future_thread_executor(
|
||||||
break
|
parallel_show_task, threads=len(parallel_show_task)
|
||||||
|
) + future_thread_executor(parallel_episodes_task, threads=1):
|
||||||
if movie_found:
|
if show_guids and episode_guids:
|
||||||
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
|
watched[show_guids] = episode_guids
|
||||||
if not dryrun:
|
logger(
|
||||||
logger(f"Marked {msg}", 0)
|
f"Plex: Added {episode_guids} to {user_name} {show_guids} watched list",
|
||||||
movies_search.markWatched()
|
3,
|
||||||
else:
|
)
|
||||||
logger(f"Dryrun {msg}", 0)
|
|
||||||
else:
|
else:
|
||||||
logger(f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 1)
|
watched = None
|
||||||
|
|
||||||
|
logger(f"Plex: Got watched for {user_name} in library {library.title}", 1)
|
||||||
if videos_shows_ids and videos_episodes_ids:
|
logger(f"Plex: {watched}", 3)
|
||||||
for show_search in library_videos.search(unwatched=True):
|
|
||||||
show_found = False
|
return {user_name: {library.title: watched} if watched is not None else {}}
|
||||||
for show_location in show_search.locations:
|
except Exception as e:
|
||||||
if show_location.split("/")[-1] in videos_shows_ids["locations"]:
|
logger(
|
||||||
show_found = True
|
f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}",
|
||||||
break
|
2,
|
||||||
|
)
|
||||||
if not show_found:
|
return {}
|
||||||
for show_guid in show_search.guids:
|
|
||||||
show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower()
|
|
||||||
show_guid_id = re.search(r'://(.*)', show_guid.id).group(1)
|
def find_video(plex_search, video_ids, videos=None):
|
||||||
|
try:
|
||||||
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
|
if not generate_guids and not generate_locations:
|
||||||
if show_guid_source in videos_shows_ids.keys():
|
return False, []
|
||||||
if show_guid_id in videos_shows_ids[show_guid_source]:
|
|
||||||
show_found = True
|
if generate_locations:
|
||||||
break
|
for location in plex_search.locations:
|
||||||
|
if (
|
||||||
if show_found:
|
contains_nested(location.split("/")[-1], video_ids["locations"])
|
||||||
for episode_search in show_search.episodes():
|
is not None
|
||||||
episode_found = False
|
):
|
||||||
|
episode_videos = []
|
||||||
for episode_location in episode_search.locations:
|
if videos:
|
||||||
if episode_location.split("/")[-1] in videos_episodes_ids["locations"]:
|
for show, seasons in videos.items():
|
||||||
episode_found = True
|
show = {k: v for k, v in show}
|
||||||
break
|
if (
|
||||||
|
contains_nested(
|
||||||
if not episode_found:
|
location.split("/")[-1], show["locations"]
|
||||||
for episode_guid in episode_search.guids:
|
)
|
||||||
episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower()
|
is not None
|
||||||
episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1)
|
):
|
||||||
|
for season in seasons.values():
|
||||||
# If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
|
for episode in season:
|
||||||
if episode_guid_source in videos_episodes_ids.keys():
|
episode_videos.append(episode)
|
||||||
if episode_guid_id in videos_episodes_ids[episode_guid_source]:
|
|
||||||
episode_found = True
|
return True, episode_videos
|
||||||
break
|
|
||||||
|
if generate_guids:
|
||||||
if episode_found:
|
for guid in plex_search.guids:
|
||||||
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
guid_source, guid_id = guid.id.split("://")
|
||||||
if not dryrun:
|
|
||||||
logger(f"Marked {msg}", 0)
|
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
|
||||||
episode_search.markWatched()
|
if guid_source in video_ids.keys():
|
||||||
else:
|
if guid_id in video_ids[guid_source]:
|
||||||
logger(f"Dryrun {msg}", 0)
|
episode_videos = []
|
||||||
else:
|
if videos:
|
||||||
logger(f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", 1)
|
for show, seasons in videos.items():
|
||||||
else:
|
show = {k: v for k, v in show}
|
||||||
logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1)
|
if guid_source in show.keys():
|
||||||
|
if guid_id == show[guid_source]:
|
||||||
if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids:
|
for season in seasons.values():
|
||||||
logger(f"Jellyfin: No videos to mark as watched for {user.title} in library {library}", 1)
|
for episode in season:
|
||||||
|
episode_videos.append(episode)
|
||||||
except Exception as e:
|
|
||||||
logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2)
|
return True, episode_videos
|
||||||
raise Exception(e)
|
|
||||||
|
return False, []
|
||||||
|
except Exception:
|
||||||
def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False):
|
return False, []
|
||||||
try:
|
|
||||||
args = []
|
|
||||||
|
def get_video_status(plex_search, video_ids, videos):
|
||||||
for user, libraries in watched_list.items():
|
try:
|
||||||
user_other = None
|
if not generate_guids and not generate_locations:
|
||||||
# If type of user is dict
|
return None
|
||||||
if user_mapping:
|
|
||||||
if user in user_mapping.keys():
|
if generate_locations:
|
||||||
user_other = user_mapping[user]
|
for location in plex_search.locations:
|
||||||
elif user in user_mapping.values():
|
if (
|
||||||
user_other = search_mapping(user_mapping, user)
|
contains_nested(location.split("/")[-1], video_ids["locations"])
|
||||||
|
is not None
|
||||||
for index, value in enumerate(self.users):
|
):
|
||||||
if user.lower() == value.title.lower():
|
for video in videos:
|
||||||
user = self.users[index]
|
if (
|
||||||
break
|
contains_nested(location.split("/")[-1], video["locations"])
|
||||||
elif user_other and user_other.lower() == value.title.lower():
|
is not None
|
||||||
user = self.users[index]
|
):
|
||||||
break
|
return video["status"]
|
||||||
|
|
||||||
if self.admin_user == user:
|
if generate_guids:
|
||||||
user_plex = self.plex
|
for guid in plex_search.guids:
|
||||||
else:
|
guid_source, guid_id = guid.id.split("://")
|
||||||
user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier))
|
|
||||||
|
# If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list
|
||||||
for library, videos in libraries.items():
|
if guid_source in video_ids.keys():
|
||||||
library_other = None
|
if guid_id in video_ids[guid_source]:
|
||||||
if library_mapping:
|
for video in videos:
|
||||||
if library in library_mapping.keys():
|
if guid_source in video.keys():
|
||||||
library_other = library_mapping[library]
|
if guid_id == video[guid_source]:
|
||||||
elif library in library_mapping.values():
|
return video["status"]
|
||||||
library_other = search_mapping(library_mapping, library)
|
|
||||||
|
return None
|
||||||
# if library in plex library list
|
except Exception:
|
||||||
library_list = user_plex.library.sections()
|
return None
|
||||||
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]:
|
def update_user_watched(user, user_plex, library, videos, dryrun):
|
||||||
logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1)
|
try:
|
||||||
library = library_other
|
logger(f"Plex: Updating watched for {user.title} in library {library}", 1)
|
||||||
else:
|
(
|
||||||
logger(f"Plex: Library {library} or {library_other} not found in library list", 2)
|
videos_shows_ids,
|
||||||
continue
|
videos_episodes_ids,
|
||||||
else:
|
videos_movies_ids,
|
||||||
logger(f"Plex: Library {library} not found in library list", 2)
|
) = generate_library_guids_dict(videos)
|
||||||
continue
|
logger(
|
||||||
|
f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
|
||||||
|
1,
|
||||||
args.append([self.update_user_watched, user, user_plex, library, videos, dryrun])
|
)
|
||||||
|
|
||||||
future_thread_executor(args)
|
library_videos = user_plex.library.section(library)
|
||||||
except Exception as e:
|
if videos_movies_ids:
|
||||||
logger(f"Plex: Failed to update watched, Error: {e}", 2)
|
for movies_search in library_videos.search(unwatched=True):
|
||||||
raise Exception(e)
|
video_status = get_video_status(
|
||||||
|
movies_search, videos_movies_ids, videos
|
||||||
|
)
|
||||||
|
if video_status:
|
||||||
|
if video_status["completed"]:
|
||||||
|
msg = f"Plex: {movies_search.title} as watched for {user.title} in {library}"
|
||||||
|
if not dryrun:
|
||||||
|
logger(msg, 5)
|
||||||
|
movies_search.markWatched()
|
||||||
|
else:
|
||||||
|
logger(msg, 6)
|
||||||
|
|
||||||
|
log_marked(user.title, library, movies_search.title, None, None)
|
||||||
|
elif video_status["time"] > 60_000:
|
||||||
|
msg = f"Plex: {movies_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library}"
|
||||||
|
if not dryrun:
|
||||||
|
logger(msg, 5)
|
||||||
|
movies_search.updateTimeline(video_status["time"])
|
||||||
|
else:
|
||||||
|
logger(msg, 6)
|
||||||
|
|
||||||
|
log_marked(
|
||||||
|
user.title,
|
||||||
|
library,
|
||||||
|
movies_search.title,
|
||||||
|
duration=video_status["time"],
|
||||||
|
)
|
||||||
|
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"Plex: {show_search.title} {episode_search.title} as watched for {user.title} in {library}"
|
||||||
|
if not dryrun:
|
||||||
|
logger(msg, 5)
|
||||||
|
episode_search.markWatched()
|
||||||
|
else:
|
||||||
|
logger(msg, 6)
|
||||||
|
|
||||||
|
log_marked(
|
||||||
|
user.title,
|
||||||
|
library,
|
||||||
|
show_search.title,
|
||||||
|
episode_search.title,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = f"Plex: {show_search.title} {episode_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library}"
|
||||||
|
if not dryrun:
|
||||||
|
logger(msg, 5)
|
||||||
|
episode_search.updateTimeline(video_status["time"])
|
||||||
|
else:
|
||||||
|
logger(msg, 6)
|
||||||
|
|
||||||
|
log_marked(
|
||||||
|
user.title,
|
||||||
|
library,
|
||||||
|
show_search.title,
|
||||||
|
episode_search.title,
|
||||||
|
video_status["time"],
|
||||||
|
)
|
||||||
|
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 info(self) -> str:
|
||||||
|
return f"{self.plex.friendlyName}: {self.plex.version}"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
user_watched = get_user_library_watched(user, user_plex, library)
|
||||||
|
|
||||||
|
for user_watched, user_watched_temp in user_watched.items():
|
||||||
|
if user_watched not in users_watched:
|
||||||
|
users_watched[user_watched] = {}
|
||||||
|
users_watched[user_watched].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
91
src/users.py
Normal 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
|
||||||
317
src/watched.py
Normal file
317
src/watched.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
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):
|
||||||
|
# Ensure that the input is a list of dictionaries
|
||||||
|
if not all(isinstance(d, dict) for d in dicts):
|
||||||
|
raise ValueError("Input must be a list of dictionaries")
|
||||||
|
|
||||||
|
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]:
|
||||||
|
# Filter the episode_watched_list_2_keys_dict dictionary to handle cases
|
||||||
|
# where episode location names are not unique such as S01E01.mkv
|
||||||
|
filtered_episode_watched_list_2_keys_dict = (
|
||||||
|
filter_episode_watched_list_2_keys_dict(
|
||||||
|
episode_watched_list_2_keys_dict, show_key_dict, season
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for episode in watched_list_1[user_1][library_1][show_key_1][
|
||||||
|
season
|
||||||
|
]:
|
||||||
|
episode_index = get_episode_index_in_dict(
|
||||||
|
episode, filtered_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 filter_episode_watched_list_2_keys_dict(
|
||||||
|
episode_watched_list_2_keys_dict, show_key_dict, season
|
||||||
|
):
|
||||||
|
# If the episode_watched_list_2_keys_dict dictionary is empty, missing season or show then return an empty dictionary
|
||||||
|
if (
|
||||||
|
len(episode_watched_list_2_keys_dict) == 0
|
||||||
|
or "season" not in episode_watched_list_2_keys_dict.keys()
|
||||||
|
or "show" not in episode_watched_list_2_keys_dict.keys()
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Filter the episode_watched_list_2_keys_dict dictionary to only include values for the correct show and season
|
||||||
|
filtered_episode_watched_list_2_keys_dict = {}
|
||||||
|
show_indecies = []
|
||||||
|
season_indecies = []
|
||||||
|
|
||||||
|
# Iterate through episode_watched_list_2_keys_dict["season"] and find the indecies that match season
|
||||||
|
for season_index, season_value in enumerate(
|
||||||
|
episode_watched_list_2_keys_dict.get("season")
|
||||||
|
):
|
||||||
|
if season_value == season:
|
||||||
|
season_indecies.append(season_index)
|
||||||
|
|
||||||
|
# Iterate through episode_watched_list_2_keys_dict["show"] and find the indecies that match show_key_dict
|
||||||
|
for show_index, show_value in enumerate(episode_watched_list_2_keys_dict["show"]):
|
||||||
|
# Iterate through the keys and values of the show_value dictionary and check if they match show_key_dict
|
||||||
|
for show_key, show_key_value in show_value.items():
|
||||||
|
if show_key == "locations":
|
||||||
|
# Iterate through the locations in the show_value dictionary
|
||||||
|
for location in show_key_value:
|
||||||
|
# If the location is in the episode_watched_list_2_keys_dict dictionary, return index of the key
|
||||||
|
if (
|
||||||
|
contains_nested(location, show_key_dict["locations"])
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
show_indecies.append(show_index)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if show_key in show_key_dict.keys():
|
||||||
|
if show_key_value == show_key_dict[show_key]:
|
||||||
|
show_indecies.append(show_index)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find the intersection of the show_indecies and season_indecies lists
|
||||||
|
indecies = list(set(show_indecies) & set(season_indecies))
|
||||||
|
|
||||||
|
# If there are no indecies that match the show and season, return an empty dictionary
|
||||||
|
if len(indecies) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Create a copy of the dictionary with indecies that match the show and season and none that don't
|
||||||
|
for key, value in episode_watched_list_2_keys_dict.items():
|
||||||
|
if key not in filtered_episode_watched_list_2_keys_dict:
|
||||||
|
filtered_episode_watched_list_2_keys_dict[key] = []
|
||||||
|
|
||||||
|
for index, _ in enumerate(value):
|
||||||
|
if index in indecies:
|
||||||
|
filtered_episode_watched_list_2_keys_dict[key].append(value[index])
|
||||||
|
else:
|
||||||
|
filtered_episode_watched_list_2_keys_dict[key].append(None)
|
||||||
|
|
||||||
|
return filtered_episode_watched_list_2_keys_dict
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
96
test/ci1.env
Normal file
96
test/ci1.env
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 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 = "True"
|
||||||
|
|
||||||
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
|
DEBUG_LEVEL = "debug"
|
||||||
|
|
||||||
|
## If set to true then the script will only run once and then exit
|
||||||
|
RUN_ONLY_ONCE = "True"
|
||||||
|
|
||||||
|
## How often to run the script in seconds
|
||||||
|
SLEEP_DURATION = 10
|
||||||
|
|
||||||
|
## Log file where all output will be written to
|
||||||
|
LOG_FILE = "log.log"
|
||||||
|
|
||||||
|
## Mark file where all shows/movies that have been marked as played will be written to
|
||||||
|
MARK_FILE = "mark.log"
|
||||||
|
|
||||||
|
## Timeout for requests for jellyfin
|
||||||
|
REQUEST_TIMEOUT = 300
|
||||||
|
|
||||||
|
## Max threads for processing
|
||||||
|
MAX_THREADS = 2
|
||||||
|
|
||||||
|
## Generate guids
|
||||||
|
## Generating guids is a slow process, so this is a way to speed up the process
|
||||||
|
# by using the location only, useful when using same files on multiple servers
|
||||||
|
GENERATE_GUIDS = "False"
|
||||||
|
|
||||||
|
## Generate locations
|
||||||
|
## Generating locations is a slow process, so this is a way to speed up the process
|
||||||
|
## by using the guid only, useful when using different files on multiple servers
|
||||||
|
GENERATE_LOCATIONS = "True"
|
||||||
|
|
||||||
|
## Map usernames between servers in the event that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
USER_MAPPING = {"JellyUser":"jellyplex_watched"}
|
||||||
|
|
||||||
|
## Map libraries between servers in the even that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
LIBRARY_MAPPING = { "Shows": "TV Shows" }
|
||||||
|
|
||||||
|
|
||||||
|
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#BLACKLIST_LIBRARY = ""
|
||||||
|
#WHITELIST_LIBRARY = "Movies"
|
||||||
|
#BLACKLIST_LIBRARY_TYPE = "Series"
|
||||||
|
#WHITELIST_LIBRARY_TYPE = "Movies, movie"
|
||||||
|
#BLACKLIST_USERS = ""
|
||||||
|
WHITELIST_USERS = "jellyplex_watched"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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 seperated list for multiple servers
|
||||||
|
PLEX_BASEURL = "https://localhost:32400"
|
||||||
|
|
||||||
|
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
|
||||||
|
|
||||||
|
## If not using plex token then use username and password of the server admin along with the servername
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#PLEX_USERNAME = "PlexUser, PlexUser2"
|
||||||
|
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
|
||||||
|
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
|
||||||
|
|
||||||
|
## Skip hostname validation for ssl certificates.
|
||||||
|
## Set to True if running into ssl certificate errors
|
||||||
|
SSL_BYPASS = "True"
|
||||||
|
|
||||||
|
## 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 seperated list for multiple servers
|
||||||
|
JELLYFIN_BASEURL = "http://localhost:8096"
|
||||||
|
|
||||||
|
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
|
||||||
96
test/ci2.env
Normal file
96
test/ci2.env
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 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 = "True"
|
||||||
|
|
||||||
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
|
DEBUG_LEVEL = "debug"
|
||||||
|
|
||||||
|
## If set to true then the script will only run once and then exit
|
||||||
|
RUN_ONLY_ONCE = "True"
|
||||||
|
|
||||||
|
## How often to run the script in seconds
|
||||||
|
SLEEP_DURATION = 10
|
||||||
|
|
||||||
|
## Log file where all output will be written to
|
||||||
|
LOG_FILE = "log.log"
|
||||||
|
|
||||||
|
## Mark file where all shows/movies that have been marked as played will be written to
|
||||||
|
MARK_FILE = "mark.log"
|
||||||
|
|
||||||
|
## Timeout for requests for jellyfin
|
||||||
|
REQUEST_TIMEOUT = 300
|
||||||
|
|
||||||
|
## Max threads for processing
|
||||||
|
MAX_THREADS = 2
|
||||||
|
|
||||||
|
## Generate guids
|
||||||
|
## Generating guids is a slow process, so this is a way to speed up the process
|
||||||
|
# by using the location only, useful when using same files on multiple servers
|
||||||
|
GENERATE_GUIDS = "True"
|
||||||
|
|
||||||
|
## Generate locations
|
||||||
|
## Generating locations is a slow process, so this is a way to speed up the process
|
||||||
|
## by using the guid only, useful when using different files on multiple servers
|
||||||
|
GENERATE_LOCATIONS = "False"
|
||||||
|
|
||||||
|
## Map usernames between servers in the event that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
USER_MAPPING = {"JellyUser":"jellyplex_watched"}
|
||||||
|
|
||||||
|
## Map libraries between servers in the even that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
LIBRARY_MAPPING = { "Shows": "TV Shows" }
|
||||||
|
|
||||||
|
|
||||||
|
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#BLACKLIST_LIBRARY = ""
|
||||||
|
#WHITELIST_LIBRARY = "Movies"
|
||||||
|
#BLACKLIST_LIBRARY_TYPE = "Series"
|
||||||
|
#WHITELIST_LIBRARY_TYPE = "Movies, movie"
|
||||||
|
#BLACKLIST_USERS = ""
|
||||||
|
WHITELIST_USERS = "jellyplex_watched"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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 seperated list for multiple servers
|
||||||
|
PLEX_BASEURL = "https://localhost:32400"
|
||||||
|
|
||||||
|
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
|
||||||
|
|
||||||
|
## If not using plex token then use username and password of the server admin along with the servername
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#PLEX_USERNAME = "PlexUser, PlexUser2"
|
||||||
|
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
|
||||||
|
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
|
||||||
|
|
||||||
|
## Skip hostname validation for ssl certificates.
|
||||||
|
## Set to True if running into ssl certificate errors
|
||||||
|
SSL_BYPASS = "True"
|
||||||
|
|
||||||
|
## 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 seperated list for multiple servers
|
||||||
|
JELLYFIN_BASEURL = "http://localhost:8096"
|
||||||
|
|
||||||
|
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
|
||||||
96
test/ci3.env
Normal file
96
test/ci3.env
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Global Settings
|
||||||
|
|
||||||
|
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||||
|
DRYRUN = "False"
|
||||||
|
|
||||||
|
## Additional logging information
|
||||||
|
DEBUG = "True"
|
||||||
|
|
||||||
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
|
DEBUG_LEVEL = "debug"
|
||||||
|
|
||||||
|
## If set to true then the script will only run once and then exit
|
||||||
|
RUN_ONLY_ONCE = "True"
|
||||||
|
|
||||||
|
## How often to run the script in seconds
|
||||||
|
SLEEP_DURATION = 10
|
||||||
|
|
||||||
|
## Log file where all output will be written to
|
||||||
|
LOG_FILE = "log.log"
|
||||||
|
|
||||||
|
## Mark file where all shows/movies that have been marked as played will be written to
|
||||||
|
MARK_FILE = "mark.log"
|
||||||
|
|
||||||
|
## Timeout for requests for jellyfin
|
||||||
|
REQUEST_TIMEOUT = 300
|
||||||
|
|
||||||
|
## Max threads for processing
|
||||||
|
MAX_THREADS = 2
|
||||||
|
|
||||||
|
## Generate guids
|
||||||
|
## Generating guids is a slow process, so this is a way to speed up the process
|
||||||
|
# by using the location only, useful when using same files on multiple servers
|
||||||
|
GENERATE_GUIDS = "True"
|
||||||
|
|
||||||
|
## Generate locations
|
||||||
|
## Generating locations is a slow process, so this is a way to speed up the process
|
||||||
|
## by using the guid only, useful when using different files on multiple servers
|
||||||
|
GENERATE_LOCATIONS = "True"
|
||||||
|
|
||||||
|
## Map usernames between servers in the event that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
USER_MAPPING = {"JellyUser":"jellyplex_watched"}
|
||||||
|
|
||||||
|
## Map libraries between servers in the even that they are different, order does not matter
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
LIBRARY_MAPPING = { "Shows": "TV Shows" }
|
||||||
|
|
||||||
|
|
||||||
|
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#BLACKLIST_LIBRARY = ""
|
||||||
|
#WHITELIST_LIBRARY = "Movies"
|
||||||
|
#BLACKLIST_LIBRARY_TYPE = "Series"
|
||||||
|
#WHITELIST_LIBRARY_TYPE = "Movies, movie"
|
||||||
|
#BLACKLIST_USERS = ""
|
||||||
|
WHITELIST_USERS = "jellyplex_watched"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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 seperated list for multiple servers
|
||||||
|
PLEX_BASEURL = "https://localhost:32400"
|
||||||
|
|
||||||
|
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
|
||||||
|
|
||||||
|
## If not using plex token then use username and password of the server admin along with the servername
|
||||||
|
## Comma seperated for multiple options
|
||||||
|
#PLEX_USERNAME = "PlexUser, PlexUser2"
|
||||||
|
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
|
||||||
|
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
|
||||||
|
|
||||||
|
## Skip hostname validation for ssl certificates.
|
||||||
|
## Set to True if running into ssl certificate errors
|
||||||
|
SSL_BYPASS = "True"
|
||||||
|
|
||||||
|
## 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 seperated list for multiple servers
|
||||||
|
JELLYFIN_BASEURL = "http://localhost:8096"
|
||||||
|
|
||||||
|
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
||||||
|
## Comma seperated list for multiple servers
|
||||||
|
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
|
||||||
@@ -1 +1 @@
|
|||||||
pytest
|
pytest==7.3.0
|
||||||
|
|||||||
78
test/test_black_white.py
Normal file
78
test/test_black_white.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# getting the name of the directory
|
||||||
|
# where the this file is present.
|
||||||
|
current = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
# Getting the parent directory name
|
||||||
|
# where the current directory is present.
|
||||||
|
parent = os.path.dirname(current)
|
||||||
|
|
||||||
|
# adding the parent directory to
|
||||||
|
# the sys.path.
|
||||||
|
sys.path.append(parent)
|
||||||
|
|
||||||
|
from src.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"]
|
||||||
327
test/test_library.py
Normal file
327
test/test_library.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
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],
|
||||||
|
"season": ["Season 1"],
|
||||||
|
"show": [
|
||||||
|
{
|
||||||
|
"imdb": "tt3581920",
|
||||||
|
"locations": ("The Last of Us",),
|
||||||
|
"title": "The Last of Us",
|
||||||
|
"tmdb": "100088",
|
||||||
|
"tvdb": "392256",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
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
78
test/test_main.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# getting the name of the directory
|
||||||
|
# where the this file is present.
|
||||||
|
current = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
# Getting the parent directory name
|
||||||
|
# where the current directory is present.
|
||||||
|
parent = os.path.dirname(current)
|
||||||
|
|
||||||
|
# adding the parent directory to
|
||||||
|
# the sys.path.
|
||||||
|
sys.path.append(parent)
|
||||||
|
|
||||||
|
from src.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"]
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# getting the name of the directory
|
|
||||||
# where the this file is present.
|
|
||||||
current = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
|
|
||||||
# Getting the parent directory name
|
|
||||||
# where the current directory is present.
|
|
||||||
parent = os.path.dirname(current)
|
|
||||||
|
|
||||||
# adding the parent directory to
|
|
||||||
# the sys.path.
|
|
||||||
sys.path.append(parent)
|
|
||||||
|
|
||||||
from src.main import setup_black_white_lists
|
|
||||||
|
|
||||||
def test_setup_black_white_lists():
|
|
||||||
# Simple
|
|
||||||
blacklist_library = 'library1, library2'
|
|
||||||
whitelist_library = 'library1, library2'
|
|
||||||
blacklist_library_type = 'library_type1, library_type2'
|
|
||||||
whitelist_library_type = 'library_type1, library_type2'
|
|
||||||
blacklist_users = 'user1, user2'
|
|
||||||
whitelist_users = 'user1, user2'
|
|
||||||
|
|
||||||
results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users)
|
|
||||||
|
|
||||||
assert results_blacklist_library == ['library1', 'library2']
|
|
||||||
assert return_whitelist_library == ['library1', 'library2']
|
|
||||||
assert return_blacklist_library_type == ['library_type1', 'library_type2']
|
|
||||||
assert return_whitelist_library_type == ['library_type1', 'library_type2']
|
|
||||||
assert return_blacklist_users == ['user1', 'user2']
|
|
||||||
assert return_whitelist_users == ['user1', 'user2']
|
|
||||||
|
|
||||||
# Library Mapping and user mapping
|
|
||||||
library_mapping = { "library1": "library3" }
|
|
||||||
user_mapping = { "user1": "user3" }
|
|
||||||
|
|
||||||
results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users, library_mapping, user_mapping)
|
|
||||||
|
|
||||||
assert results_blacklist_library == ['library1', 'library2', 'library3']
|
|
||||||
assert return_whitelist_library == ['library1', 'library2', 'library3']
|
|
||||||
assert return_blacklist_library_type == ['library_type1', 'library_type2']
|
|
||||||
assert return_whitelist_library_type == ['library_type1', 'library_type2']
|
|
||||||
assert return_blacklist_users == ['user1', 'user2', 'user3']
|
|
||||||
assert return_whitelist_users == ['user1', 'user2', 'user3']
|
|
||||||
@@ -1,176 +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
39
test/test_users.py
Normal 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"}
|
||||||
684
test/test_watched.py
Normal file
684
test/test_watched.py
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
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(
|
||||||
|
{
|
||||||
|
("locations", ("Doctor Who (2005) {tvdb-78804} {imdb-tt0436992}",)),
|
||||||
|
("imdb", "tt0436992"),
|
||||||
|
("tmdb", "57243"),
|
||||||
|
("tvdb", "78804"),
|
||||||
|
("title", "Doctor Who (2005)"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"imdb": "tt0563001",
|
||||||
|
"tmdb": "968589",
|
||||||
|
"tvdb": "295296",
|
||||||
|
"title": "The Unquiet Dead",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt0562985",
|
||||||
|
"tmdb": "968590",
|
||||||
|
"tvdb": "295297",
|
||||||
|
"title": "Aliens of London (1)",
|
||||||
|
"locations": ("S01E04.mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt0563003",
|
||||||
|
"tmdb": "968592",
|
||||||
|
"tvdb": "295298",
|
||||||
|
"title": "World War Three (2)",
|
||||||
|
"locations": ("S01E05.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("title", "Monarch: Legacy of Monsters"),
|
||||||
|
("imdb", "tt17220216"),
|
||||||
|
("tvdb", "422598"),
|
||||||
|
("tmdb", "202411"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"imdb": "tt21255044",
|
||||||
|
"tmdb": "4661246",
|
||||||
|
"tvdb": "10009418",
|
||||||
|
"title": "Secrets and Lies",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt21255050",
|
||||||
|
"tmdb": "4712059",
|
||||||
|
"tvdb": "10009419",
|
||||||
|
"title": "Parallels and Interiors",
|
||||||
|
"locations": ("S01E04.mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt23787572",
|
||||||
|
"tmdb": "4712061",
|
||||||
|
"tvdb": "10009420",
|
||||||
|
"title": "The Way Out",
|
||||||
|
"locations": ("S01E05.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("tmdb", "125928"),
|
||||||
|
("imdb", "tt14681924"),
|
||||||
|
("tvdb", "403172"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("My Adventures with Superman {tvdb-403172} {imdb-tt14681924}",),
|
||||||
|
),
|
||||||
|
("title", "My Adventures with Superman"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"imdb": "tt15699926",
|
||||||
|
"tmdb": "3070048",
|
||||||
|
"tvdb": "8438181",
|
||||||
|
"title": "Adventures of a Normal Man (1)",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt20413322",
|
||||||
|
"tmdb": "4568681",
|
||||||
|
"tvdb": "9829910",
|
||||||
|
"title": "Adventures of a Normal Man (2)",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt20413328",
|
||||||
|
"tmdb": "4497012",
|
||||||
|
"tvdb": "9870382",
|
||||||
|
"title": "My Interview with Superman",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tv_shows_watched_list_2 = {
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("locations", ("Doctor Who (2005) {tvdb-78804} {imdb-tt0436992}",)),
|
||||||
|
("imdb", "tt0436992"),
|
||||||
|
("tmdb", "57243"),
|
||||||
|
("title", "Doctor Who"),
|
||||||
|
("tvdb", "78804"),
|
||||||
|
("tvrage", "3332"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"tvdb": "295294",
|
||||||
|
"imdb": "tt0562992",
|
||||||
|
"title": "Rose",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "295295",
|
||||||
|
"imdb": "tt0562997",
|
||||||
|
"title": "The End of the World",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": False, "time": 300670},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "295298",
|
||||||
|
"imdb": "tt0563003",
|
||||||
|
"title": "World War Three (2)",
|
||||||
|
"locations": ("S01E05.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("title", "Monarch: Legacy of Monsters"),
|
||||||
|
("imdb", "tt17220216"),
|
||||||
|
("tvdb", "422598"),
|
||||||
|
("tmdb", "202411"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"tvdb": "9959300",
|
||||||
|
"imdb": "tt20412166",
|
||||||
|
"title": "Aftermath",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "10009417",
|
||||||
|
"imdb": "tt22866594",
|
||||||
|
"title": "Departure",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": False, "time": 300741},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "10009420",
|
||||||
|
"imdb": "tt23787572",
|
||||||
|
"title": "The Way Out",
|
||||||
|
"locations": ("S01E05.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("tmdb", "125928"),
|
||||||
|
("imdb", "tt14681924"),
|
||||||
|
("tvdb", "403172"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("My Adventures with Superman {tvdb-403172} {imdb-tt14681924}",),
|
||||||
|
),
|
||||||
|
("title", "My Adventures with Superman"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"tvdb": "8438181",
|
||||||
|
"imdb": "tt15699926",
|
||||||
|
"title": "Adventures of a Normal Man (1)",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "9829910",
|
||||||
|
"imdb": "tt20413322",
|
||||||
|
"title": "Adventures of a Normal Man (2)",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "9870382",
|
||||||
|
"imdb": "tt20413328",
|
||||||
|
"title": "My Interview with Superman",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_tv_show_watched_list_1 = {
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("locations", ("Doctor Who (2005) {tvdb-78804} {imdb-tt0436992}",)),
|
||||||
|
("imdb", "tt0436992"),
|
||||||
|
("tmdb", "57243"),
|
||||||
|
("tvdb", "78804"),
|
||||||
|
("title", "Doctor Who (2005)"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"imdb": "tt0563001",
|
||||||
|
"tmdb": "968589",
|
||||||
|
"tvdb": "295296",
|
||||||
|
"title": "The Unquiet Dead",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt0562985",
|
||||||
|
"tmdb": "968590",
|
||||||
|
"tvdb": "295297",
|
||||||
|
"title": "Aliens of London (1)",
|
||||||
|
"locations": ("S01E04.mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("title", "Monarch: Legacy of Monsters"),
|
||||||
|
("imdb", "tt17220216"),
|
||||||
|
("tvdb", "422598"),
|
||||||
|
("tmdb", "202411"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"imdb": "tt21255044",
|
||||||
|
"tmdb": "4661246",
|
||||||
|
"tvdb": "10009418",
|
||||||
|
"title": "Secrets and Lies",
|
||||||
|
"locations": ("S01E03.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt21255050",
|
||||||
|
"tmdb": "4712059",
|
||||||
|
"tvdb": "10009419",
|
||||||
|
"title": "Parallels and Interiors",
|
||||||
|
"locations": ("S01E04.mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_tv_show_watched_list_2 = {
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("locations", ("Doctor Who (2005) {tvdb-78804} {imdb-tt0436992}",)),
|
||||||
|
("imdb", "tt0436992"),
|
||||||
|
("tmdb", "57243"),
|
||||||
|
("title", "Doctor Who"),
|
||||||
|
("tvdb", "78804"),
|
||||||
|
("tvrage", "3332"),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"tvdb": "295294",
|
||||||
|
"imdb": "tt0562992",
|
||||||
|
"title": "Rose",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "295295",
|
||||||
|
"imdb": "tt0562997",
|
||||||
|
"title": "The End of the World",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": False, "time": 300670},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
frozenset(
|
||||||
|
{
|
||||||
|
("title", "Monarch: Legacy of Monsters"),
|
||||||
|
("imdb", "tt17220216"),
|
||||||
|
("tvdb", "422598"),
|
||||||
|
("tmdb", "202411"),
|
||||||
|
(
|
||||||
|
"locations",
|
||||||
|
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
): {
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"tvdb": "9959300",
|
||||||
|
"imdb": "tt20412166",
|
||||||
|
"title": "Aftermath",
|
||||||
|
"locations": ("S01E01.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tvdb": "10009417",
|
||||||
|
"imdb": "tt22866594",
|
||||||
|
"title": "Departure",
|
||||||
|
"locations": ("S01E02.mkv",),
|
||||||
|
"status": {"completed": False, "time": 300741},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
movies_watched_list_1 = [
|
||||||
|
{
|
||||||
|
"imdb": "tt1254207",
|
||||||
|
"tmdb": "10378",
|
||||||
|
"tvdb": "12352",
|
||||||
|
"title": "Big Buck Bunny",
|
||||||
|
"locations": ("Big Buck Bunny.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt16431870",
|
||||||
|
"tmdb": "1029575",
|
||||||
|
"tvdb": "351194",
|
||||||
|
"title": "The Family Plan",
|
||||||
|
"locations": ("The Family Plan (2023).mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt5537002",
|
||||||
|
"tmdb": "466420",
|
||||||
|
"tvdb": "135852",
|
||||||
|
"title": "Killers of the Flower Moon",
|
||||||
|
"locations": ("Killers of the Flower Moon (2023).mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
movies_watched_list_2 = [
|
||||||
|
{
|
||||||
|
"imdb": "tt16431870",
|
||||||
|
"tmdb": "1029575",
|
||||||
|
"title": "The Family Plan",
|
||||||
|
"locations": ("The Family Plan (2023).mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt4589218",
|
||||||
|
"tmdb": "507089",
|
||||||
|
"title": "Five Nights at Freddy's",
|
||||||
|
"locations": ("Five Nights at Freddy's (2023).mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt10545296",
|
||||||
|
"tmdb": "695721",
|
||||||
|
"tmdbcollection": "131635",
|
||||||
|
"title": "The Hunger Games: The Ballad of Songbirds & Snakes",
|
||||||
|
"locations": ("The Hunger Games The Ballad of Songbirds & Snakes (2023).mkv",),
|
||||||
|
"status": {"completed": False, "time": 301215},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
expected_movie_watched_list_1 = [
|
||||||
|
{
|
||||||
|
"imdb": "tt1254207",
|
||||||
|
"tmdb": "10378",
|
||||||
|
"tvdb": "12352",
|
||||||
|
"title": "Big Buck Bunny",
|
||||||
|
"locations": ("Big Buck Bunny.mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt5537002",
|
||||||
|
"tmdb": "466420",
|
||||||
|
"tvdb": "135852",
|
||||||
|
"title": "Killers of the Flower Moon",
|
||||||
|
"locations": ("Killers of the Flower Moon (2023).mkv",),
|
||||||
|
"status": {"completed": False, "time": 240000},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_movie_watched_list_2 = [
|
||||||
|
{
|
||||||
|
"imdb": "tt4589218",
|
||||||
|
"tmdb": "507089",
|
||||||
|
"title": "Five Nights at Freddy's",
|
||||||
|
"locations": ("Five Nights at Freddy's (2023).mkv",),
|
||||||
|
"status": {"completed": True, "time": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"imdb": "tt10545296",
|
||||||
|
"tmdb": "695721",
|
||||||
|
"tmdbcollection": "131635",
|
||||||
|
"title": "The Hunger Games: The Ballad of Songbirds & Snakes",
|
||||||
|
"locations": ("The Hunger Games The Ballad of Songbirds & Snakes (2023).mkv",),
|
||||||
|
"status": {"completed": False, "time": 301215},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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
|
||||||
76
test/validate_ci_marklog.py
Normal file
76
test/validate_ci_marklog.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Check the mark.log file that is generated by the CI to make sure it contains the expected values
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def read_marklog():
|
||||||
|
marklog = os.path.join(os.getcwd(), "mark.log")
|
||||||
|
with open(marklog, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def check_marklog(lines, expected_values):
|
||||||
|
try:
|
||||||
|
# Check to make sure the marklog contains all the expected values and nothing else
|
||||||
|
found_values = []
|
||||||
|
for line in lines:
|
||||||
|
# Remove the newline character
|
||||||
|
line = line.strip()
|
||||||
|
if line not in expected_values:
|
||||||
|
raise Exception("Line not found in marklog: " + line)
|
||||||
|
|
||||||
|
found_values.append(line)
|
||||||
|
|
||||||
|
# Check to make sure the marklog contains the same number of values as the expected values
|
||||||
|
if len(found_values) != len(expected_values):
|
||||||
|
raise Exception(
|
||||||
|
"Marklog did not contain the same number of values as the expected values, found "
|
||||||
|
+ str(len(found_values))
|
||||||
|
+ " values, expected "
|
||||||
|
+ str(len(expected_values))
|
||||||
|
+ " values"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the two lists contain the same values
|
||||||
|
if sorted(found_values) != sorted(expected_values):
|
||||||
|
raise Exception(
|
||||||
|
"Marklog did not contain the same values as the expected values, found:\n"
|
||||||
|
+ "\n".join(sorted(found_values))
|
||||||
|
+ "\n\nExpected:\n"
|
||||||
|
+ "\n".join(sorted(expected_values))
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
expected_values = [
|
||||||
|
"jellyplex_watched/Movies/Five Nights at Freddy's",
|
||||||
|
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
|
||||||
|
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
|
||||||
|
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
|
||||||
|
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
|
||||||
|
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
|
||||||
|
"JellyUser/Movies/Big Buck Bunny",
|
||||||
|
"JellyUser/Shows/Doctor Who/The Unquiet Dead",
|
||||||
|
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Triple the expected values because the CI runs three times
|
||||||
|
expected_values = expected_values * 3
|
||||||
|
|
||||||
|
lines = read_marklog()
|
||||||
|
if not check_marklog(lines, expected_values):
|
||||||
|
print("Failed to validate marklog")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print("Successfully validated marklog")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user