59 Commits

Author SHA1 Message Date
Luigi311
4771f736b0 Merge pull request #223 from luigi311/update
Update python and PlexAPI
2025-02-18 17:23:47 -07:00
Luis Garcia
8d7436579e CI: Update plex token 2025-02-18 17:10:54 -07:00
Luis Garcia
43e1df98b1 Update to Python 3.13 2025-02-18 16:43:22 -07:00
Luis Garcia
3017030f52 Requirements: Update PlexAPI 4.16.1 2025-02-18 16:42:35 -07:00
Luis Garcia
348a0b8226 Dont show error on extract show/episode/movies output dict
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-11-09 13:38:59 -07:00
Luis Garcia
4e60c08120 Reduce sample max_threads to 2
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-11-09 12:53:13 -07:00
Luis Garcia
10b58379cd Test: Validate for GUIDS and Locations
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-11-09 12:17:33 -07:00
Luis Garcia
fa9201b20f Update requirements
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-11-09 10:44:31 -07:00
Luigi311
86f72997b4 Merge pull request #205 from luigi311/simplify_watched
Simplify get watched process
2024-10-27 18:13:31 -06:00
Luis Garcia
62d0319aad Remove unused
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luis Garcia
a096a09eb7 CI: Fix Validation. Print marklog on failed validation
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luis Garcia
7294241fed Add server type and name to marklog
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luis Garcia
a5995d3999 CI: More verbose
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luis Garcia
30f31b2f3f Remove unused combine_watched_dicts
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luis Garcia
bc09c873e9 Simplify get watched process. Only get watched for syncing libraries
Signed-off-by: Luis Garcia <git@luigi311.com>
2024-10-27 18:05:27 -06:00
Luigi311
8428be9dda Create FUNDING.yml 2024-10-27 09:05:06 -06:00
Luigi311
6a45ad18f9 Merge pull request #202 from luigi311/python-12
Update to python 12
2024-10-07 23:10:00 -06:00
Luigi311
023b638729 CI: Pin to python 3.12 2024-10-08 04:59:44 +00:00
Luigi311
7e13c14636 Merge pull request #195 from luigi311/fix_user
Fix user
2024-09-13 17:01:45 -06:00
Luis Garcia
0c218fa9dd Entrypoint: Alpine fix overlapping PGID issue 2024-09-13 16:24:58 -06:00
Luis Garcia
b3b0ccac73 Docker: Fix alpine 2024-09-13 10:12:18 -06:00
Luis Garcia
fa0134551f Entrypoint: Check root user, check addgroup/adduser command exists 2024-09-13 09:56:34 -06:00
Luis Garcia
34d62c9021 Docker: Add dos2unix 2024-09-08 01:28:11 -06:00
Luis Garcia
920bbbb3be Move more functions out of main 2024-09-05 16:21:06 -06:00
Luis Garcia
762e5f10da Fix typos in variables 2024-08-28 17:14:45 -06:00
Luis Garcia
27797cb361 Formatting 2024-08-28 17:14:37 -06:00
Luis Garcia
066f9d1f66 Docker Compose: Use env_file for most variables 2024-08-28 16:46:19 -06:00
Luis Garcia
acf7c2cdf2 Entrypoint: Fix typos 2024-08-28 16:45:58 -06:00
Luis Garcia
469857a31a Dockerfiles: Remove most env 2024-08-28 16:45:37 -06:00
Luigi311
405e5decf2 CI: Move away from docker-compose 2024-08-11 06:14:50 -06:00
Luigi311
da9abf8a24 Merge pull request #187 from luigi311/puid_pgid
Add puid pgid support to fix permission issues
2024-08-03 08:30:18 -06:00
Luis Garcia
128c6a1c76 Fix missing logs/mark folder if set 2024-07-24 02:09:04 -06:00
Luis Garcia
99f32c10ef Add support for PGID and PUID 2024-07-24 01:57:45 -06:00
Luigi311
44e42f99db Merge pull request #185 from luigi311/partial_support_check
Jellyfin/Emby: Check partial sync support
2024-07-15 21:08:07 -06:00
awakenedhaggis
b1639eab0f Jellyfin/Emby: Check partial sync support
- add `is_partial_update_supported` method to each class to validate given version against earliest known supported version
- add `get_server_version` to get server version number
- add `update_partial` parameter to user update function, deciding whether or not to allow partial updates
2024-07-15 11:45:43 -06:00
Luis Garcia
679d3535b1 CI: Fix latest 2024-07-15 05:10:36 -06:00
Luis Garcia
a795d4bba5 README: Remove dev information 2024-07-15 04:36:27 -06:00
Luis Garcia
0a025cf5fa Tie latest to version releases
Tie latest to version releases so dev no longer needs to exists and instead main is the new dev.
2024-07-15 04:36:27 -06:00
Luigi311
6a1ceb4db3 Merge pull request #182 from luigi311/dev
Jellyfin: Skip partial on version lower than 10.9
2024-07-15 03:36:40 -06:00
Luis Garcia
99c339c405 CI: Plex remove https 2024-07-15 03:28:59 -06:00
Luis Garcia
bd75d865ba Update PlexAPI and requests 2024-07-15 03:03:44 -06:00
Luis Garcia
d30e03b702 Jellyfin: Skip partial on version lower than 10.9 2024-07-15 03:01:53 -06:00
Luigi311
3b749faefb Merge pull request #175 from luigi311/dev
Remove seasons from watch list generation
2024-06-02 23:27:51 -06:00
Luis Garcia
74f29d44b3 README: Formatting 2024-06-02 23:22:06 -06:00
Luigi311
a397ceb54e Merge pull request #174 from luigi311/remove_season
Remove seasons from watch list
2024-06-02 23:14:06 -06:00
Luis Garcia
502b3616df Fix ci validation marklog 2024-06-02 23:11:11 -06:00
Luis Garcia
1a7178e32d Jellyfin: Remove episode filter as it doesnt exist in jellyfin 2024-06-02 23:11:11 -06:00
Luis Garcia
7119956ec7 Remove seasons from watch list 2024-06-02 20:14:02 -06:00
Luigi311
24035e217e Merge pull request #173 from luigi311/dev
Add support for emby, add sync in progress for jellyfin and emby
2024-06-02 18:29:02 -06:00
Luis Garcia
21ffce674f README: Add sync in progress ticks for Jellyfin and Emby 2024-06-02 18:22:30 -06:00
Luis Garcia
4185f5fc94 Fix tests to include partially watched jellyfin and emby 2024-06-02 18:19:48 -06:00
Luigi311
3fdcc99304 Merge pull request #171 from awakenedhaggis/feature/jellyfin-partial-watched
Jellyfin: support marking item as partially watched
2024-06-02 17:50:33 -06:00
awakenedhaggis
0fa2a698ac feat(jellyfin+emby): support marking item as partially watched
- this feature uses the new endpoint introduced with 24cbd64
2024-06-02 14:26:12 -06:00
dependabot[bot]
2b871c58ed ---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-01 23:30:27 -06:00
Luigi311
5078243938 Merge pull request #172 from luigi311/emby
Add Emby support
2024-06-01 23:25:16 -06:00
Luis Garcia
b67e6d7257 Add skip if nothing is being synced 2024-06-01 23:21:46 -06:00
Luis Garcia
632dfbcadb CI: Add more tests 2024-06-01 23:21:46 -06:00
Luis Garcia
1f7da2f609 Add support for emby 2024-06-01 23:21:07 -06:00
Luigi311
4a4c9f9ccf Update to python 12 2023-12-06 14:16:47 -07:00
28 changed files with 2442 additions and 1923 deletions

View File

