43 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
4a4c9f9ccf Update to python 12 2023-12-06 14:16:47 -07:00
27 changed files with 756 additions and 722 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

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
@@ -46,7 +57,7 @@ jobs:
sleep 10 sleep 10
for FOLDER in $(find "JellyPlex-Watched-CI" -type f -name "docker-compose.yml" -exec dirname {} \;); do for FOLDER in $(find "JellyPlex-Watched-CI" -type f -name "docker-compose.yml" -exec dirname {} \;); do
docker-compose -f "${FOLDER}/docker-compose.yml" logs docker compose -f "${FOLDER}/docker-compose.yml" logs
done done
- name: "Test Plex" - name: "Test Plex"
@@ -77,7 +88,7 @@ jobs:
run: | run: |
mv test/ci_guids.env .env mv test/ci_guids.env .env
python main.py python main.py
python test/validate_ci_marklog.py --dry python test/validate_ci_marklog.py --guids
rm mark.log rm mark.log
@@ -85,7 +96,7 @@ jobs:
run: | run: |
mv test/ci_locations.env .env mv test/ci_locations.env .env
python main.py python main.py
python test/validate_ci_marklog.py --dry python test/validate_ci_marklog.py --locations
rm mark.log rm mark.log
@@ -129,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"]

View File

@@ -107,7 +107,7 @@ Full list of configuration options can be found in the [.env.sample](.env.sample
## 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

View File

@@ -1,4 +1,5 @@
from src.jellyfin_emby import JellyfinEmby from src.jellyfin_emby import JellyfinEmby
from packaging import version
class Emby(JellyfinEmby): class Emby(JellyfinEmby):
@@ -8,7 +9,7 @@ class Emby(JellyfinEmby):
'Client="JellyPlex-Watched", ' 'Client="JellyPlex-Watched", '
'Device="script", ' 'Device="script", '
'DeviceId="script", ' 'DeviceId="script", '
'Version="0.0.0"' 'Version="6.0.2"'
) )
headers = { headers = {
"Accept": "application/json", "Accept": "application/json",
@@ -19,3 +20,6 @@ class Emby(JellyfinEmby):
super().__init__( super().__init__(
server_type="Emby", baseurl=baseurl, token=token, headers=headers 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,7 +56,7 @@ 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")
@@ -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,4 +1,5 @@
from src.jellyfin_emby import JellyfinEmby from src.jellyfin_emby import JellyfinEmby
from packaging import version
class Jellyfin(JellyfinEmby): class Jellyfin(JellyfinEmby):
@@ -8,7 +9,7 @@ class Jellyfin(JellyfinEmby):
'Client="JellyPlex-Watched", ' 'Client="JellyPlex-Watched", '
'Device="script", ' 'Device="script", '
'DeviceId="script", ' 'DeviceId="script", '
'Version="5.2.0", ' 'Version="6.0.2", '
f'Token="{token}"' f'Token="{token}"'
) )
headers = { headers = {
@@ -19,3 +20,6 @@ class Jellyfin(JellyfinEmby):
super().__init__( super().__init__(
server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers
) )
def is_partial_update_supported(self, server_version):
return server_version >= version.parse("10.9.0")

View File

@@ -4,6 +4,7 @@ import traceback, os
from math import floor from math import floor
from dotenv import load_dotenv from dotenv import load_dotenv
import requests import requests
from packaging import version
from src.functions import ( from src.functions import (
logger, logger,
@@ -12,13 +13,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,
)
from src.watched import (
combine_watched_dicts,
)
load_dotenv(override=True) load_dotenv(override=True)
@@ -111,7 +106,6 @@ class JellyfinEmby:
def __init__(self, server_type, baseurl, token, headers): def __init__(self, server_type, baseurl, token, headers):
if server_type not in ["Jellyfin", "Emby"]: if server_type not in ["Jellyfin", "Emby"]:
raise Exception(f"Server type {server_type} not supported") raise Exception(f"Server type {server_type} not supported")
self.server_type = server_type self.server_type = server_type
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
@@ -126,6 +120,7 @@ class JellyfinEmby:
self.session = requests.Session() self.session = requests.Session()
self.users = self.get_users() self.users = self.get_users()
self.server_name = self.info(name_only=True)
def query(self, query, query_type, identifiers=None, json=None): def query(self, query, query_type, identifiers=None, json=None):
try: try:
@@ -177,13 +172,15 @@ class JellyfinEmby:
) )
raise Exception(e) raise Exception(e)
def info(self) -> str: def info(self, name_only: bool = False) -> str:
try: try:
query_string = "/System/Info/Public" query_string = "/System/Info/Public"
response = self.query(query_string, "get") response = self.query(query_string, "get")
if response: if response:
if name_only:
return f"{response['ServerName']}"
return f"{self.server_type} {response['ServerName']}: {response['Version']}" return f"{self.server_type} {response['ServerName']}: {response['Version']}"
else: else:
return None return None
@@ -192,6 +189,19 @@ class JellyfinEmby:
logger(f"{self.server_type}: Get server name failed {e}", 2) logger(f"{self.server_type}: Get server name failed {e}", 2)
raise Exception(e) 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): def get_users(self):
try: try:
users = {} users = {}
@@ -209,13 +219,54 @@ class JellyfinEmby:
logger(f"{self.server_type}: Get users failed {e}", 2) logger(f"{self.server_type}: Get users failed {e}", 2)
raise Exception(e) 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( def get_user_library_watched(
self, user_name, user_id, library_type, library_id, library_title self, user_name, user_id, library_type, library_id, library_title
): ):
try: try:
user_name = user_name.lower() user_name = user_name.lower()
user_watched = {} user_watched = {}
user_watched[user_name] = {}
logger( logger(
f"{self.server_type}: Generating watched for {user_name} in library {library_title}", f"{self.server_type}: Generating watched for {user_name} in library {library_title}",
@@ -224,7 +275,7 @@ class JellyfinEmby:
# Movies # Movies
if library_type == "Movie": if library_type == "Movie":
user_watched[user_name][library_title] = [] user_watched[library_title] = []
watched = self.query( watched = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources", + f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
@@ -260,7 +311,7 @@ class JellyfinEmby:
movie_guids = get_guids(self.server_type, movie) movie_guids = get_guids(self.server_type, movie)
# Append the movie dictionary to the list for the given user and library # Append the movie dictionary to the list for the given user and library
user_watched[user_name][library_title].append(movie_guids) user_watched[library_title].append(movie_guids)
logger( logger(
f"{self.server_type}: Added {movie_guids} to {user_name} watched list", f"{self.server_type}: Added {movie_guids} to {user_name} watched list",
3, 3,
@@ -269,7 +320,7 @@ class JellyfinEmby:
# TV Shows # TV Shows
if library_type in ["Series", "Episode"]: if library_type in ["Series", "Episode"]:
# Initialize an empty dictionary for the given user and library # Initialize an empty dictionary for the given user and library
user_watched[user_name][library_title] = {} user_watched[library_title] = {}
# Retrieve a list of watched TV shows # Retrieve a list of watched TV shows
watched_shows = self.query( watched_shows = self.query(
@@ -301,11 +352,6 @@ class JellyfinEmby:
if "Path" in show if "Path" in show
else tuple() else tuple()
) )
show_display_name = (
show_guids["title"]
if show_guids["title"]
else show_guids["locations"]
)
show_guids = frozenset(show_guids.items()) show_guids = frozenset(show_guids.items())
@@ -338,26 +384,22 @@ class JellyfinEmby:
if mark_episodes_list: if mark_episodes_list:
# Add the show dictionary to the user's watched list # Add the show dictionary to the user's watched list
if show_guids not in user_watched[user_name][library_title]: if show_guids not in user_watched[library_title]:
user_watched[user_name][library_title][show_guids] = [] user_watched[library_title][show_guids] = []
user_watched[user_name][library_title][ user_watched[library_title][show_guids] = mark_episodes_list
show_guids
] = mark_episodes_list
for episode in mark_episodes_list: for episode in mark_episodes_list:
logger( logger(
f"{self.server_type}: Added {episode} to {user_name} {show_display_name} watched list", f"{self.server_type}: Added {episode} to {user_name} watched list",
1, 3,
) )
logger( logger(
f"{self.server_type}: Got watched for {user_name} in library {library_title}", f"{self.server_type}: Got watched for {user_name} in library {library_title}",
1, 1,
) )
if library_title in user_watched[user_name]: if library_title in user_watched:
logger( logger(f"{self.server_type}: {user_watched[library_title]}", 3)
f"{self.server_type}: {user_watched[user_name][library_title]}", 3
)
return user_watched return user_watched
except Exception as e: except Exception as e:
@@ -369,27 +411,22 @@ class JellyfinEmby:
logger(traceback.format_exc(), 2) logger(traceback.format_exc(), 2)
return {} return {}
def get_users_watched( def get_watched(self, users, sync_libraries):
self,
user_name,
user_id,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
try: try:
# Get all libraries users_watched = {}
user_name = user_name.lower()
watched = [] watched = []
for user_name, user_id in users.items():
libraries = [] libraries = []
all_libraries = self.query(f"/Users/{user_id}/Views", "get") all_libraries = self.query(f"/Users/{user_id}/Views", "get")
for library in all_libraries["Items"]: for library in all_libraries["Items"]:
library_id = library["Id"] library_id = library["Id"]
library_title = library["Name"] library_title = library["Name"]
if library_title not in sync_libraries:
continue
identifiers = { identifiers = {
"library_id": library_id, "library_id": library_id,
"library_title": library_title, "library_title": library_title,
@@ -409,6 +446,7 @@ class JellyfinEmby:
library_id = library["Identifiers"]["library_id"] library_id = library["Identifiers"]["library_id"]
library_title = library["Identifiers"]["library_title"] library_title = library["Identifiers"]["library_title"]
# Get all library types excluding "Folder" # Get all library types excluding "Folder"
types = set( types = set(
[ [
@@ -418,81 +456,19 @@ class JellyfinEmby:
] ]
) )
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"{self.server_type}: 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"{self.server_type}: Skipping Library {library_title} found types: {types}, all types: {all_types}",
1,
)
continue
for library_type in types: for library_type in types:
# Get watched for user # Get watched for user
watched.append( watched = self.get_user_library_watched(
self.get_user_library_watched(
user_name, user_name,
user_id, user_id,
library_type, library_type,
library_id, library_id,
library_title, library_title,
) )
)
return watched if user_name.lower() not in users_watched:
except Exception as e: users_watched[user_name.lower()] = {}
logger(f"{self.server_type}: Failed to get users watched, Error: {e}", 2) users_watched[user_name.lower()].update(watched)
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 return users_watched
except Exception as e: except Exception as e:
@@ -500,7 +476,7 @@ class JellyfinEmby:
raise Exception(e) raise Exception(e)
def update_user_watched( def update_user_watched(
self, user_name, user_id, library, library_id, videos, dryrun self, user_name, user_id, library, library_id, videos, update_partial, dryrun
): ):
try: try:
logger( logger(
@@ -556,11 +532,13 @@ class JellyfinEmby:
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
self.server_type,
self.server_name,
user_name, user_name,
library, library,
jellyfin_video.get("Name"), jellyfin_video.get("Name"),
) )
else: 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}" 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: if not dryrun:
@@ -578,6 +556,8 @@ class JellyfinEmby:
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
self.server_type,
self.server_name,
user_name, user_name,
library, library,
jellyfin_video.get("Name"), jellyfin_video.get("Name"),
@@ -684,12 +664,14 @@ class JellyfinEmby:
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
self.server_type,
self.server_name,
user_name, user_name,
library, library,
jellyfin_episode.get("SeriesName"), jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"), jellyfin_episode.get("Name"),
) )
else: elif update_partial:
msg = ( msg = (
f"{self.server_type}: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}" 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}" + f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library}"
@@ -712,6 +694,8 @@ class JellyfinEmby:
logger(msg, 6) logger(msg, 6)
log_marked( log_marked(
self.server_type,
self.server_name,
user_name, user_name,
library, library,
jellyfin_episode.get("SeriesName"), jellyfin_episode.get("SeriesName"),
@@ -741,6 +725,15 @@ class JellyfinEmby:
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
): ):
try: 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(): for user, libraries in watched_list.items():
logger(f"{self.server_type}: Updating for entry {user}, {libraries}", 1) logger(f"{self.server_type}: Updating for entry {user}, {libraries}", 1)
user_other = None user_other = None
@@ -813,7 +806,13 @@ class JellyfinEmby:
if library_id: if library_id:
self.update_user_watched( self.update_user_watched(
user_name, user_id, library, library_id, videos, dryrun user_name,
user_id,
library,
library_id,
videos,
update_partial,
dryrun,
) )
except Exception as e: except Exception as 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 {}
@@ -219,7 +290,6 @@ def episode_title_dict(user_list: dict):
return episode_output_dict return episode_output_dict
except Exception: except Exception:
logger("Skipping episode_output_dict", 1)
return {} return {}
@@ -252,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,200 +2,21 @@ 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
from src.emby import Emby
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 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
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")
@@ -262,10 +83,10 @@ def should_sync_server(server_1_type, server_2_type):
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)
@@ -333,24 +154,24 @@ 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 = server_1[1].get_watched( server_1[1],
server_1_users, server_2[1],
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 = server_2[1].get_watched( 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)

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)
@@ -186,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,
) )
@@ -317,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:
@@ -327,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,
@@ -358,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,
@@ -372,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,
@@ -466,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 = {}
@@ -500,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

@@ -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 (

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

View File

@@ -7,7 +7,7 @@ DRYRUN = "False"
DEBUG = "True" DEBUG = "True"
## Debugging level, "info" is default, "debug" is more verbose ## Debugging level, "info" is default, "debug" is more verbose
DEBUG_LEVEL = "info" DEBUG_LEVEL = "debug"
## If set to true then the script will only run once and then exit ## If set to true then the script will only run once and then exit
RUN_ONLY_ONCE = "True" RUN_ONLY_ONCE = "True"
@@ -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

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(
@@ -541,116 +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"),
}
): [
{
"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"),
}
): [
{
"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

@@ -8,7 +8,10 @@ def parse_args():
description="Check the mark.log file that is generated by the CI to make sure it contains the expected values" description="Check the mark.log file that is generated by the CI to make sure it contains the expected values"
) )
parser.add_argument( parser.add_argument(
"--dry", action="store_true", help="Check the mark.log file for dry-run" "--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( parser.add_argument(
"--write", action="store_true", help="Check the mark.log file for write-run" "--write", action="store_true", help="Check the mark.log file for write-run"
@@ -74,81 +77,110 @@ def check_marklog(lines, expected_values):
def main(): def main():
args = parse_args() args = parse_args()
expected_jellyfin = [ expected_jellyfin = [
"jellyplex_watched/Movies/Five Nights at Freddy's", "Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Two (2021)",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215", "Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 2",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose", "Plex/JellyPlex-CI/jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670", "Plex/JellyPlex-CI/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"jellyplex_watched/Movies/The Family Plan", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"jellyplex_watched/Movies/Five Nights at Freddy's", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5", "Emby/Emby-Server/jellyplex_watched/Custom Movies/Movie Two",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose", "Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E02",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5", "Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5", "Emby/Emby-Server/jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out", "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 = [ expected_emby = [
"jellyplex_watched/Movies/Tears of Steel", "Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Three (2022)",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath", "Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 3",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429", "Plex/JellyPlex-CI/jellyplex_watched/Movies/Tears of Steel",
"JellyUser/Movies/Tears of Steel", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4", "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 = [ expected_plex = [
"JellyUser/Movies/Big Buck Bunny", "Jellyfin/Jellyfin-Server/JellyUser/Movies/Big Buck Bunny",
"JellyUser/Movies/Killers of the Flower Moon/4", "Jellyfin/Jellyfin-Server/JellyUser/Movies/Killers of the Flower Moon/4",
"JellyUser/Shows/Doctor Who/The Unquiet Dead", "Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"JellyUser/Shows/Doctor Who/Aliens of London (1)/4", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/The Unquiet Dead",
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"jellyplex_watched/Movies/Big Buck Bunny", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"jellyplex_watched/Movies/The Family Plan", "Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie One (2020)",
"jellyplex_watched/Movies/Killers of the Flower Moon/4", "Emby/Emby-Server/jellyplex_watched/Movies/Big Buck Bunny",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead", "Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4", "Emby/Emby-Server/jellyplex_watched/Movies/Killers of the Flower Moon/4",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies", "Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out", "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",
] ]
expected_dry = expected_emby + expected_plex + expected_jellyfin expected_locations = expected_emby + expected_plex + expected_jellyfin
# 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 = [ expected_write = [
"jellyplex_watched/Movies/Five Nights at Freddy's", "Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Two (2021)",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215", "Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 2",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose", "Plex/JellyPlex-CI/jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670", "Plex/JellyPlex-CI/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"JellyUser/Movies/Big Buck Bunny", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"JellyUser/Movies/Killers of the Flower Moon/4", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"JellyUser/Shows/Doctor Who/The Unquiet Dead", "Jellyfin/Jellyfin-Server/JellyUser/Movies/Big Buck Bunny",
"JellyUser/Shows/Doctor Who/Aliens of London (1)/4", "Jellyfin/Jellyfin-Server/JellyUser/Movies/Killers of the Flower Moon/4",
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies", "Jellyfin/Jellyfin-Server/JellyUser/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/The Unquiet Dead",
"jellyplex_watched/Movies/Tears of Steel", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"jellyplex_watched/Movies/Big Buck Bunny", "Jellyfin/Jellyfin-Server/JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"jellyplex_watched/Movies/The Family Plan", "Jellyfin/Jellyfin-Server/JellyUser/Custom Movies/Movie One (2020)",
"jellyplex_watched/Movies/Five Nights at Freddy's", "Plex/JellyPlex-CI/jellyplex_watched/Custom Movies/Movie Three (2022)",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5", "Plex/JellyPlex-CI/jellyplex_watched/Custom TV Shows/Greatest Show Ever 3000/Episode 3",
"jellyplex_watched/Movies/Killers of the Flower Moon/4", "Plex/JellyPlex-CI/jellyplex_watched/Movies/Tears of Steel",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose", "Plex/JellyPlex-CI/jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5", "Emby/Emby-Server/jellyplex_watched/Movies/Big Buck Bunny",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead", "Emby/Emby-Server/jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4", "Emby/Emby-Server/jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5", "Emby/Emby-Server/jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies", "Emby/Emby-Server/jellyplex_watched/Movies/Killers of the Flower Moon/4",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out", "Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E01",
"JellyUser/Movies/Tears of Steel", "Emby/Emby-Server/jellyplex_watched/Custom TV Shows/Greatest Show Ever (3000)/S01E02",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4", "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 # 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 # due to some of the items being copied over from one server to another and now being there
# for the next server run. # for the next server run.
if args.dry: if args.guids:
expected_values = expected_dry expected_values = expected_guids
elif args.locations:
expected_values = expected_locations
elif args.write: elif args.write:
expected_values = expected_write expected_values = expected_write
elif args.plex: elif args.plex:
@@ -164,6 +196,12 @@ def main():
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")