@@ -35,7 +35,7 @@ GENERATE_GUIDS = "True"
GENERATE_LOCATIONS = "True" GENERATE_LOCATIONS = "True"
## Max threads for processing ## Max threads for processing
MAX_THREADS = 32 MAX_THREADS = 2
## Map usernames between servers in the event that they are different, order does not matter ## Map usernames between servers in the event that they are different, order does not matter
## Comma separated for multiple options ## Comma separated for multiple options
@@ -55,7 +55,6 @@ MAX_THREADS = 32
WHITELIST_USERS = "testuser1,testuser2" WHITELIST_USERS = "testuser1,testuser2"
# Plex # 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 ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
@@ -77,13 +76,6 @@ PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2"
## Set to True if running into ssl certificate errors ## Set to True if running into ssl certificate errors
SSL_BYPASS = "False" 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
@@ -94,3 +86,31 @@ 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 ## 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 ## Comma separated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2" JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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_PLEX_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_EMBY = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_EMBY = "True"
SYNC_FROM_EMBY_TO_PLEX = "True"
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
SYNC_FROM_EMBY_TO_EMBY = "True"

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [Luigi311]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -10,12 +10,19 @@ on:
- .gitignore - .gitignore
- "*.md" - "*.md"
env:
PYTHON_VERSION: '3.13'
jobs: jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install dependencies" - name: "Install dependencies"
run: pip install -r requirements.txt && pip install -r test/requirements.txt run: pip install -r requirements.txt && pip install -r test/requirements.txt
@@ -27,6 +34,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install dependencies" - name: "Install dependencies"
run: | run: |
pip install -r requirements.txt pip install -r requirements.txt
@@ -40,43 +51,67 @@ jobs:
- name: "Start containers" - name: "Start containers"
run: | run: |
export PGID=$(id -g) JellyPlex-Watched-CI/start_containers.sh
export PUID=$(id -u)
sudo chown -R $PUID:$PGID JellyPlex-Watched-CI
docker pull lscr.io/linuxserver/plex &
docker pull lscr.io/linuxserver/jellyfin &
wait
docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml up -d
docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml up -d
# Wait for containers to start # Wait for containers to start
sleep 10 sleep 10
docker-compose -f JellyPlex-Watched-CI/plex/docker-compose.yml logs for FOLDER in $(find "JellyPlex-Watched-CI" -type f -name "docker-compose.yml" -exec dirname {} \;); do
docker-compose -f JellyPlex-Watched-CI/jellyfin/docker-compose.yml logs docker compose -f "${FOLDER}/docker-compose.yml" logs
done
- name: "Run tests" - name: "Test Plex"
run: | run: |
# Test ci1 mv test/ci_plex.env .env
mv test/ci1.env .env
python main.py python main.py
python test/validate_ci_marklog.py --plex
# Test ci2 rm mark.log
mv test/ci2.env .env
- name: "Test Jellyfin"
run: |
mv test/ci_jellyfin.env .env
python main.py python main.py
python test/validate_ci_marklog.py --jellyfin
# Test ci3 rm mark.log
mv test/ci3.env .env
- name: "Test Emby"
run: |
mv test/ci_emby.env .env
python main.py
python test/validate_ci_marklog.py --emby
rm mark.log
- name: "Test Guids"
run: |
mv test/ci_guids.env .env
python main.py
python test/validate_ci_marklog.py --guids
rm mark.log
- name: "Test Locations"
run: |
mv test/ci_locations.env .env
python main.py
python test/validate_ci_marklog.py --locations
rm mark.log
- name: "Test writing to the servers"
run: |
# Test writing to the servers
mv test/ci_write.env .env
python main.py python main.py
# Test again to test if it can handle existing data # Test again to test if it can handle existing data
python main.py python main.py
cat mark.log python test/validate_ci_marklog.py --write
python test/validate_ci_marklog.py
rm mark.log
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -105,18 +140,23 @@ jobs:
${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }} ${{ 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 # 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'}} ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' && github.repository_owner == 'luigi311'}}
flavor: latest=false
tags: | tags: |
type=raw,value=latest,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == github.event.repository.default_branch }} type=raw,value=latest,enable=${{ matrix.variant == env.DEFAULT_VARIANT && startsWith(github.ref, 'refs/tags/') }}
type=raw,value=dev,enable=${{ matrix.variant == env.DEFAULT_VARIANT && github.ref_name == 'dev' }} type=raw,value=latest,suffix=-${{ matrix.variant }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=raw,value=latest,suffix=-${{ matrix.variant }},enable={{ is_default_branch }}
type=ref,event=branch,suffix=-${{ matrix.variant }} type=ref,event=branch,suffix=-${{ matrix.variant }}
type=ref,event=branch,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=ref,event=branch,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
type=ref,event=pr,suffix=-${{ matrix.variant }} type=ref,event=pr,suffix=-${{ matrix.variant }}
type=ref,event=pr,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=ref,event=pr,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
type=semver,pattern={{ version }},suffix=-${{ matrix.variant }} type=semver,pattern={{ version }},suffix=-${{ matrix.variant }}
type=semver,pattern={{ version }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=semver,pattern={{ version }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
type=semver,pattern={{ major }}.{{ minor }},suffix=-${{ matrix.variant }} type=semver,pattern={{ major }}.{{ minor }},suffix=-${{ matrix.variant }}
type=semver,pattern={{ major }}.{{ minor }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=semver,pattern={{ major }}.{{ minor }},enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
type=sha,suffix=-${{ matrix.variant }} type=sha,suffix=-${{ matrix.variant }}
type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}

View File

@@ -1,53 +1,49 @@
FROM python:3.11-alpine FROM python:3.13-alpine
ENV DRYRUN 'True' ENV PUID=1000
ENV DEBUG 'True' ENV PGID=1000
ENV DEBUG_LEVEL 'INFO' ENV GOSU_VERSION=1.17
ENV RUN_ONLY_ONCE 'False'
ENV SLEEP_DURATION '3600'
ENV LOGFILE 'log.log'
ENV MARKFILE 'mark.log'
ENV USER_MAPPING '' RUN apk add --no-cache tini dos2unix
ENV LIBRARY_MAPPING ''
ENV PLEX_BASEURL '' # Install gosu
ENV PLEX_TOKEN '' RUN set -eux; \
ENV PLEX_USERNAME '' \
ENV PLEX_PASSWORD '' apk add --no-cache --virtual .gosu-deps \
ENV PLEX_SERVERNAME '' ca-certificates \
dpkg \
ENV JELLYFIN_BASEURL '' gnupg \
ENV JELLYFIN_TOKEN '' ; \
\
ENV SYNC_FROM_PLEX_TO_JELLYFIN 'True' dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
ENV SYNC_FROM_JELLYFIN_TO_PLEX 'True' wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
ENV SYNC_FROM_PLEX_TO_PLEX 'True' wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
ENV SYNC_FROM_JELLYFIN_TO_JELLYFIN 'True' \
# verify the signature
ENV BLACKLIST_LIBRARY '' export GNUPGHOME="$(mktemp -d)"; \
ENV WHITELIST_LIBRARY '' gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
ENV BLACKLIST_LIBRARY_TYPE '' gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
ENV WHITELIST_LIBRARY_TYPE '' gpgconf --kill all; \
ENV BLACKLIST_USERS '' rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
ENV WHITELIST_USERS '' \
# clean up fetch dependencies
apk del --no-network .gosu-deps; \
RUN apk add --no-cache tini && \ \
addgroup --system jellyplex_user && \ chmod +x /usr/local/bin/gosu; \
adduser --system --no-create-home jellyplex_user --ingroup jellyplex_user && \ # verify that the binary works
mkdir -p /app && \ gosu --version; \
chown -R jellyplex_user:jellyplex_user /app gosu nobody true
WORKDIR /app WORKDIR /app
COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./ COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=jellyplex_user:jellyplex_user . . COPY . .
USER jellyplex_user RUN chmod +x *.sh && \
dos2unix *.sh
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
CMD ["python", "-u", "main.py"] CMD ["python", "-u", "main.py"]

View File

@@ -1,56 +1,23 @@
FROM python:3.11-slim FROM python:3.13-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 ''
ENV PUID=1000
ENV PGID=1000
RUN apt-get update && \ RUN apt-get update && \
apt-get install tini --yes --no-install-recommends && \ apt-get install tini gosu dos2unix --yes --no-install-recommends && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* && \ 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 WORKDIR /app
COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./ COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=jellyplex_user:jellyplex_user . . COPY . .
USER jellyplex_user RUN chmod +x *.sh && \
dos2unix *.sh
ENTRYPOINT ["/bin/tini", "--"] ENTRYPOINT ["/bin/tini", "--", "/app/entrypoint.sh"]
CMD ["python", "-u", "main.py"] CMD ["python", "-u", "main.py"]

112
README.md
View File

@@ -1,44 +1,44 @@
# JellyPlex-Watched # JellyPlex-Watched
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=luigi311/JellyPlex-Watched\&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/26b47c5db63942f28f02f207f692dc85)](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&utm_campaign=Badge_Grade)
Sync watched between jellyfin and plex locally Sync watched between jellyfin, plex and emby locally
## Description ## Description
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. Keep in sync all your users watched history between jellyfin, plex and emby 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 ## Features
### Plex ### Plex
* \[x] Match via filenames - \[x] Match via filenames
* \[x] Match via provider ids - \[x] Match via provider ids
* \[x] Map usernames - \[x] Map usernames
* \[x] Use single login - \[x] Use single login
* \[x] One way/multi way sync - \[x] One way/multi way sync
* \[x] Sync watched - \[x] Sync watched
* \[x] Sync in progress - \[x] Sync in progress
### Jellyfin ### Jellyfin
* \[x] Match via filenames - \[x] Match via filenames
* \[x] Match via provider ids - \[x] Match via provider ids
* \[x] Map usernames - \[x] Map usernames
* \[x] Use single login - \[x] Use single login
* \[x] One way/multi way sync - \[x] One way/multi way sync
* \[x] Sync watched - \[x] Sync watched
* \[ ] Sync in progress - \[x] Sync in progress
### Emby ### Emby
* \[ ] Match via filenames - \[x] Match via filenames
* \[ ] Match via provider ids - \[x] Match via provider ids
* \[ ] Map usernames - \[x] Map usernames
* \[ ] Use single login - \[x] Use single login
* \[ ] One way/multi way sync - \[x] One way/multi way sync
* \[ ] Sync watched - \[x] Sync watched
* \[ ] Sync in progress - \[x] Sync in progress
## Configuration ## Configuration
@@ -48,66 +48,66 @@ Full list of configuration options can be found in the [.env.sample](.env.sample
### Baremetal ### Baremetal
* Setup virtualenv of your choice - Setup virtualenv of your choice
* Install dependencies - Install dependencies
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
* Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens - Create a .env file similar to .env.sample, uncomment whitelist and blacklist if needed, fill in baseurls and tokens
* Run - Run
```bash ```bash
python main.py python main.py
``` ```
### Docker ### Docker
* Build docker image - Build docker image
```bash ```bash
docker build -t jellyplex-watched . docker build -t jellyplex-watched .
``` ```
* or use pre-built image - or use pre-built image
```bash ```bash
docker pull luigi311/jellyplex-watched:latest docker pull luigi311/jellyplex-watched:latest
``` ```
#### With variables #### With variables
* Run - Run
```bash ```bash
docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest docker run --rm -it -e PLEX_TOKEN='SuperSecretToken' luigi311/jellyplex-watched:latest
``` ```
#### With .env #### With .env
* Create a .env file similar to .env.sample and set the variables to match your setup - 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 ## Troubleshooting/Issues
* Jellyfin - 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 - Attempt to decode JSON with unexpected mimetype, make sure you enable remote access or add your docker subnet to lan networks in jellyfin settings
* Do not use quotes around variables in docker compose
- Configuration
- Do not use quotes around variables in docker compose
## Contributing ## Contributing
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. 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.
## License ## License

View File

@@ -1,32 +1,11 @@
version: '3' # Sync watched status between media servers locally
services: services:
jellyplex-watched: jellyplex-watched:
image: luigi311/jellyplex-watched:latest image: luigi311/jellyplex-watched:latest
container_name: jellyplex-watched container_name: jellyplex-watched
restart: always restart: unless-stopped
environment: environment:
- DRYRUN=True - PUID=1000
- DEBUG=True - PGID=1000
- DEBUG_LEVEL=info env_file: "./.env"
- 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

61
entrypoint.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env sh
set -e
# Check if user is root
if [ "$(id -u)" = '0' ]; then
echo "User is root, checking if we need to create a user and group based on environment variables"
# Create group and user based on environment variables
if [ ! "$(getent group "$PGID")" ]; then
# If groupadd exists, use it
if command -v groupadd > /dev/null; then
groupadd -g "$PGID" jellyplex_watched
elif command -v addgroup > /dev/null; then
addgroup -g "$PGID" jellyplex_watched
fi
fi
# If user id does not exist, create the user
if [ ! "$(getent passwd "$PUID")" ]; then
if command -v useradd > /dev/null; then
useradd --no-create-home -u "$PUID" -g "$PGID" jellyplex_watched
elif command -v adduser > /dev/null; then
# Get the group name based on the PGID since adduser does not have a flag to specify the group id
# and if the group id already exists the group name will be sommething unexpected
GROUPNAME=$(getent group "$PGID" | cut -d: -f1)
# Use alpine busybox adduser syntax
adduser -D -H -u "$PUID" -G "$GROUPNAME" jellyplex_watched
fi
fi
else
# If user is not root, set the PUID and PGID to the current user
PUID=$(id -u)
PGID=$(id -g)
fi
# Get directory of log and mark file to create base folder if it doesnt exist
LOG_DIR=$(dirname "$LOG_FILE")
# If LOG_DIR is set, create the directory
if [ -n "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR"
fi
MARK_DIR=$(dirname "$MARK_FILE")
if [ -n "$MARK_DIR" ]; then
mkdir -p "$MARK_DIR"
fi
echo "Starting JellyPlex-Watched with UID: $PUID and GID: $PGID"
# If root run as the created user
if [ "$(id -u)" = '0' ]; then
chown -R "$PUID:$PGID" "$LOG_DIR"
chown -R "$PUID:$PGID" "$MARK_DIR"
# Run the application as the created user
exec gosu "$PUID:$PGID" "$@"
fi
# Run the application as the current user
exec "$@"

Binary file not shown.

139
src/connection.py Normal file
View File

@@ -0,0 +1,139 @@
import os
from dotenv import load_dotenv
from src.functions import logger, str_to_bool
from src.plex import Plex
from src.jellyfin import Jellyfin
from src.emby import Emby
load_dotenv(override=True)
def jellyfin_emby_server_connection(server_baseurl, server_token, server_type):
servers = []
server_baseurl = server_baseurl.split(",")
server_token = server_token.split(",")
if len(server_baseurl) != len(server_token):
raise Exception(
f"{server_type.upper()}_BASEURL and {server_type.upper()}_TOKEN must have the same number of entries"
)
for i, baseurl in enumerate(server_baseurl):
baseurl = baseurl.strip()
if baseurl[-1] == "/":
baseurl = baseurl[:-1]
if server_type == "jellyfin":
server = Jellyfin(baseurl=baseurl, token=server_token[i].strip())
servers.append(
(
"jellyfin",
server,
)
)
elif server_type == "emby":
server = Emby(baseurl=baseurl, token=server_token[i].strip())
servers.append(
(
"emby",
server,
)
)
else:
raise Exception("Unknown server type")
logger(f"{server_type} Server {i} info: {server.info()}", 3)
return servers
def generate_server_connections():
servers = []
plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None)
plex_username = os.getenv("PLEX_USERNAME", None)
plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None)
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",")
plex_token = plex_token.split(",")
if len(plex_baseurl) != len(plex_token):
raise Exception(
"PLEX_BASEURL and PLEX_TOKEN must have the same number of entries"
)
for i, url in enumerate(plex_baseurl):
server = Plex(
baseurl=url.strip(),
token=plex_token[i].strip(),
username=None,
password=None,
servername=None,
ssl_bypass=ssl_bypass,
)
logger(f"Plex Server {i} info: {server.info()}", 3)
servers.append(
(
"plex",
server,
)
)
if plex_username and plex_password and plex_servername:
plex_username = plex_username.split(",")
plex_password = plex_password.split(",")
plex_servername = plex_servername.split(",")
if len(plex_username) != len(plex_password) or len(plex_username) != len(
plex_servername
):
raise Exception(
"PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries"
)
for i, username in enumerate(plex_username):
server = Plex(
baseurl=None,
token=None,
username=username.strip(),
password=plex_password[i].strip(),
servername=plex_servername[i].strip(),
ssl_bypass=ssl_bypass,
)
logger(f"Plex Server {i} info: {server.info()}", 3)
servers.append(
(
"plex",
server,
)
)
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
if jellyfin_baseurl and jellyfin_token:
servers.extend(
jellyfin_emby_server_connection(
jellyfin_baseurl, jellyfin_token, "jellyfin"
)
)
emby_baseurl = os.getenv("EMBY_BASEURL", None)
emby_token = os.getenv("EMBY_TOKEN", None)
if emby_baseurl and emby_token:
servers.extend(
jellyfin_emby_server_connection(emby_baseurl, emby_token, "emby")
)
return servers

25
src/emby.py Normal file
View File

@@ -0,0 +1,25 @@
from src.jellyfin_emby import JellyfinEmby
from packaging import version
class Emby(JellyfinEmby):
def __init__(self, baseurl, token):
authorization = (
"Emby , "
'Client="JellyPlex-Watched", '
'Device="script", '
'DeviceId="script", '
'Version="6.0.2"'
)
headers = {
"Accept": "application/json",
"X-Emby-Token": token,
"X-Emby-Authorization": authorization,
}
super().__init__(
server_type="Emby", baseurl=baseurl, token=token, headers=headers
)
def is_partial_update_supported(self, server_version):
return server_version > version.parse("4.4")

View File

@@ -4,8 +4,8 @@ from dotenv import load_dotenv
load_dotenv(override=True) load_dotenv(override=True)
logfile = os.getenv("LOGFILE", "log.log") log_file = os.getenv("LOG_FILE", os.getenv("LOGFILE", "log.log"))
markfile = os.getenv("MARKFILE", "mark.log") mark_file = os.getenv("MARK_FILE", os.getenv("MARKFILE", "mark.log"))
def logger(message: str, log_type=0): def logger(message: str, log_type=0):
@@ -32,17 +32,23 @@ def logger(message: str, log_type=0):
if output is not None: if output is not None:
print(output) print(output)
file = open(logfile, "a", encoding="utf-8") with open(f"{log_file}", "a", encoding="utf-8") as file:
file.write(output + "\n") file.write(output + "\n")
def log_marked( def log_marked(
username: str, library: str, movie_show: str, episode: str = None, duration=None server_type: str,
server_name: str,
username: str,
library: str,
movie_show: str,
episode: str = None,
duration=None,
): ):
if markfile is None: if mark_file is None:
return return
output = f"{username}/{library}/{movie_show}" output = f"{server_type}/{server_name}/{username}/{library}/{movie_show}"
if episode: if episode:
output += f"/{episode}" output += f"/{episode}"
@@ -50,8 +56,8 @@ def log_marked(
if duration: if duration:
output += f"/{duration}" output += f"/{duration}"
file = open(f"{markfile}", "a", encoding="utf-8") with open(f"{mark_file}", "a", encoding="utf-8") as file:
file.write(output + "\n") file.write(output + "\n")
# Reimplementation of distutils.util.strtobool due to it being deprecated # Reimplementation of distutils.util.strtobool due to it being deprecated
@@ -93,6 +99,20 @@ def search_mapping(dictionary: dict, key_value: str):
return None return None
# Return list of objects that exist in both lists including mappings
def match_list(list1, list2, list_mapping=None):
output = []
for element in list1:
if element in list2:
output.append(element)
elif list_mapping:
element_other = search_mapping(list_mapping, element)
if element_other in list2:
output.append(element)
return output
def future_thread_executor( def future_thread_executor(
args: list, threads: int = None, override_threads: bool = False args: list, threads: int = None, override_threads: bool = False
): ):

View File

@@ -1,859 +1,25 @@
import traceback, os from src.jellyfin_emby import JellyfinEmby
from math import floor from packaging import version
from dotenv import load_dotenv
import requests
from src.functions import (
logger,
search_mapping,
contains_nested,
log_marked,
str_to_bool,
)
from src.library import (
check_skip_logic,
generate_library_guids_dict,
)
from src.watched import (
combine_watched_dicts,
)
load_dotenv(override=True)
generate_guids = str_to_bool(os.getenv("GENERATE_GUIDS", "True"))
generate_locations = str_to_bool(os.getenv("GENERATE_LOCATIONS", "True"))
def get_guids(item): class Jellyfin(JellyfinEmby):
if item.get("Name"):
guids = {"title": item.get("Name")}
else:
logger(f"Jellyfin: Name not found in {item.get('Id')}", 1)
guids = {"title": None}
if "ProviderIds" in item:
guids.update({k.lower(): v for k, v in item["ProviderIds"].items()})
else:
logger(f"Jellyfin: ProviderIds not found in {item.get('Name')}", 1)
if "MediaSources" in item:
guids["locations"] = tuple(
[x["Path"].split("/")[-1] for x in item["MediaSources"] if "Path" in x]
)
else:
logger(f"Jellyfin: MediaSources not found in {item.get('Name')}", 1)
guids["locations"] = tuple()
if "UserData" in item:
guids["status"] = {
"completed": item["UserData"]["Played"],
# Convert ticks to milliseconds to match Plex
"time": floor(item["UserData"]["PlaybackPositionTicks"] / 10000),
}
else:
logger(f"Jellyfin: UserData not found in {item.get('Name')}", 1)
guids["status"] = {}
return guids
def get_video_status(jellyfin_video, videos_ids, videos):
video_status = None
if generate_locations:
if "MediaSources" in jellyfin_video:
for video_location in jellyfin_video["MediaSources"]:
if "Path" in video_location:
if (
contains_nested(
video_location["Path"].split("/")[-1],
videos_ids["locations"],
)
is not None
):
for video in videos:
if (
contains_nested(
video_location["Path"].split("/")[-1],
video["locations"],
)
is not None
):
video_status = video["status"]
break
break
if generate_guids:
if not video_status:
for (
video_provider_source,
video_provider_id,
) in jellyfin_video["ProviderIds"].items():
if video_provider_source.lower() in videos_ids:
if (
video_provider_id.lower()
in videos_ids[video_provider_source.lower()]
):
for video in videos:
if video_provider_id.lower() in video.get(
video_provider_source.lower(), []
):
video_status = video["status"]
break
break
return video_status
class Jellyfin:
def __init__(self, baseurl, token): def __init__(self, baseurl, token):
self.baseurl = baseurl authorization = (
self.token = token "MediaBrowser , "
self.timeout = int(os.getenv("REQUEST_TIMEOUT", 300)) 'Client="JellyPlex-Watched", '
'Device="script", '
if not self.baseurl: 'DeviceId="script", '
raise Exception("Jellyfin baseurl not set") 'Version="6.0.2", '
f'Token="{token}"'
if not self.token: )
raise Exception("Jellyfin token not set") headers = {
"Accept": "application/json",
self.session = requests.Session() "Authorization": authorization,
self.users = self.get_users() }
def query(self, query, query_type, session=None, identifiers=None): super().__init__(
try: server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers
results = None )
authorization = ( def is_partial_update_supported(self, server_version):
"MediaBrowser , " return server_version >= version.parse("10.9.0")
'Client="other", '
'Device="script", '
'DeviceId="script", '
'Version="0.0.0"'
)
headers = {
"Accept": "application/json",
"X-Emby-Token": self.token,
"X-Emby-Authorization": authorization,
}
if query_type == "get":
response = self.session.get(
self.baseurl + query, headers=headers, timeout=self.timeout
)
if response.status_code != 200:
raise Exception(
f"Query failed with status {response.status_code} {response.reason}"
)
results = response.json()
elif query_type == "post":
response = self.session.post(
self.baseurl + query, headers=headers, timeout=self.timeout
)
if response.status_code != 200:
raise Exception(
f"Query failed with status {response.status_code} {response.reason}"
)
results = response.json()
if not isinstance(results, list) and not isinstance(results, dict):
raise Exception("Query result is not of type list or dict")
# append identifiers to results
if identifiers:
results["Identifiers"] = identifiers
return results
except Exception as e:
logger(f"Jellyfin: Query {query_type} {query}\nResults {results}\n{e}", 2)
raise Exception(e)
def info(self) -> str:
try:
query_string = "/System/Info/Public"
response = self.query(query_string, "get")
if response:
return f"{response['ServerName']}: {response['Version']}"
else:
return None
except Exception as e:
logger(f"Jellyfin: Get server name failed {e}", 2)
raise Exception(e)
def get_users(self):
try:
users = {}
query_string = "/Users"
response = self.query(query_string, "get")
# If response is not empty
if response:
for user in response:
users[user["Name"]] = user["Id"]
return users
except Exception as e:
logger(f"Jellyfin: Get users failed {e}", 2)
raise Exception(e)
def get_user_library_watched(
self, user_name, user_id, library_type, library_id, library_title
):
try:
user_name = user_name.lower()
user_watched = {}
user_watched[user_name] = {}
logger(
f"Jellyfin: Generating watched for {user_name} in library {library_title}",
0,
)
# Movies
if library_type == "Movie":
user_watched[user_name][library_title] = []
watched = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
)
in_progress = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsResumable&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
)
for movie in watched["Items"] + in_progress["Items"]:
if "MediaSources" in movie and movie["MediaSources"] != {}:
if "UserData" not in movie:
continue
# Skip if not watched or watched less than a minute
if (
movie["UserData"]["Played"] == True
or movie["UserData"]["PlaybackPositionTicks"] > 600000000
):
logger(
f"Jellyfin: Adding {movie.get('Name')} to {user_name} watched list",
3,
)
# Get the movie's GUIDs
movie_guids = get_guids(movie)
# Append the movie dictionary to the list for the given user and library
user_watched[user_name][library_title].append(movie_guids)
logger(
f"Jellyfin: Added {movie_guids} to {user_name} watched list",
3,
)
# TV Shows
if library_type in ["Series", "Episode"]:
# Initialize an empty dictionary for the given user and library
user_watched[user_name][library_title] = {}
# Retrieve a list of watched TV shows
watched_shows = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount",
"get",
)
# Filter the list of shows to only include those that have been partially or fully watched
watched_shows_filtered = []
for show in watched_shows["Items"]:
if not "UserData" in show:
continue
if "PlayedPercentage" in show["UserData"]:
if show["UserData"]["PlayedPercentage"] > 0:
watched_shows_filtered.append(show)
# Retrieve the seasons of each watched show
seasons_watched = []
for show in watched_shows_filtered:
logger(
f"Jellyfin: Adding {show.get('Name')} to {user_name} watched list",
3,
)
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
show_guids["title"] = show["Name"]
show_guids["locations"] = (
tuple([show["Path"].split("/")[-1]])
if "Path" in show
else tuple()
)
show_guids = frozenset(show_guids.items())
show_identifiers = {
"show_guids": show_guids,
"show_id": show["Id"],
}
seasons_watched.append(
self.query(
f"/Shows/{show['Id']}/Seasons"
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount",
"get",
identifiers=frozenset(show_identifiers.items()),
)
)
# Filter the list of seasons to only include those that have been partially or fully watched
seasons_watched_filtered = []
for seasons in seasons_watched:
seasons_watched_filtered_dict = {}
seasons_watched_filtered_dict["Identifiers"] = seasons[
"Identifiers"
]
seasons_watched_filtered_dict["Items"] = []
for season in seasons["Items"]:
if "PlayedPercentage" in season["UserData"]:
if season["UserData"]["PlayedPercentage"] > 0:
seasons_watched_filtered_dict["Items"].append(season)
if seasons_watched_filtered_dict["Items"]:
seasons_watched_filtered.append(seasons_watched_filtered_dict)
# Create a list of tasks to retrieve the episodes of each watched season
watched_episodes = []
for seasons in seasons_watched_filtered:
if len(seasons["Items"]) > 0:
for season in seasons["Items"]:
if "IndexNumber" not in season:
logger(
f"Jellyfin: Skipping show {season.get('SeriesName')} season {season.get('Name')} as it has no index number",
3,
)
continue
season_identifiers = dict(seasons["Identifiers"])
season_identifiers["season_index"] = season["IndexNumber"]
watched_task = self.query(
f"/Shows/{season_identifiers['show_id']}/Episodes"
+ f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsPlayed&Fields=ProviderIds,MediaSources",
"get",
identifiers=frozenset(season_identifiers.items()),
)
in_progress_task = self.query(
f"/Shows/{season_identifiers['show_id']}/Episodes"
+ f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsResumable&Fields=ProviderIds,MediaSources",
"get",
identifiers=frozenset(season_identifiers.items()),
)
watched_episodes.append(watched_task)
watched_episodes.append(in_progress_task)
# Iterate through the watched episodes
for episodes in watched_episodes:
# If the season has any watched episodes
if len(episodes["Items"]) > 0:
# Create a dictionary for the season with its identifier and episodes
season_dict = {}
season_dict["Identifiers"] = dict(episodes["Identifiers"])
season_dict["Episodes"] = []
for episode in episodes["Items"]:
if (
"MediaSources" in episode
and episode["MediaSources"] != {}
):
# If watched or watched more than a minute
if (
episode["UserData"]["Played"] == True
or episode["UserData"]["PlaybackPositionTicks"]
> 600000000
):
episode_dict = get_guids(episode)
# Add the episode dictionary to the season's list of episodes
season_dict["Episodes"].append(episode_dict)
# Add the season dictionary to the show's list of seasons
if (
season_dict["Identifiers"]["show_guids"]
not in user_watched[user_name][library_title]
):
user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
] = {}
if (
season_dict["Identifiers"]["season_index"]
not in user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
]
):
user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
][season_dict["Identifiers"]["season_index"]] = []
user_watched[user_name][library_title][
season_dict["Identifiers"]["show_guids"]
][season_dict["Identifiers"]["season_index"]] = season_dict[
"Episodes"
]
logger(
f"Jellyfin: Added {season_dict['Episodes']} to {user_name} {season_dict['Identifiers']['show_guids']} watched list",
1,
)
logger(
f"Jellyfin: Got watched for {user_name} in library {library_title}", 1
)
if library_title in user_watched[user_name]:
logger(f"Jellyfin: {user_watched[user_name][library_title]}", 3)
return user_watched
except Exception as e:
logger(
f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
2,
)
logger(traceback.format_exc(), 2)
return {}
def get_users_watched(
self,
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
try:
# Get all libraries
user_name = user_name.lower()
watched = []
libraries = []
all_libraries = self.query(f"/Users/{user_id}/Views", "get")
for library in all_libraries["Items"]:
library_id = library["Id"]
library_title = library["Name"]
identifiers = {
"library_id": library_id,
"library_title": library_title,
}
libraries.append(
self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
"get",
identifiers=identifiers,
)
)
for library in libraries:
if len(library["Items"]) == 0:
continue
library_id = library["Identifiers"]["library_id"]
library_title = library["Identifiers"]["library_title"]
# Get all library types excluding "Folder"
types = set(
[
x["Type"]
for x in library["Items"]
if x["Type"] in ["Movie", "Series", "Episode"]
]
)
skip_reason = check_skip_logic(
library_title,
types,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
if skip_reason:
logger(
f"Jellyfin: Skipping library {library_title}: {skip_reason}",
1,
)
continue
# If there are multiple types in library raise error
if types is None or len(types) < 1:
all_types = set([x["Type"] for x in library["Items"]])
logger(
f"Jellyfin: Skipping Library {library_title} found types: {types}, all types: {all_types}",
1,
)
continue
for library_type in types:
# Get watched for user
watched.append(
self.get_user_library_watched(
user_name,
user_id,
library_type,
library_id,
library_title,
)
)
return watched
except Exception as e:
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
raise Exception(e)
def get_watched(
self,
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping=None,
):
try:
users_watched = {}
watched = []
for user_name, user_id in users.items():
watched.append(
self.get_users_watched(
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
)
for user_watched in watched:
user_watched_combine = combine_watched_dicts(user_watched)
for user, user_watched_temp in user_watched_combine.items():
if user not in users_watched:
users_watched[user] = {}
users_watched[user].update(user_watched_temp)
return users_watched
except Exception as e:
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
raise Exception(e)
def update_user_watched(
self, user_name, user_id, library, library_id, videos, dryrun
):
try:
logger(
f"Jellyfin: Updating watched for {user_name} in library {library}", 1
)
(
videos_shows_ids,
videos_episodes_ids,
videos_movies_ids,
) = generate_library_guids_dict(videos)
if (
not videos_movies_ids
and not videos_shows_ids
and not videos_episodes_ids
):
logger(
f"Jellyfin: No videos to mark as watched for {user_name} in library {library}",
1,
)
return
logger(
f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
1,
)
if videos_movies_ids:
jellyfin_search = self.query(
f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie",
"get",
)
for jellyfin_video in jellyfin_search["Items"]:
movie_status = get_video_status(
jellyfin_video, videos_movies_ids, videos
)
if movie_status:
jellyfin_video_id = jellyfin_video["Id"]
if movie_status["completed"]:
msg = f"Jellyfin: {jellyfin_video.get('Name')} as watched for {user_name} in {library}"
if not dryrun:
logger(msg, 5)
self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
"post",
)
else:
logger(msg, 6)
log_marked(
user_name,
library,
jellyfin_video.get("Name"),
)
else:
# TODO add support for partially watched movies
msg = f"Jellyfin: {jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library}"
"""
if not dryrun:
pass
# logger(msg, 5)
else:
pass
# logger(msg, 6)
log_marked(
user_name,
library,
jellyfin_video.get("Name"),
duration=floor(movie_status["time"] / 60_000),
)"""
else:
logger(
f"Jellyfin: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}",
3,
)
# TV Shows
if videos_shows_ids and videos_episodes_ids:
jellyfin_search = self.query(
f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series",
"get",
)
jellyfin_shows = [x for x in jellyfin_search["Items"]]
for jellyfin_show in jellyfin_shows:
show_found = False
episode_videos = []
if generate_locations:
if "Path" in jellyfin_show:
if (
contains_nested(
jellyfin_show["Path"].split("/")[-1],
videos_shows_ids["locations"],
)
is not None
):
show_found = True
for shows, seasons in videos.items():
show = {k: v for k, v in shows}
if (
contains_nested(
jellyfin_show["Path"].split("/")[-1],
show["locations"],
)
is not None
):
for season in seasons.values():
for episode in season:
episode_videos.append(episode)
break
if generate_guids:
if not show_found:
for show_provider_source, show_provider_id in jellyfin_show[
"ProviderIds"
].items():
if show_provider_source.lower() in videos_shows_ids:
if (
show_provider_id.lower()
in videos_shows_ids[
show_provider_source.lower()
]
):
show_found = True
for show, seasons in videos.items():
show = {k: v for k, v in show}
if show_provider_id.lower() in show.get(
show_provider_source.lower(), []
):
for season in seasons.values():
for episode in season:
episode_videos.append(episode)
break
if show_found:
logger(
f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show.get('Name')}",
1,
)
jellyfin_show_id = jellyfin_show["Id"]
jellyfin_episodes = self.query(
f"/Shows/{jellyfin_show_id}/Episodes"
+ f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
)
for jellyfin_episode in jellyfin_episodes["Items"]:
episode_status = get_video_status(
jellyfin_episode, videos_episodes_ids, episode_videos
)
if episode_status:
jellyfin_episode_id = jellyfin_episode["Id"]
if episode_status["completed"]:
msg = (
f"Jellyfin: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
+ f" as watched for {user_name} in {library}"
)
if not dryrun:
logger(msg, 5)
self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
"post",
)
else:
logger(msg, 6)
log_marked(
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"),
)
else:
# TODO add support for partially watched episodes
msg = (
f"Jellyfin: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
+ f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library}"
)
"""
if not dryrun:
pass
# logger(f"Marked {msg}", 0)
else:
pass
# logger(f"Dryrun {msg}", 0)
log_marked(
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get('Name'),
duration=floor(episode_status["time"] / 60_000),
)"""
else:
logger(
f"Jellyfin: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}",
3,
)
else:
logger(
f"Jellyfin: Skipping show {jellyfin_show.get('Name')} as it is not in mark list for {user_name}",
3,
)
except Exception as e:
logger(
f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}",
2,
)
logger(traceback.format_exc(), 2)
raise Exception(e)
def update_watched(
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
):
try:
for user, libraries in watched_list.items():
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
user_other = None
user_name = None
if user_mapping:
if user in user_mapping.keys():
user_other = user_mapping[user]
elif user in user_mapping.values():
user_other = search_mapping(user_mapping, user)
user_id = None
for key in self.users:
if user.lower() == key.lower():
user_id = self.users[key]
user_name = key
break
elif user_other and user_other.lower() == key.lower():
user_id = self.users[key]
user_name = key
break
if not user_id:
logger(f"{user} {user_other} not found in Jellyfin", 2)
continue
jellyfin_libraries = self.query(
f"/Users/{user_id}/Views",
"get",
)
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
for library, videos in libraries.items():
library_other = None
if library_mapping:
if library in library_mapping.keys():
library_other = library_mapping[library]
elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library)
if library.lower() not in [
x["Name"].lower() for x in jellyfin_libraries
]:
if library_other:
if library_other.lower() in [
x["Name"].lower() for x in jellyfin_libraries
]:
logger(
f"Jellyfin: Library {library} not found, but {library_other} found, using {library_other}",
1,
)
library = library_other
else:
logger(
f"Jellyfin: Library {library} or {library_other} not found in library list",
1,
)
continue
else:
logger(
f"Jellyfin: Library {library} not found in library list",
1,
)
continue
library_id = None
for jellyfin_library in jellyfin_libraries:
if jellyfin_library["Name"] == library:
library_id = jellyfin_library["Id"]
continue
if library_id:
self.update_user_watched(
user_name, user_id, library, library_id, videos, dryrun
)
except Exception as e:
logger(f"Jellyfin: Error updating watched, {e}", 2)
raise Exception(e)

820
src/jellyfin_emby.py Normal file
View File

@@ -0,0 +1,820 @@
# Functions for Jellyfin and Emby
import traceback, os
from math import floor
from dotenv import load_dotenv
import requests
from packaging import version
from src.functions import (
logger,
search_mapping,
contains_nested,
log_marked,
str_to_bool,
)
from src.library import generate_library_guids_dict
load_dotenv(override=True)
generate_guids = str_to_bool(os.getenv("GENERATE_GUIDS", "True"))
generate_locations = str_to_bool(os.getenv("GENERATE_LOCATIONS", "True"))
def get_guids(server_type, item):
if item.get("Name"):
guids = {"title": item.get("Name")}
else:
logger(f"{server_type}: Name not found in {item.get('Id')}", 1)
guids = {"title": None}
if "ProviderIds" in item:
guids.update({k.lower(): v for k, v in item["ProviderIds"].items()})
else:
logger(f"{server_type}: ProviderIds not found in {item.get('Name')}", 1)
if "MediaSources" in item:
guids["locations"] = tuple(
[x["Path"].split("/")[-1] for x in item["MediaSources"] if "Path" in x]
)
else:
logger(f"{server_type}: MediaSources not found in {item.get('Name')}", 1)
guids["locations"] = tuple()
if "UserData" in item:
guids["status"] = {
"completed": item["UserData"]["Played"],
# Convert ticks to milliseconds to match Plex
"time": floor(item["UserData"]["PlaybackPositionTicks"] / 10000),
}
else:
logger(f"{server_type}: UserData not found in {item.get('Name')}", 1)
guids["status"] = {}
return guids
def get_video_status(server_video, videos_ids, videos):
video_status = None
if generate_locations:
if "MediaSources" in server_video:
for video_location in server_video["MediaSources"]:
if "Path" in video_location:
if (
contains_nested(
video_location["Path"].split("/")[-1],
videos_ids["locations"],
)
is not None
):
for video in videos:
if (
contains_nested(
video_location["Path"].split("/")[-1],
video["locations"],
)
is not None
):
video_status = video["status"]
break
break
if generate_guids:
if not video_status:
for (
video_provider_source,
video_provider_id,
) in server_video["ProviderIds"].items():
if video_provider_source.lower() in videos_ids:
if (
video_provider_id.lower()
in videos_ids[video_provider_source.lower()]
):
for video in videos:
if video_provider_id.lower() in video.get(
video_provider_source.lower(), []
):
video_status = video["status"]
break
break
return video_status
class JellyfinEmby:
def __init__(self, server_type, baseurl, token, headers):
if server_type not in ["Jellyfin", "Emby"]:
raise Exception(f"Server type {server_type} not supported")
self.server_type = server_type
self.baseurl = baseurl
self.token = token
self.headers = headers
self.timeout = int(os.getenv("REQUEST_TIMEOUT", 300))
if not self.baseurl:
raise Exception(f"{self.server_type} baseurl not set")
if not self.token:
raise Exception(f"{self.server_type} token not set")
self.session = requests.Session()
self.users = self.get_users()
self.server_name = self.info(name_only=True)
def query(self, query, query_type, identifiers=None, json=None):
try:
results = None
if query_type == "get":
response = self.session.get(
self.baseurl + query, headers=self.headers, timeout=self.timeout
)
if response.status_code not in [200, 204]:
raise Exception(
f"Query failed with status {response.status_code} {response.reason}"
)
if response.status_code == 204:
results = None
else:
results = response.json()
elif query_type == "post":
response = self.session.post(
self.baseurl + query,
headers=self.headers,
json=json,
timeout=self.timeout,
)
if response.status_code not in [200, 204]:
raise Exception(
f"Query failed with status {response.status_code} {response.reason}"
)
if response.status_code == 204:
results = None
else:
results = response.json()
if results is not None:
if not isinstance(results, list) and not isinstance(results, dict):
raise Exception("Query result is not of type list or dict")
# append identifiers to results
if identifiers:
results["Identifiers"] = identifiers
return results
except Exception as e:
logger(
f"{self.server_type}: Query {query_type} {query}\nResults {results}\n{e}",
2,
)
raise Exception(e)
def info(self, name_only: bool = False) -> str:
try:
query_string = "/System/Info/Public"
response = self.query(query_string, "get")
if response:
if name_only:
return f"{response['ServerName']}"
return f"{self.server_type} {response['ServerName']}: {response['Version']}"
else:
return None
except Exception as e:
logger(f"{self.server_type}: Get server name failed {e}", 2)
raise Exception(e)
def get_server_version(self):
try:
response = self.query("/System/Info/Public", "get")
if response:
return version.parse(response["Version"])
else:
return None
except Exception as e:
logger(f"{self.server_type}: Get server version failed: {e}", 2)
raise Exception(e)
def get_users(self):
try:
users = {}
query_string = "/Users"
response = self.query(query_string, "get")
# If response is not empty
if response:
for user in response:
users[user["Name"]] = user["Id"]
return users
except Exception as e:
logger(f"{self.server_type}: Get users failed {e}", 2)
raise Exception(e)
def get_libraries(self):
try:
libraries = {}
# Theres no way to get all libraries so individually get list of libraries from all users
users = self.get_users()
for _, user_id in users.items():
user_libraries = self.query(f"/Users/{user_id}/Views", "get")
for library in user_libraries["Items"]:
library_id = library["Id"]
library_title = library["Name"]
# Get library items to check the type
media_info = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
"get",
)
types = set(
[
x["Type"]
for x in media_info["Items"]
if x["Type"] in ["Movie", "Series", "Episode"]
]
)
all_types = set([x["Type"] for x in media_info["Items"]])
if not types:
logger(
f"{self.server_type}: Skipping Library {library_title} found wanted types: {all_types}",
1,
)
else:
libraries[library_title] = str(types)
return libraries
except Exception as e:
logger(f"{self.server_type}: Get libraries failed {e}", 2)
raise Exception(e)
def get_user_library_watched(
self, user_name, user_id, library_type, library_id, library_title
):
try:
user_name = user_name.lower()
user_watched = {}
logger(
f"{self.server_type}: Generating watched for {user_name} in library {library_title}",
0,
)
# Movies
if library_type == "Movie":
user_watched[library_title] = []
watched = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
).get("Items", [])
in_progress = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsResumable&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
).get("Items", [])
for movie in watched + in_progress:
# Skip if theres no user data which means the movie has not been watched
if "UserData" not in movie:
continue
# Skip if theres no media tied to the movie
if "MediaSources" not in movie or movie["MediaSources"] == {}:
continue
# Skip if not watched or watched less than a minute
if (
movie["UserData"]["Played"] == True
or movie["UserData"]["PlaybackPositionTicks"] > 600000000
):
logger(
f"{self.server_type}: Adding {movie.get('Name')} to {user_name} watched list",
3,
)
# Get the movie's GUIDs
movie_guids = get_guids(self.server_type, movie)
# Append the movie dictionary to the list for the given user and library
user_watched[library_title].append(movie_guids)
logger(
f"{self.server_type}: Added {movie_guids} to {user_name} watched list",
3,
)
# TV Shows
if library_type in ["Series", "Episode"]:
# Initialize an empty dictionary for the given user and library
user_watched[library_title] = {}
# Retrieve a list of watched TV shows
watched_shows = self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount",
"get",
).get("Items", [])
# Filter the list of shows to only include those that have been partially or fully watched
watched_shows_filtered = []
for show in watched_shows:
if "UserData" not in show:
continue
if "PlayedPercentage" in show["UserData"]:
if show["UserData"]["PlayedPercentage"] > 0:
watched_shows_filtered.append(show)
# Retrieve the watched/partially watched list of episodes of each watched show
for show in watched_shows_filtered:
logger(
f"{self.server_type}: Adding {show.get('Name')} to {user_name} watched list",
3,
)
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
show_guids["title"] = show["Name"]
show_guids["locations"] = (
tuple([show["Path"].split("/")[-1]])
if "Path" in show
else tuple()
)
show_guids = frozenset(show_guids.items())
show_episodes = self.query(
f"/Shows/{show['Id']}/Episodes"
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,MediaSources",
"get",
).get("Items", [])
# Iterate through the episodes
# Create a list to store the episodes
mark_episodes_list = []
for episode in show_episodes:
if "UserData" not in episode:
continue
if (
"MediaSources" not in episode
or episode["MediaSources"] == {}
):
continue
# If watched or watched more than a minute
if (
episode["UserData"]["Played"] == True
or episode["UserData"]["PlaybackPositionTicks"] > 600000000
):
episode_guids = get_guids(self.server_type, episode)
mark_episodes_list.append(episode_guids)
if mark_episodes_list:
# Add the show dictionary to the user's watched list
if show_guids not in user_watched[library_title]:
user_watched[library_title][show_guids] = []
user_watched[library_title][show_guids] = mark_episodes_list
for episode in mark_episodes_list:
logger(
f"{self.server_type}: Added {episode} to {user_name} watched list",
3,
)
logger(
f"{self.server_type}: Got watched for {user_name} in library {library_title}",
1,
)
if library_title in user_watched:
logger(f"{self.server_type}: {user_watched[library_title]}", 3)
return user_watched
except Exception as e:
logger(
f"{self.server_type}: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
2,
)
logger(traceback.format_exc(), 2)
return {}
def get_watched(self, users, sync_libraries):
try:
users_watched = {}
watched = []
for user_name, user_id in users.items():
libraries = []
all_libraries = self.query(f"/Users/{user_id}/Views", "get")
for library in all_libraries["Items"]:
library_id = library["Id"]
library_title = library["Name"]
if library_title not in sync_libraries:
continue
identifiers = {
"library_id": library_id,
"library_title": library_title,
}
libraries.append(
self.query(
f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
"get",
identifiers=identifiers,
)
)
for library in libraries:
if len(library["Items"]) == 0:
continue
library_id = library["Identifiers"]["library_id"]
library_title = library["Identifiers"]["library_title"]
# Get all library types excluding "Folder"
types = set(
[
x["Type"]
for x in library["Items"]
if x["Type"] in ["Movie", "Series", "Episode"]
]
)
for library_type in types:
# Get watched for user
watched = self.get_user_library_watched(
user_name,
user_id,
library_type,
library_id,
library_title,
)
if user_name.lower() not in users_watched:
users_watched[user_name.lower()] = {}
users_watched[user_name.lower()].update(watched)
return users_watched
except Exception as e:
logger(f"{self.server_type}: Failed to get watched, Error: {e}", 2)
raise Exception(e)
def update_user_watched(
self, user_name, user_id, library, library_id, videos, update_partial, dryrun
):
try:
logger(
f"{self.server_type}: Updating watched for {user_name} in library {library}",
1,
)
(
videos_shows_ids,
videos_episodes_ids,
videos_movies_ids,
) = generate_library_guids_dict(videos)
if (
not videos_movies_ids
and not videos_shows_ids
and not videos_episodes_ids
):
logger(
f"{self.server_type}: No videos to mark as watched for {user_name} in library {library}",
1,
)
return
logger(
f"{self.server_type}: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
1,
)
if videos_movies_ids:
jellyfin_search = self.query(
f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie",
"get",
)
for jellyfin_video in jellyfin_search["Items"]:
movie_status = get_video_status(
jellyfin_video, videos_movies_ids, videos
)
if movie_status:
jellyfin_video_id = jellyfin_video["Id"]
if movie_status["completed"]:
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as watched for {user_name} in {library}"
if not dryrun:
logger(msg, 5)
self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
"post",
)
else:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_video.get("Name"),
)
elif update_partial:
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library}"
if not dryrun:
logger(msg, 5)
playback_position_payload = {
"PlaybackPositionTicks": movie_status["time"]
* 10_000,
}
self.query(
f"/Users/{user_id}/Items/{jellyfin_video_id}/UserData",
"post",
json=playback_position_payload,
)
else:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_video.get("Name"),
duration=floor(movie_status["time"] / 60_000),
)
else:
logger(
f"{self.server_type}: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}",
3,
)
# TV Shows
if videos_shows_ids and videos_episodes_ids:
jellyfin_search = self.query(
f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series",
"get",
)
jellyfin_shows = [x for x in jellyfin_search["Items"]]
for jellyfin_show in jellyfin_shows:
show_found = False
episode_videos = []
if generate_locations:
if "Path" in jellyfin_show:
if (
contains_nested(
jellyfin_show["Path"].split("/")[-1],
videos_shows_ids["locations"],
)
is not None
):
show_found = True
for shows, episodes in videos.items():
show = {k: v for k, v in shows}
if (
contains_nested(
jellyfin_show["Path"].split("/")[-1],
show["locations"],
)
is not None
):
for episode in episodes:
episode_videos.append(episode)
break
if generate_guids:
if not show_found:
for show_provider_source, show_provider_id in jellyfin_show[
"ProviderIds"
].items():
if show_provider_source.lower() in videos_shows_ids:
if (
show_provider_id.lower()
in videos_shows_ids[
show_provider_source.lower()
]
):
show_found = True
for show, episodes in videos.items():
show = {k: v for k, v in show}
if show_provider_id.lower() in show.get(
show_provider_source.lower(), []
):
for episode in episodes:
episode_videos.append(episode)
break
if show_found:
logger(
f"{self.server_type}: Updating watched for {user_name} in library {library} for show {jellyfin_show.get('Name')}",
1,
)
jellyfin_show_id = jellyfin_show["Id"]
jellyfin_episodes = self.query(
f"/Shows/{jellyfin_show_id}/Episodes"
+ f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources",
"get",
)
for jellyfin_episode in jellyfin_episodes["Items"]:
episode_status = get_video_status(
jellyfin_episode, videos_episodes_ids, episode_videos
)
if episode_status:
jellyfin_episode_id = jellyfin_episode["Id"]
if episode_status["completed"]:
msg = (
f"{self.server_type}: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
+ f" as watched for {user_name} in {library}"
)
if not dryrun:
logger(msg, 5)
self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
"post",
)
else:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"),
)
elif update_partial:
msg = (
f"{self.server_type}: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
+ f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library}"
)
if not dryrun:
logger(msg, 5)
playback_position_payload = {
"PlaybackPositionTicks": episode_status[
"time"
]
* 10_000,
}
self.query(
f"/Users/{user_id}/Items/{jellyfin_episode_id}/UserData",
"post",
json=playback_position_payload,
)
else:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"),
duration=floor(episode_status["time"] / 60_000),
)
else:
logger(
f"{self.server_type}: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}",
3,
)
else:
logger(
f"{self.server_type}: Skipping show {jellyfin_show.get('Name')} as it is not in mark list for {user_name}",
3,
)
except Exception as e:
logger(
f"{self.server_type}: Error updating watched for {user_name} in library {library}, {e}",
2,
)
logger(traceback.format_exc(), 2)
raise Exception(e)
def update_watched(
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
):
try:
server_version = self.get_server_version()
update_partial = self.is_partial_update_supported(server_version)
if not update_partial:
logger(
f"{self.server_type}: Server version {server_version} does not support updating playback position.",
2,
)
for user, libraries in watched_list.items():
logger(f"{self.server_type}: Updating for entry {user}, {libraries}", 1)
user_other = None
user_name = None
if user_mapping:
if user in user_mapping.keys():
user_other = user_mapping[user]
elif user in user_mapping.values():
user_other = search_mapping(user_mapping, user)
user_id = None
for key in self.users:
if user.lower() == key.lower():
user_id = self.users[key]
user_name = key
break
elif user_other and user_other.lower() == key.lower():
user_id = self.users[key]
user_name = key
break
if not user_id:
logger(f"{user} {user_other} not found in Jellyfin", 2)
continue
jellyfin_libraries = self.query(
f"/Users/{user_id}/Views",
"get",
)
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
for library, videos in libraries.items():
library_other = None
if library_mapping:
if library in library_mapping.keys():
library_other = library_mapping[library]
elif library in library_mapping.values():
library_other = search_mapping(library_mapping, library)
if library.lower() not in [
x["Name"].lower() for x in jellyfin_libraries
]:
if library_other:
if library_other.lower() in [
x["Name"].lower() for x in jellyfin_libraries
]:
logger(
f"{self.server_type}: Library {library} not found, but {library_other} found, using {library_other}",
1,
)
library = library_other
else:
logger(
f"{self.server_type}: Library {library} or {library_other} not found in library list",
1,
)
continue
else:
logger(
f"{self.server_type}: Library {library} not found in library list",
1,
)
continue
library_id = None
for jellyfin_library in jellyfin_libraries:
if jellyfin_library["Name"] == library:
library_id = jellyfin_library["Id"]
continue
if library_id:
self.update_user_watched(
user_name,
user_id,
library,
library_id,
videos,
update_partial,
dryrun,
)
except Exception as e:
logger(f"{self.server_type}: Error updating watched, {e}", 2)
raise Exception(e)

View File

@@ -1,5 +1,6 @@
from src.functions import ( from src.functions import (
logger, logger,
match_list,
search_mapping, search_mapping,
) )
@@ -129,6 +130,77 @@ def check_whitelist_logic(
return skip_reason return skip_reason
def filter_libaries(
server_libraries,
blacklist_library,
blacklist_library_type,
whitelist_library,
whitelist_library_type,
library_mapping=None,
):
filtered_libaries = []
for library in server_libraries:
skip_reason = check_skip_logic(
library,
server_libraries[library],
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
if skip_reason:
logger(f"Skipping library {library}: {skip_reason}", 1)
continue
filtered_libaries.append(library)
return filtered_libaries
def setup_libraries(
server_1,
server_2,
blacklist_library,
blacklist_library_type,
whitelist_library,
whitelist_library_type,
library_mapping=None,
):
server_1_libraries = server_1.get_libraries()
server_2_libraries = server_2.get_libraries()
logger(f"Server 1 libraries: {server_1_libraries}", 1)
logger(f"Server 2 libraries: {server_2_libraries}", 1)
# Filter out all blacklist, whitelist libaries
filtered_server_1_libraries = filter_libaries(
server_1_libraries,
blacklist_library,
blacklist_library_type,
whitelist_library,
whitelist_library_type,
library_mapping,
)
filtered_server_2_libraries = filter_libaries(
server_2_libraries,
blacklist_library,
blacklist_library_type,
whitelist_library,
whitelist_library_type,
library_mapping,
)
output_server_1_libaries = match_list(
filtered_server_1_libraries, filtered_server_2_libraries, library_mapping
)
output_server_2_libaries = match_list(
filtered_server_2_libraries, filtered_server_1_libraries, library_mapping
)
return output_server_1_libaries, output_server_2_libaries
def show_title_dict(user_list: dict): def show_title_dict(user_list: dict):
try: try:
show_output_dict = {} show_output_dict = {}
@@ -158,7 +230,6 @@ def show_title_dict(user_list: dict):
return show_output_dict return show_output_dict
except Exception: except Exception:
logger("Skipping show_output_dict ", 1)
return {} return {}
@@ -169,67 +240,56 @@ def episode_title_dict(user_list: dict):
episode_output_dict["time"] = [] episode_output_dict["time"] = []
episode_output_dict["locations"] = [] episode_output_dict["locations"] = []
episode_output_dict["show"] = [] episode_output_dict["show"] = []
episode_output_dict["season"] = []
episode_counter = 0 # Initialize a counter for the current episode position episode_counter = 0 # Initialize a counter for the current episode position
# Iterate through the shows, seasons, and episodes in user_list # Iterate through the shows and episodes in user_list
for show 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 for episode in user_list[show]:
if "season" not in episode_output_dict: # Add the show title to the episode_output_dict if it doesn't exist
episode_output_dict["season"] = [None] * episode_counter if "show" not in episode_output_dict:
episode_output_dict["show"] = [None] * episode_counter
# Add the show title to the episode_output_dict # Add the show title to the episode_output_dict
episode_output_dict["show"].append(dict(show)) episode_output_dict["show"].append(dict(show))
# Add the season number to the episode_output_dict # Iterate through the keys and values in each episode
episode_output_dict["season"].append(season) 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
# Iterate through the keys and values in each episode # If the key is "locations", append each location to the list
for episode_key, episode_value in episode.items(): if episode_key == "locations":
# If the key is not "status", add the key to episode_output_dict if it doesn't exist episode_output_dict[episode_key.lower()].append(episode_value)
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 the key is "status", append the "completed" and "time" values
if episode_key == "locations": elif episode_key == "status":
episode_output_dict[episode_key.lower()].append( episode_output_dict["completed"].append(
episode_value episode_value["completed"]
) )
episode_output_dict["time"].append(episode_value["time"])
# If the key is "status", append the "completed" and "time" values # For other keys, append the value to the list
elif episode_key == "status": else:
episode_output_dict["completed"].append( episode_output_dict[episode_key.lower()].append(
episode_value["completed"] episode_value.lower()
) )
episode_output_dict["time"].append(episode_value["time"])
# For other keys, append the value to the list # Increment the episode_counter
else: episode_counter += 1
episode_output_dict[episode_key.lower()].append(
episode_value.lower()
)
# Increment the episode_counter # Extend the lists in episode_output_dict with None values to match the current episode_counter
episode_counter += 1 for key in episode_output_dict:
if len(episode_output_dict[key]) < episode_counter:
# Extend the lists in episode_output_dict with None values to match the current episode_counter episode_output_dict[key].append(None)
for key in episode_output_dict:
if len(episode_output_dict[key]) < episode_counter:
episode_output_dict[key].append(None)
return episode_output_dict return episode_output_dict
except Exception: except Exception:
logger("Skipping episode_output_dict", 1)
return {} return {}
@@ -262,7 +322,6 @@ def movies_title_dict(user_list: dict):
return movies_output_dict return movies_output_dict
except Exception: except Exception:
logger("Skipping movies_output_dict failed", 1)
return {} return {}

View File

@@ -2,265 +2,91 @@ import os, traceback, json
from dotenv import load_dotenv from dotenv import load_dotenv
from time import sleep, perf_counter from time import sleep, perf_counter
from src.library import setup_libraries
from src.functions import ( from src.functions import (
logger, logger,
str_to_bool, str_to_bool,
) )
from src.users import ( from src.users import setup_users
generate_user_list,
combine_user_lists,
filter_user_lists,
generate_server_users,
)
from src.watched import ( from src.watched import (
cleanup_watched, cleanup_watched,
) )
from src.black_white import setup_black_white_lists from src.black_white import setup_black_white_lists
from src.connection import generate_server_connections
from src.plex import Plex
from src.jellyfin import Jellyfin
load_dotenv(override=True) load_dotenv(override=True)
def setup_users(
server_1, server_2, blacklist_users, whitelist_users, user_mapping=None
):
server_1_users = generate_user_list(server_1)
server_2_users = generate_user_list(server_2)
logger(f"Server 1 users: {server_1_users}", 1)
logger(f"Server 2 users: {server_2_users}", 1)
users = combine_user_lists(server_1_users, server_2_users, user_mapping)
logger(f"User list that exist on both servers {users}", 1)
users_filtered = filter_user_lists(users, blacklist_users, whitelist_users)
logger(f"Filtered user list {users_filtered}", 1)
output_server_1_users = generate_server_users(server_1, users_filtered)
output_server_2_users = generate_server_users(server_2, users_filtered)
# Check if users is none or empty
if output_server_1_users is None or len(output_server_1_users) == 0:
logger(
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 output_server_2_users is None or len(output_server_2_users) == 0:
logger(
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}"
)
if (
output_server_1_users is None
or len(output_server_1_users) == 0
or output_server_2_users is None
or len(output_server_2_users) == 0
):
raise Exception("No users found for one or both servers")
logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1)
return output_server_1_users, output_server_2_users
def generate_server_connections():
servers = []
plex_baseurl = os.getenv("PLEX_BASEURL", None)
plex_token = os.getenv("PLEX_TOKEN", None)
plex_username = os.getenv("PLEX_USERNAME", None)
plex_password = os.getenv("PLEX_PASSWORD", None)
plex_servername = os.getenv("PLEX_SERVERNAME", None)
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
if plex_baseurl and plex_token:
plex_baseurl = plex_baseurl.split(",")
plex_token = plex_token.split(",")
if len(plex_baseurl) != len(plex_token):
raise Exception(
"PLEX_BASEURL and PLEX_TOKEN must have the same number of entries"
)
for i, url in enumerate(plex_baseurl):
server = Plex(
baseurl=url.strip(),
token=plex_token[i].strip(),
username=None,
password=None,
servername=None,
ssl_bypass=ssl_bypass,
)
logger(f"Plex Server {i} info: {server.info()}", 3)
servers.append(
(
"plex",
server,
)
)
if plex_username and plex_password and plex_servername:
plex_username = plex_username.split(",")
plex_password = plex_password.split(",")
plex_servername = plex_servername.split(",")
if len(plex_username) != len(plex_password) or len(plex_username) != len(
plex_servername
):
raise Exception(
"PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries"
)
for i, username in enumerate(plex_username):
server = Plex(
baseurl=None,
token=None,
username=username.strip(),
password=plex_password[i].strip(),
servername=plex_servername[i].strip(),
ssl_bypass=ssl_bypass,
)
logger(f"Plex Server {i} info: {server.info()}", 3)
servers.append(
(
"plex",
server,
)
)
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
if jellyfin_baseurl and jellyfin_token:
jellyfin_baseurl = jellyfin_baseurl.split(",")
jellyfin_token = jellyfin_token.split(",")
if len(jellyfin_baseurl) != len(jellyfin_token):
raise Exception(
"JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries"
)
for i, baseurl in enumerate(jellyfin_baseurl):
baseurl = baseurl.strip()
if baseurl[-1] == "/":
baseurl = baseurl[:-1]
server = Jellyfin(baseurl=baseurl, token=jellyfin_token[i].strip())
logger(f"Jellyfin Server {i} info: {server.info()}", 3)
servers.append(
(
"jellyfin",
server,
)
)
return servers
def get_server_watched(
server_connection: list,
users: dict,
blacklist_library: list,
whitelist_library: list,
blacklist_library_type: list,
whitelist_library_type: list,
library_mapping: dict,
):
if server_connection[0] == "plex":
return server_connection[1].get_watched(
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
elif server_connection[0] == "jellyfin":
return server_connection[1].get_watched(
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
def update_server_watched(
server_connection: list,
server_watched_filtered: dict,
user_mapping: dict,
library_mapping: dict,
dryrun: bool,
):
if server_connection[0] == "plex":
server_connection[1].update_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun
)
elif server_connection[0] == "jellyfin":
server_connection[1].update_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun
)
def should_sync_server(server_1_type, server_2_type): def should_sync_server(server_1_type, server_2_type):
sync_from_plex_to_jellyfin = str_to_bool( sync_from_plex_to_jellyfin = str_to_bool(
os.getenv("SYNC_FROM_PLEX_TO_JELLYFIN", "True") os.getenv("SYNC_FROM_PLEX_TO_JELLYFIN", "True")
) )
sync_from_plex_to_plex = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_PLEX", "True"))
sync_from_plex_to_emby = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_EMBY", "True"))
sync_from_jelly_to_plex = str_to_bool( sync_from_jelly_to_plex = str_to_bool(
os.getenv("SYNC_FROM_JELLYFIN_TO_PLEX", "True") os.getenv("SYNC_FROM_JELLYFIN_TO_PLEX", "True")
) )
sync_from_plex_to_plex = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_PLEX", "True"))
sync_from_jelly_to_jellyfin = str_to_bool( sync_from_jelly_to_jellyfin = str_to_bool(
os.getenv("SYNC_FROM_JELLYFIN_TO_JELLYFIN", "True") os.getenv("SYNC_FROM_JELLYFIN_TO_JELLYFIN", "True")
) )
sync_from_jelly_to_emby = str_to_bool(
os.getenv("SYNC_FROM_JELLYFIN_TO_EMBY", "True")
)
if ( sync_from_emby_to_plex = str_to_bool(os.getenv("SYNC_FROM_EMBY_TO_PLEX", "True"))
server_1_type == "plex" sync_from_emby_to_jellyfin = str_to_bool(
and server_2_type == "plex" os.getenv("SYNC_FROM_EMBY_TO_JELLYFIN", "True")
and not sync_from_plex_to_plex )
): sync_from_emby_to_emby = str_to_bool(os.getenv("SYNC_FROM_EMBY_TO_EMBY", "True"))
logger("Sync between plex and plex is disabled", 1)
return False
if ( if server_1_type == "plex":
server_1_type == "plex" if server_2_type == "jellyfin" and not sync_from_plex_to_jellyfin:
and server_2_type == "jellyfin" logger("Sync from plex -> jellyfin is disabled", 1)
and not sync_from_jelly_to_plex return False
):
logger("Sync from jellyfin to plex disabled", 1)
return False
if ( if server_2_type == "emby" and not sync_from_plex_to_emby:
server_1_type == "jellyfin" logger("Sync from plex -> emby is disabled", 1)
and server_2_type == "jellyfin" return False
and not sync_from_jelly_to_jellyfin
):
logger("Sync between jellyfin and jellyfin is disabled", 1)
return False
if ( if server_2_type == "plex" and not sync_from_plex_to_plex:
server_1_type == "jellyfin" logger("Sync from plex -> plex is disabled", 1)
and server_2_type == "plex" return False
and not sync_from_plex_to_jellyfin
): if server_1_type == "jellyfin":
logger("Sync from plex to jellyfin is disabled", 1) if server_2_type == "plex" and not sync_from_jelly_to_plex:
return False logger("Sync from jellyfin -> plex is disabled", 1)
return False
if server_2_type == "jellyfin" and not sync_from_jelly_to_jellyfin:
logger("Sync from jellyfin -> jellyfin is disabled", 1)
return False
if server_2_type == "emby" and not sync_from_jelly_to_emby:
logger("Sync from jellyfin -> emby is disabled", 1)
return False
if server_1_type == "emby":
if server_2_type == "plex" and not sync_from_emby_to_plex:
logger("Sync from emby -> plex is disabled", 1)
return False
if server_2_type == "jellyfin" and not sync_from_emby_to_jellyfin:
logger("Sync from emby -> jellyfin is disabled", 1)
return False
if server_2_type == "emby" and not sync_from_emby_to_emby:
logger("Sync from emby -> emby is disabled", 1)
return False
return True return True
def main_loop(): def main_loop():
logfile = os.getenv("LOGFILE", "log.log") log_file = os.getenv("LOG_FILE", os.getenv("LOGFILE", "log.log"))
# Delete logfile if it exists # Delete log_file if it exists
if os.path.exists(logfile): if os.path.exists(log_file):
os.remove(logfile) os.remove(log_file)
dryrun = str_to_bool(os.getenv("DRYRUN", "False")) dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
logger(f"Dryrun: {dryrun}", 1) logger(f"Dryrun: {dryrun}", 1)
@@ -313,6 +139,12 @@ def main_loop():
# Start server_2 at the next server in the list # Start server_2 at the next server in the list
for server_2 in servers[servers.index(server_1) + 1 :]: for server_2 in servers[servers.index(server_1) + 1 :]:
# Check if server 1 and server 2 are going to be synced in either direction, skip if not
if not should_sync_server(
server_1[0], server_2[0]
) and not should_sync_server(server_2[0], server_1[0]):
continue
logger(f"Server 1: {server_1[0].capitalize()}: {server_1[1].info()}", 0) logger(f"Server 1: {server_1[0].capitalize()}: {server_1[1].info()}", 0)
logger(f"Server 2: {server_2[0].capitalize()}: {server_2[1].info()}", 0) logger(f"Server 2: {server_2[0].capitalize()}: {server_2[1].info()}", 0)
@@ -322,27 +154,27 @@ def main_loop():
server_1, server_2, blacklist_users, whitelist_users, user_mapping server_1, server_2, blacklist_users, whitelist_users, user_mapping
) )
logger("Creating watched lists", 1) server_1_libraries, server_2_libraries = setup_libraries(
server_1_watched = get_server_watched( server_1[1],
server_1, server_2[1],
server_1_users,
blacklist_library, blacklist_library,
whitelist_library,
blacklist_library_type, blacklist_library_type,
whitelist_library,
whitelist_library_type, whitelist_library_type,
library_mapping, library_mapping,
) )
logger("Creating watched lists", 1)
server_1_watched = server_1[1].get_watched(
server_1_users, server_1_libraries
)
logger("Finished creating watched list server 1", 1) logger("Finished creating watched list server 1", 1)
server_2_watched = get_server_watched(
server_2, server_2_watched = server_2[1].get_watched(
server_2_users, server_2_users, server_2_libraries
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
) )
logger("Finished creating watched list server 2", 1) logger("Finished creating watched list server 2", 1)
logger(f"Server 1 watched: {server_1_watched}", 3) logger(f"Server 1 watched: {server_1_watched}", 3)
logger(f"Server 2 watched: {server_2_watched}", 3) logger(f"Server 2 watched: {server_2_watched}", 3)
@@ -365,18 +197,18 @@ def main_loop():
1, 1,
) )
if should_sync_server(server_1[0], server_2[0]): if should_sync_server(server_2[0], server_1[0]):
update_server_watched( logger(f"Syncing {server_2[1].info()} -> {server_1[1].info()}", 0)
server_1, server_1[1].update_watched(
server_2_watched_filtered, server_2_watched_filtered,
user_mapping, user_mapping,
library_mapping, library_mapping,
dryrun, dryrun,
) )
if should_sync_server(server_2[0], server_1[0]): if should_sync_server(server_1[0], server_2[0]):
update_server_watched( logger(f"Syncing {server_1[1].info()} -> {server_2[1].info()}", 0)
server_2, server_2[1].update_watched(
server_1_watched_filtered, server_1_watched_filtered,
user_mapping, user_mapping,
library_mapping, library_mapping,

View File

@@ -19,10 +19,7 @@ from src.functions import (
log_marked, log_marked,
str_to_bool, str_to_bool,
) )
from src.library import ( from src.library import generate_library_guids_dict
check_skip_logic,
generate_library_guids_dict,
)
load_dotenv(override=True) load_dotenv(override=True)
@@ -117,11 +114,9 @@ def get_user_library_watched_show(show, process_episodes, threads=None):
episode_guids_args, threads=threads episode_guids_args, threads=threads
) )
episode_guids = {} episode_guids = []
for index, episode in enumerate(process_episodes): for index, episode in enumerate(process_episodes):
if episode.parentIndex not in episode_guids: episode_guids.append(episode_guids_results[index])
episode_guids[episode.parentIndex] = []
episode_guids[episode.parentIndex].append(episode_guids_results[index])
return show_guids, episode_guids return show_guids, episode_guids
except Exception: except Exception:
@@ -188,7 +183,7 @@ def get_user_library_watched(user, user_plex, library):
if show_guids and episode_guids: if show_guids and episode_guids:
watched[show_guids] = episode_guids watched[show_guids] = episode_guids
logger( logger(
f"Plex: Added {episode_guids} to {user_name} {show_guids} watched list", f"Plex: Added {episode_guids} to {user_name} watched list",
3, 3,
) )
@@ -220,7 +215,7 @@ def find_video(plex_search, video_ids, videos=None):
): ):
episode_videos = [] episode_videos = []
if videos: if videos:
for show, seasons in videos.items(): for show, episodes in videos.items():
show = {k: v for k, v in show} show = {k: v for k, v in show}
if ( if (
contains_nested( contains_nested(
@@ -228,9 +223,8 @@ def find_video(plex_search, video_ids, videos=None):
) )
is not None is not None
): ):
for season in seasons.values(): for episode in episodes:
for episode in season: episode_videos.append(episode)
episode_videos.append(episode)
return True, episode_videos return True, episode_videos
@@ -243,13 +237,12 @@ def find_video(plex_search, video_ids, videos=None):
if guid_id in video_ids[guid_source]: if guid_id in video_ids[guid_source]:
episode_videos = [] episode_videos = []
if videos: if videos:
for show, seasons in videos.items(): for show, episodes in videos.items():
show = {k: v for k, v in show} show = {k: v for k, v in show}
if guid_source in show.keys(): if guid_source in show.keys():
if guid_id == show[guid_source]: if guid_id == show[guid_source]:
for season in seasons.values(): for episode in episodes:
for episode in season: episode_videos.append(episode)
episode_videos.append(episode)
return True, episode_videos return True, episode_videos
@@ -321,7 +314,15 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
else: else:
logger(msg, 6) logger(msg, 6)
log_marked(user.title, library, movies_search.title, None, None) log_marked(
"Plex",
user_plex.friendlyName,
user.title,
library,
movies_search.title,
None,
None,
)
elif video_status["time"] > 60_000: 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}" 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: if not dryrun:
@@ -331,6 +332,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
"Plex",
user_plex.friendlyName,
user.title, user.title,
library, library,
movies_search.title, movies_search.title,
@@ -362,6 +365,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
"Plex",
user_plex.friendlyName,
user.title, user.title,
library, library,
show_search.title, show_search.title,
@@ -376,6 +381,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
"Plex",
user_plex.friendlyName,
user.title, user.title,
library, library,
show_search.title, show_search.title,
@@ -456,7 +463,7 @@ class Plex:
raise Exception(e) raise Exception(e)
def info(self) -> str: def info(self) -> str:
return f"{self.plex.friendlyName}: {self.plex.version}" return f"Plex {self.plex.friendlyName}: {self.plex.version}"
def get_users(self): def get_users(self):
try: try:
@@ -470,15 +477,24 @@ class Plex:
logger(f"Plex: Failed to get users, Error: {e}", 2) logger(f"Plex: Failed to get users, Error: {e}", 2)
raise Exception(e) raise Exception(e)
def get_watched( def get_libraries(self):
self, try:
users, output = {}
blacklist_library,
whitelist_library, libraries = self.plex.library.sections()
blacklist_library_type,
whitelist_library_type, for library in libraries:
library_mapping, library_title = library.title
): library_type = library.type
output[library_title] = library_type
return output
except Exception as e:
logger(f"Plex: Failed to get libraries, Error: {e}", 2)
raise Exception(e)
def get_watched(self, users, sync_libraries):
try: try:
# Get all libraries # Get all libraries
users_watched = {} users_watched = {}
@@ -504,23 +520,7 @@ class Plex:
libraries = user_plex.library.sections() libraries = user_plex.library.sections()
for library in libraries: for library in libraries:
library_title = library.title if library.title not in sync_libraries:
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 continue
user_watched = get_user_library_watched(user, user_plex, library) user_watched = get_user_library_watched(user, user_plex, library)

View File

@@ -16,7 +16,7 @@ def generate_user_list(server):
user.username.lower() if user.username else user.title.lower() user.username.lower() if user.username else user.title.lower()
) )
elif server_type == "jellyfin": elif server_type in ["jellyfin", "emby"]:
server_users = [key.lower() for key in server_connection.users.keys()] server_users = [key.lower() for key in server_connection.users.keys()]
return server_users return server_users
@@ -79,7 +79,7 @@ def generate_server_users(server, users):
or username_title.lower() in users.values() or username_title.lower() in users.values()
): ):
server_users.append(plex_user) server_users.append(plex_user)
elif server[0] == "jellyfin": elif server[0] in ["jellyfin", "emby"]:
server_users = {} server_users = {}
for jellyfin_user, jellyfin_id in server[1].users.items(): for jellyfin_user, jellyfin_id in server[1].users.items():
if ( if (
@@ -89,3 +89,45 @@ def generate_server_users(server, users):
server_users[jellyfin_user] = jellyfin_id server_users[jellyfin_user] = jellyfin_id
return server_users return server_users
def setup_users(
server_1, server_2, blacklist_users, whitelist_users, user_mapping=None
):
server_1_users = generate_user_list(server_1)
server_2_users = generate_user_list(server_2)
logger(f"Server 1 users: {server_1_users}", 1)
logger(f"Server 2 users: {server_2_users}", 1)
users = combine_user_lists(server_1_users, server_2_users, user_mapping)
logger(f"User list that exist on both servers {users}", 1)
users_filtered = filter_user_lists(users, blacklist_users, whitelist_users)
logger(f"Filtered user list {users_filtered}", 1)
output_server_1_users = generate_server_users(server_1, users_filtered)
output_server_2_users = generate_server_users(server_2, users_filtered)
# Check if users is none or empty
if output_server_1_users is None or len(output_server_1_users) == 0:
logger(
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 output_server_2_users is None or len(output_server_2_users) == 0:
logger(
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}"
)
if (
output_server_1_users is None
or len(output_server_1_users) == 0
or output_server_2_users is None
or len(output_server_2_users) == 0
):
raise Exception("No users found for one or both servers")
logger(f"Server 1 users: {output_server_1_users}", 1)
logger(f"Server 2 users: {output_server_2_users}", 1)
return output_server_1_users, output_server_2_users

View File

@@ -5,33 +5,6 @@ from src.functions import logger, search_mapping, contains_nested
from src.library import generate_library_guids_dict 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): def check_remove_entry(video, library, video_index, library_watched_list_2):
if video_index is not None: if video_index is not None:
if ( if (
@@ -122,53 +95,27 @@ def cleanup_watched(
for show_key_1 in watched_list_1[user_1][library_1].keys(): for show_key_1 in watched_list_1[user_1][library_1].keys():
show_key_dict = dict(show_key_1) 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
# Filter the episode_watched_list_2_keys_dict dictionary to handle cases # where episode location names are not unique such as S01E01.mkv
# where episode location names are not unique such as S01E01.mkv filtered_episode_watched_list_2_keys_dict = (
filtered_episode_watched_list_2_keys_dict = ( filter_episode_watched_list_2_keys_dict(
filter_episode_watched_list_2_keys_dict( episode_watched_list_2_keys_dict, show_key_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 for episode in watched_list_1[user_1][library_1][show_key_1]:
]: episode_index = get_episode_index_in_dict(
episode_index = get_episode_index_in_dict( episode, filtered_episode_watched_list_2_keys_dict
episode, filtered_episode_watched_list_2_keys_dict )
) if episode_index is not None:
if episode_index is not None: if check_remove_entry(
if check_remove_entry( episode,
episode, library_1,
library_1, episode_index,
episode_index, episode_watched_list_2_keys_dict,
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( modified_watched_list_1[user_1][library_1][
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 show_key_1
][season] ].remove(episode)
# Remove empty shows # Remove empty shows
if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0: if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0:
@@ -231,27 +178,18 @@ def get_movie_index_in_dict(movie, movies_watched_list_2_keys_dict):
def filter_episode_watched_list_2_keys_dict( def filter_episode_watched_list_2_keys_dict(
episode_watched_list_2_keys_dict, show_key_dict, season episode_watched_list_2_keys_dict, show_key_dict
): ):
# If the episode_watched_list_2_keys_dict dictionary is empty, missing season or show then return an empty dictionary # If the episode_watched_list_2_keys_dict dictionary is empty, missing show then return an empty dictionary
if ( if (
len(episode_watched_list_2_keys_dict) == 0 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() or "show" not in episode_watched_list_2_keys_dict.keys()
): ):
return {} return {}
# Filter the episode_watched_list_2_keys_dict dictionary to only include values for the correct show and season # Filter the episode_watched_list_2_keys_dict dictionary to only include values for the correct show
filtered_episode_watched_list_2_keys_dict = {} filtered_episode_watched_list_2_keys_dict = {}
show_indecies = [] 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 # 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"]): for show_index, show_value in enumerate(episode_watched_list_2_keys_dict["show"]):
@@ -273,14 +211,14 @@ def filter_episode_watched_list_2_keys_dict(
show_indecies.append(show_index) show_indecies.append(show_index)
break break
# Find the intersection of the show_indecies and season_indecies lists # lists
indecies = list(set(show_indecies) & set(season_indecies)) indecies = list(set(show_indecies))
# If there are no indecies that match the show and season, return an empty dictionary # If there are no indecies that match the show, return an empty dictionary
if len(indecies) == 0: if len(indecies) == 0:
return {} return {}
# Create a copy of the dictionary with indecies that match the show and season and none that don't # Create a copy of the dictionary with indecies that match the show and none that don't
for key, value in episode_watched_list_2_keys_dict.items(): for key, value in episode_watched_list_2_keys_dict.items():
if key not in filtered_episode_watched_list_2_keys_dict: if key not in filtered_episode_watched_list_2_keys_dict:
filtered_episode_watched_list_2_keys_dict[key] = [] filtered_episode_watched_list_2_keys_dict[key] = []

116
test/ci_emby.env Normal file
View File

@@ -0,0 +1,116 @@
# 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 = "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 = "http://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 = "6S28yhwKg4y-vAXYMi1c"
## 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"
# 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"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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 = "False"
SYNC_FROM_PLEX_TO_PLEX = "False"
SYNC_FROM_PLEX_TO_EMBY = "False"
SYNC_FROM_JELLYFIN_TO_PLEX = "False"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "False"
SYNC_FROM_JELLYFIN_TO_EMBY = "False"
SYNC_FROM_EMBY_TO_PLEX = "True"
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
SYNC_FROM_EMBY_TO_EMBY = "True"

View File

@@ -62,11 +62,11 @@ WHITELIST_USERS = "jellyplex_watched"
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_BASEURL = "https://localhost:32400" PLEX_BASEURL = "http://localhost:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ ## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y" PLEX_TOKEN = "6S28yhwKg4y-vAXYMi1c"
## If not using plex token then use username and password of the server admin along with the servername ## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options ## Comma seperated for multiple options
@@ -78,13 +78,6 @@ PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
## Set to True if running into ssl certificate errors ## Set to True if running into ssl certificate errors
SSL_BYPASS = "True" 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
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
@@ -94,3 +87,30 @@ JELLYFIN_BASEURL = "http://localhost:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key ## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c" JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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_PLEX_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_EMBY = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_EMBY = "True"
SYNC_FROM_EMBY_TO_PLEX = "True"
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
SYNC_FROM_EMBY_TO_EMBY = "True"

116
test/ci_jellyfin.env Normal file
View File

@@ -0,0 +1,116 @@
# 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 = "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 = "http://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 = "6S28yhwKg4y-vAXYMi1c"
## 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"
# 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"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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 = "False"
SYNC_FROM_PLEX_TO_PLEX = "False"
SYNC_FROM_PLEX_TO_EMBY = "False"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_EMBY = "True"
SYNC_FROM_EMBY_TO_PLEX = "False"
SYNC_FROM_EMBY_TO_JELLYFIN = "False"
SYNC_FROM_EMBY_TO_EMBY = "False"

View File

@@ -62,11 +62,11 @@ WHITELIST_USERS = "jellyplex_watched"
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_BASEURL = "https://localhost:32400" PLEX_BASEURL = "http://localhost:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ ## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y" PLEX_TOKEN = "6S28yhwKg4y-vAXYMi1c"
## If not using plex token then use username and password of the server admin along with the servername ## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options ## Comma seperated for multiple options
@@ -78,13 +78,6 @@ PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
## Set to True if running into ssl certificate errors ## Set to True if running into ssl certificate errors
SSL_BYPASS = "True" 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
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
@@ -94,3 +87,30 @@ JELLYFIN_BASEURL = "http://localhost:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key ## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c" JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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_PLEX_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_EMBY = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_EMBY = "True"
SYNC_FROM_EMBY_TO_PLEX = "True"
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
SYNC_FROM_EMBY_TO_EMBY = "True"

116
test/ci_plex.env Normal file
View File

@@ -0,0 +1,116 @@
# 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 = "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 = "http://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 = "6S28yhwKg4y-vAXYMi1c"
## 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"
# 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"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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_PLEX_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_EMBY = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "False"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "False"
SYNC_FROM_JELLYFIN_TO_EMBY = "False"
SYNC_FROM_EMBY_TO_PLEX = "False"
SYNC_FROM_EMBY_TO_JELLYFIN = "False"
SYNC_FROM_EMBY_TO_EMBY = "False"

View File

@@ -62,11 +62,11 @@ WHITELIST_USERS = "jellyplex_watched"
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_BASEURL = "https://localhost:32400" PLEX_BASEURL = "http://localhost:32400"
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ ## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y" PLEX_TOKEN = "6S28yhwKg4y-vAXYMi1c"
## If not using plex token then use username and password of the server admin along with the servername ## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options ## Comma seperated for multiple options
@@ -78,13 +78,6 @@ PLEX_TOKEN = "mVaCzSyd78uoWkCBzZ_Y"
## Set to True if running into ssl certificate errors ## Set to True if running into ssl certificate errors
SSL_BYPASS = "True" 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
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
@@ -94,3 +87,30 @@ JELLYFIN_BASEURL = "http://localhost:8096"
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key ## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers ## Comma seperated list for multiple servers
JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c" JELLYFIN_TOKEN = "d773c4db3ecc4b028fc0904d9694804c"
# Emby
## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
EMBY_BASEURL = "http://localhost:8097"
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
## Comma seperated list for multiple servers
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
# Syncing Options
## 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_PLEX_TO_PLEX = "True"
SYNC_FROM_PLEX_TO_EMBY = "True"
SYNC_FROM_JELLYFIN_TO_PLEX = "True"
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "True"
SYNC_FROM_JELLYFIN_TO_EMBY = "True"
SYNC_FROM_EMBY_TO_PLEX = "True"
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
SYNC_FROM_EMBY_TO_EMBY = "True"

View File

@@ -42,21 +42,19 @@ show_list = {
("tvdb", "392256"), ("tvdb", "392256"),
("title", "The Last of Us"), ("title", "The Last of Us"),
} }
): { ): [
"Season 1": [ {
{ "imdb": "tt11957006",
"imdb": "tt11957006", "tmdb": "2181581",
"tmdb": "2181581", "tvdb": "8444132",
"tvdb": "8444132", "locations": (
"locations": ( (
( "The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv",
"The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv", )
) ),
), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, }
} ]
]
}
} }
movie_list = [ movie_list = [
{ {
@@ -83,7 +81,6 @@ episode_titles = {
"tvdb": ["8444132"], "tvdb": ["8444132"],
"completed": [True], "completed": [True],
"time": [0], "time": [0],
"season": ["Season 1"],
"show": [ "show": [
{ {
"imdb": "tt3581920", "imdb": "tt3581920",

View File

@@ -13,7 +13,7 @@ parent = os.path.dirname(current)
# the sys.path. # the sys.path.
sys.path.append(parent) sys.path.append(parent)
from src.watched import cleanup_watched, combine_watched_dicts from src.watched import cleanup_watched
tv_shows_watched_list_1 = { tv_shows_watched_list_1 = {
frozenset( frozenset(
@@ -24,34 +24,32 @@ tv_shows_watched_list_1 = {
("tvdb", "78804"), ("tvdb", "78804"),
("title", "Doctor Who (2005)"), ("title", "Doctor Who (2005)"),
} }
): { ): [
1: [ {
{ "imdb": "tt0563001",
"imdb": "tt0563001", "tmdb": "968589",
"tmdb": "968589", "tvdb": "295296",
"tvdb": "295296", "title": "The Unquiet Dead",
"title": "The Unquiet Dead", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt0562985",
"imdb": "tt0562985", "tmdb": "968590",
"tmdb": "968590", "tvdb": "295297",
"tvdb": "295297", "title": "Aliens of London (1)",
"title": "Aliens of London (1)", "locations": ("S01E04.mkv",),
"locations": ("S01E04.mkv",), "status": {"completed": False, "time": 240000},
"status": {"completed": False, "time": 240000}, },
}, {
{ "imdb": "tt0563003",
"imdb": "tt0563003", "tmdb": "968592",
"tmdb": "968592", "tvdb": "295298",
"tvdb": "295298", "title": "World War Three (2)",
"title": "World War Three (2)", "locations": ("S01E05.mkv",),
"locations": ("S01E05.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
frozenset( frozenset(
{ {
("title", "Monarch: Legacy of Monsters"), ("title", "Monarch: Legacy of Monsters"),
@@ -63,34 +61,32 @@ tv_shows_watched_list_1 = {
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",), ("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
), ),
} }
): { ): [
1: [ {
{ "imdb": "tt21255044",
"imdb": "tt21255044", "tmdb": "4661246",
"tmdb": "4661246", "tvdb": "10009418",
"tvdb": "10009418", "title": "Secrets and Lies",
"title": "Secrets and Lies", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt21255050",
"imdb": "tt21255050", "tmdb": "4712059",
"tmdb": "4712059", "tvdb": "10009419",
"tvdb": "10009419", "title": "Parallels and Interiors",
"title": "Parallels and Interiors", "locations": ("S01E04.mkv",),
"locations": ("S01E04.mkv",), "status": {"completed": False, "time": 240000},
"status": {"completed": False, "time": 240000}, },
}, {
{ "imdb": "tt23787572",
"imdb": "tt23787572", "tmdb": "4712061",
"tmdb": "4712061", "tvdb": "10009420",
"tvdb": "10009420", "title": "The Way Out",
"title": "The Way Out", "locations": ("S01E05.mkv",),
"locations": ("S01E05.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
frozenset( frozenset(
{ {
("tmdb", "125928"), ("tmdb", "125928"),
@@ -102,34 +98,32 @@ tv_shows_watched_list_1 = {
), ),
("title", "My Adventures with Superman"), ("title", "My Adventures with Superman"),
} }
): { ): [
1: [ {
{ "imdb": "tt15699926",
"imdb": "tt15699926", "tmdb": "3070048",
"tmdb": "3070048", "tvdb": "8438181",
"tvdb": "8438181", "title": "Adventures of a Normal Man (1)",
"title": "Adventures of a Normal Man (1)", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt20413322",
"imdb": "tt20413322", "tmdb": "4568681",
"tmdb": "4568681", "tvdb": "9829910",
"tvdb": "9829910", "title": "Adventures of a Normal Man (2)",
"title": "Adventures of a Normal Man (2)", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt20413328",
"imdb": "tt20413328", "tmdb": "4497012",
"tmdb": "4497012", "tvdb": "9870382",
"tvdb": "9870382", "title": "My Interview with Superman",
"title": "My Interview with Superman", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
} }
@@ -143,31 +137,29 @@ tv_shows_watched_list_2 = {
("tvdb", "78804"), ("tvdb", "78804"),
("tvrage", "3332"), ("tvrage", "3332"),
} }
): { ): [
1: [ {
{ "tvdb": "295294",
"tvdb": "295294", "imdb": "tt0562992",
"imdb": "tt0562992", "title": "Rose",
"title": "Rose", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "295295",
"tvdb": "295295", "imdb": "tt0562997",
"imdb": "tt0562997", "title": "The End of the World",
"title": "The End of the World", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": False, "time": 300670},
"status": {"completed": False, "time": 300670}, },
}, {
{ "tvdb": "295298",
"tvdb": "295298", "imdb": "tt0563003",
"imdb": "tt0563003", "title": "World War Three (2)",
"title": "World War Three (2)", "locations": ("S01E05.mkv",),
"locations": ("S01E05.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
frozenset( frozenset(
{ {
("title", "Monarch: Legacy of Monsters"), ("title", "Monarch: Legacy of Monsters"),
@@ -179,31 +171,29 @@ tv_shows_watched_list_2 = {
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",), ("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
), ),
} }
): { ): [
1: [ {
{ "tvdb": "9959300",
"tvdb": "9959300", "imdb": "tt20412166",
"imdb": "tt20412166", "title": "Aftermath",
"title": "Aftermath", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "10009417",
"tvdb": "10009417", "imdb": "tt22866594",
"imdb": "tt22866594", "title": "Departure",
"title": "Departure", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": False, "time": 300741},
"status": {"completed": False, "time": 300741}, },
}, {
{ "tvdb": "10009420",
"tvdb": "10009420", "imdb": "tt23787572",
"imdb": "tt23787572", "title": "The Way Out",
"title": "The Way Out", "locations": ("S01E05.mkv",),
"locations": ("S01E05.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
frozenset( frozenset(
{ {
("tmdb", "125928"), ("tmdb", "125928"),
@@ -215,31 +205,29 @@ tv_shows_watched_list_2 = {
), ),
("title", "My Adventures with Superman"), ("title", "My Adventures with Superman"),
} }
): { ): [
1: [ {
{ "tvdb": "8438181",
"tvdb": "8438181", "imdb": "tt15699926",
"imdb": "tt15699926", "title": "Adventures of a Normal Man (1)",
"title": "Adventures of a Normal Man (1)", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "9829910",
"tvdb": "9829910", "imdb": "tt20413322",
"imdb": "tt20413322", "title": "Adventures of a Normal Man (2)",
"title": "Adventures of a Normal Man (2)", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "9870382",
"tvdb": "9870382", "imdb": "tt20413328",
"imdb": "tt20413328", "title": "My Interview with Superman",
"title": "My Interview with Superman", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, ],
]
},
} }
expected_tv_show_watched_list_1 = { expected_tv_show_watched_list_1 = {
@@ -251,26 +239,24 @@ expected_tv_show_watched_list_1 = {
("tvdb", "78804"), ("tvdb", "78804"),
("title", "Doctor Who (2005)"), ("title", "Doctor Who (2005)"),
} }
): { ): [
1: [ {
{ "imdb": "tt0563001",
"imdb": "tt0563001", "tmdb": "968589",
"tmdb": "968589", "tvdb": "295296",
"tvdb": "295296", "title": "The Unquiet Dead",
"title": "The Unquiet Dead", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt0562985",
"imdb": "tt0562985", "tmdb": "968590",
"tmdb": "968590", "tvdb": "295297",
"tvdb": "295297", "title": "Aliens of London (1)",
"title": "Aliens of London (1)", "locations": ("S01E04.mkv",),
"locations": ("S01E04.mkv",), "status": {"completed": False, "time": 240000},
"status": {"completed": False, "time": 240000}, },
}, ],
]
},
frozenset( frozenset(
{ {
("title", "Monarch: Legacy of Monsters"), ("title", "Monarch: Legacy of Monsters"),
@@ -282,26 +268,24 @@ expected_tv_show_watched_list_1 = {
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",), ("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
), ),
} }
): { ): [
1: [ {
{ "imdb": "tt21255044",
"imdb": "tt21255044", "tmdb": "4661246",
"tmdb": "4661246", "tvdb": "10009418",
"tvdb": "10009418", "title": "Secrets and Lies",
"title": "Secrets and Lies", "locations": ("S01E03.mkv",),
"locations": ("S01E03.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "imdb": "tt21255050",
"imdb": "tt21255050", "tmdb": "4712059",
"tmdb": "4712059", "tvdb": "10009419",
"tvdb": "10009419", "title": "Parallels and Interiors",
"title": "Parallels and Interiors", "locations": ("S01E04.mkv",),
"locations": ("S01E04.mkv",), "status": {"completed": False, "time": 240000},
"status": {"completed": False, "time": 240000}, },
}, ],
]
},
} }
expected_tv_show_watched_list_2 = { expected_tv_show_watched_list_2 = {
@@ -314,24 +298,22 @@ expected_tv_show_watched_list_2 = {
("tvdb", "78804"), ("tvdb", "78804"),
("tvrage", "3332"), ("tvrage", "3332"),
} }
): { ): [
1: [ {
{ "tvdb": "295294",
"tvdb": "295294", "imdb": "tt0562992",
"imdb": "tt0562992", "title": "Rose",
"title": "Rose", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "295295",
"tvdb": "295295", "imdb": "tt0562997",
"imdb": "tt0562997", "title": "The End of the World",
"title": "The End of the World", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": False, "time": 300670},
"status": {"completed": False, "time": 300670}, },
}, ],
]
},
frozenset( frozenset(
{ {
("title", "Monarch: Legacy of Monsters"), ("title", "Monarch: Legacy of Monsters"),
@@ -343,24 +325,22 @@ expected_tv_show_watched_list_2 = {
("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",), ("Monarch - Legacy of Monsters {tvdb-422598} {imdb-tt17220216}",),
), ),
} }
): { ): [
1: [ {
{ "tvdb": "9959300",
"tvdb": "9959300", "imdb": "tt20412166",
"imdb": "tt20412166", "title": "Aftermath",
"title": "Aftermath", "locations": ("S01E01.mkv",),
"locations": ("S01E01.mkv",), "status": {"completed": True, "time": 0},
"status": {"completed": True, "time": 0}, },
}, {
{ "tvdb": "10009417",
"tvdb": "10009417", "imdb": "tt22866594",
"imdb": "tt22866594", "title": "Departure",
"title": "Departure", "locations": ("S01E02.mkv",),
"locations": ("S01E02.mkv",), "status": {"completed": False, "time": 300741},
"status": {"completed": False, "time": 300741}, },
}, ],
]
},
} }
movies_watched_list_1 = [ movies_watched_list_1 = [
@@ -463,20 +443,16 @@ tv_shows_2_watched_list_1 = {
("locations", ("Criminal Minds",)), ("locations", ("Criminal Minds",)),
("tmdb", "4057"), ("tmdb", "4057"),
} }
): { ): [
"Season 1": [ {
{ "imdb": "tt0550489",
"imdb": "tt0550489", "tmdb": "282843",
"tmdb": "282843", "tvdb": "176357",
"tvdb": "176357", "title": "Extreme Aggressor",
"title": "Extreme Aggressor", "locations": ("Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",),
"locations": ( "status": {"completed": True, "time": 0},
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", },
), ]
"status": {"completed": True, "time": 0},
},
]
}
} }
@@ -565,120 +541,3 @@ def test_mapping_cleanup_watched():
assert return_watched_list_1 == expected_watched_list_1 assert return_watched_list_1 == expected_watched_list_1
assert return_watched_list_2 == expected_watched_list_2 assert return_watched_list_2 == expected_watched_list_2
def test_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

View File

@@ -1,6 +1,32 @@
# Check the mark.log file that is generated by the CI to make sure it contains the expected values # Check the mark.log file that is generated by the CI to make sure it contains the expected values
import os import os, argparse
def parse_args():
parser = argparse.ArgumentParser(
description="Check the mark.log file that is generated by the CI to make sure it contains the expected values"
)
parser.add_argument(
"--guids", action="store_true", help="Check the mark.log file for guids"
)
parser.add_argument(
"--locations", action="store_true", help="Check the mark.log file for locations"
)
parser.add_argument(
"--write", action="store_true", help="Check the mark.log file for write-run"
)
parser.add_argument(
"--plex", action="store_true", help="Check the mark.log file for Plex"
)
parser.add_argument(
"--jellyfin", action="store_true", help="Check the mark.log file for Jellyfin"
)
parser.add_argument(
"--emby", action="store_true", help="Check the mark.log file for Emby"
)
return parser.parse_args()
def read_marklog(): def read_marklog():
@@ -29,7 +55,8 @@ def check_marklog(lines, expected_values):
+ str(len(found_values)) + str(len(found_values))
+ " values, expected " + " values, expected "
+ str(len(expected_values)) + str(len(expected_values))
+ " values" + " values\n"
+ "\n".join(found_values)
) )
# Check that the two lists contain the same values # Check that the two lists contain the same values
@@ -48,24 +75,133 @@ def check_marklog(lines, expected_values):
def main(): def main():
expected_values = [ args = parse_args()
"jellyplex_watched/Movies/Five Nights at Freddy's", expected_jellyfin = [
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215", "Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Two (2021)",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose", "Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 2",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670", "Plex/JellyPlex-CI/jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath", "Plex/JellyPlex-CI/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"JellyUser/Movies/Big Buck Bunny", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"JellyUser/Shows/Doctor Who/The Unquiet Dead", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"Emby/Emby-Server/jellyplex_watched/Custom Movies/Movie Two",
"Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E02",
"Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"Emby/Emby-Server/jellyplex_watched/Movies/Five Nights at Freddy's",
"Emby/Emby-Server/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
]
expected_emby = [
"Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Three (2022)",
"Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 3",
"Plex/JellyPlex-CI/jellyplex_watched/Movies/Tears of Steel",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429",
"Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie Three (2022)",
"Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E03",
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Tears of Steel",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
]
expected_plex = [
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Big Buck Bunny",
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Killers of the Flower Moon/4",
"Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/The Unquiet Dead",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie One (2020)",
"Emby/Emby-Server/jellyplex_watched/Movies/Big Buck Bunny",
"Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"Emby/Emby-Server/jellyplex_watched/Movies/Killers of the Flower Moon/4",
"Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
"Emby/Emby-Server/jellyplex_watched/Custom Movies/Movie One",
] ]
# Triple the expected values because the CI runs three times expected_locations = expected_emby + expected_plex + expected_jellyfin
expected_values = expected_values * 3 # Remove Custom Movies/TV Shows as they should not have guids
expected_guids = [item for item in expected_locations if "Custom" not in item ]
expected_write = [
"Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Two (2021)",
"Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 2",
"Plex/JellyPlex-CI/jellyplex_watched/Movies/Five Nights at Freddy's",
"Plex/JellyPlex-CI/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Big Buck Bunny",
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Killers of the Flower Moon/4",
"Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/The Unquiet Dead",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie One (2020)",
"Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Three (2022)",
"Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 3",
"Plex/JellyPlex-CI/jellyplex_watched/Movies/Tears of Steel",
"Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429",
"Emby/Emby-Server/jellyplex_watched/Movies/Big Buck Bunny",
"Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"Emby/Emby-Server/jellyplex_watched/Movies/Five Nights at Freddy's",
"Emby/Emby-Server/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5",
"Emby/Emby-Server/jellyplex_watched/Movies/Killers of the Flower Moon/4",
"Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E02",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"Emby/Emby-Server/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
"Emby/Emby-Server/jellyplex_watched/Custom Movies/Movie One",
"Emby/Emby-Server/jellyplex_watched/Custom Movies/Movie Two",
"Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie Three (2022)",
"Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E03",
"Jellyfin/Jellyfin-Server/JellyUser/Movies/Tears of Steel",
"Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4"
]
# Expected values for the mark.log file, dry-run is slightly different than write-run
# due to some of the items being copied over from one server to another and now being there
# for the next server run.
if args.guids:
expected_values = expected_guids
elif args.locations:
expected_values = expected_locations
elif args.write:
expected_values = expected_write
elif args.plex:
expected_values = expected_plex
elif args.jellyfin:
expected_values = expected_jellyfin
elif args.emby:
expected_values = expected_emby
else:
print("No server specified")
exit(1)
lines = read_marklog() lines = read_marklog()
if not check_marklog(lines, expected_values): if not check_marklog(lines, expected_values):
print("Failed to validate marklog") print("Failed to validate marklog")
for line in lines:
# Remove the newline character
line = line.strip()
print(line)
exit(1) exit(1)
print("Successfully validated marklog") print("Successfully validated marklog")