36 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
Luigi311
4a4c9f9ccf Update to python 12 2023-12-06 14:16:47 -07:00
26 changed files with 787 additions and 771 deletions

View File

@@ -35,7 +35,7 @@ GENERATE_GUIDS = "True"
GENERATE_LOCATIONS = "True"
## 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
## 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
- "*.md"
env:
PYTHON_VERSION: '3.13'
jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install dependencies"
run: pip install -r requirements.txt && pip install -r test/requirements.txt
@@ -27,6 +34,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install dependencies"
run: |
pip install -r requirements.txt
@@ -46,7 +57,7 @@ jobs:
sleep 10
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
- name: "Test Plex"
@@ -77,7 +88,7 @@ jobs:
run: |
mv test/ci_guids.env .env
python main.py
python test/validate_ci_marklog.py --dry
python test/validate_ci_marklog.py --guids
rm mark.log
@@ -85,7 +96,7 @@ jobs:
run: |
mv test/ci_locations.env .env
python main.py
python test/validate_ci_marklog.py --dry
python test/validate_ci_marklog.py --locations
rm mark.log

View File

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

View File

@@ -1,56 +1,23 @@
FROM python:3.11-slim
ENV DRYRUN 'True'
ENV DEBUG 'True'
ENV DEBUG_LEVEL 'INFO'
ENV RUN_ONLY_ONCE 'False'
ENV SLEEP_DURATION '3600'
ENV LOGFILE 'log.log'
ENV MARKFILE 'mark.log'
ENV USER_MAPPING ''
ENV LIBRARY_MAPPING ''
ENV PLEX_BASEURL ''
ENV PLEX_TOKEN ''
ENV PLEX_USERNAME ''
ENV PLEX_PASSWORD ''
ENV PLEX_SERVERNAME ''
ENV JELLYFIN_BASEURL ''
ENV JELLYFIN_TOKEN ''
ENV SYNC_FROM_PLEX_TO_JELLYFIN 'True'
ENV SYNC_FROM_JELLYFIN_TO_PLEX 'True'
ENV SYNC_FROM_PLEX_TO_PLEX 'True'
ENV SYNC_FROM_JELLYFIN_TO_JELLYFIN 'True'
ENV BLACKLIST_LIBRARY ''
ENV WHITELIST_LIBRARY ''
ENV BLACKLIST_LIBRARY_TYPE ''
ENV WHITELIST_LIBRARY_TYPE ''
ENV BLACKLIST_USERS ''
ENV WHITELIST_USERS ''
FROM python:3.13-slim
ENV PUID=1000
ENV PGID=1000
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 && \
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
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --chown=jellyplex_user:jellyplex_user ./requirements.txt ./
COPY ./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"]

View File

@@ -1,32 +1,11 @@
version: '3'
# Sync watched status between media servers locally
services:
jellyplex-watched:
image: luigi311/jellyplex-watched:latest
container_name: jellyplex-watched
restart: always
restart: unless-stopped
environment:
- DRYRUN=True
- DEBUG=True
- DEBUG_LEVEL=info
- RUN_ONLY_ONCE=False
- SLEEP_DURATION=3600
- LOGFILE=/tmp/log.log
- MARKFILE=/tmp/mark.log
- USER_MAPPING={"user1":"user2"}
- LIBRARY_MAPPING={"TV Shows":"Shows"}
- BLACKLIST_LIBRARY=
- WHITELIST_LIBRARY=
- BLACKLIST_LIBRARY_TYPE=
- WHITELIST_LIBRARY_TYPE=
- BLACKLIST_USERS=
- WHITELIST_USERS=
- PLEX_BASEURL=https://localhost:32400
- PLEX_TOKEN=plex_token
- JELLYFIN_BASEURL=http://localhost:8096
- JELLYFIN_TOKEN=jelly_token
- SSL_BYPASS=True
- SYNC_FROM_PLEX_TO_JELLYFIN=True
- SYNC_FROM_JELLYFIN_TO_PLEX=True
- SYNC_FROM_PLEX_TO_PLEX=True
- SYNC_FROM_JELLYFIN_TO_JELLYFIN=True
- PUID=1000
- PGID=1000
env_file: "./.env"

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 packaging import version
class Emby(JellyfinEmby):
@@ -8,7 +9,7 @@ class Emby(JellyfinEmby):
'Client="JellyPlex-Watched", '
'Device="script", '
'DeviceId="script", '
'Version="0.0.0"'
'Version="6.0.2"'
)
headers = {
"Accept": "application/json",
@@ -19,3 +20,6 @@ class Emby(JellyfinEmby):
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)
logfile = os.getenv("LOGFILE", "log.log")
markfile = os.getenv("MARKFILE", "mark.log")
log_file = os.getenv("LOG_FILE", os.getenv("LOGFILE", "log.log"))
mark_file = os.getenv("MARK_FILE", os.getenv("MARKFILE", "mark.log"))
def logger(message: str, log_type=0):
@@ -32,17 +32,23 @@ def logger(message: str, log_type=0):
if output is not None:
print(output)
file = open(logfile, "a", encoding="utf-8")
file.write(output + "\n")
with open(f"{log_file}", "a", encoding="utf-8") as file:
file.write(output + "\n")
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
output = f"{username}/{library}/{movie_show}"
output = f"{server_type}/{server_name}/{username}/{library}/{movie_show}"
if episode:
output += f"/{episode}"
@@ -50,8 +56,8 @@ def log_marked(
if duration:
output += f"/{duration}"
file = open(f"{markfile}", "a", encoding="utf-8")
file.write(output + "\n")
with open(f"{mark_file}", "a", encoding="utf-8") as file:
file.write(output + "\n")
# 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 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(
args: list, threads: int = None, override_threads: bool = False
):

View File

@@ -1,4 +1,5 @@
from src.jellyfin_emby import JellyfinEmby
from packaging import version
class Jellyfin(JellyfinEmby):
@@ -8,7 +9,7 @@ class Jellyfin(JellyfinEmby):
'Client="JellyPlex-Watched", '
'Device="script", '
'DeviceId="script", '
'Version="5.2.0", '
'Version="6.0.2", '
f'Token="{token}"'
)
headers = {
@@ -19,3 +20,6 @@ class Jellyfin(JellyfinEmby):
super().__init__(
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

@@ -13,19 +13,14 @@ from src.functions import (
log_marked,
str_to_bool,
)
from src.library import (
check_skip_logic,
generate_library_guids_dict,
)
from src.watched import (
combine_watched_dicts,
)
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")}
@@ -111,7 +106,6 @@ 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
@@ -125,8 +119,8 @@ class JellyfinEmby:
raise Exception(f"{self.server_type} token not set")
self.session = requests.Session()
self.version = version.parse(self.info(version=True))
self.users = self.get_users()
self.server_name = self.info(name_only=True)
def query(self, query, query_type, identifiers=None, json=None):
try:
@@ -178,17 +172,15 @@ class JellyfinEmby:
)
raise Exception(e)
def info(self, version=False) -> str:
def info(self, name_only: bool = False) -> str:
try:
query_string = "/System/Info/Public"
response = self.query(query_string, "get")
if response:
# Return version only if requested
if version:
return response['Version']
if name_only:
return f"{response['ServerName']}"
return f"{self.server_type} {response['ServerName']}: {response['Version']}"
else:
return None
@@ -197,6 +189,19 @@ class JellyfinEmby:
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 = {}
@@ -214,13 +219,54 @@ class JellyfinEmby:
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 = {}
user_watched[user_name] = {}
logger(
f"{self.server_type}: Generating watched for {user_name} in library {library_title}",
@@ -229,7 +275,7 @@ class JellyfinEmby:
# Movies
if library_type == "Movie":
user_watched[user_name][library_title] = []
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",
@@ -265,7 +311,7 @@ class JellyfinEmby:
movie_guids = get_guids(self.server_type, movie)
# 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(
f"{self.server_type}: Added {movie_guids} to {user_name} watched list",
3,
@@ -274,7 +320,7 @@ class JellyfinEmby:
# TV Shows
if library_type in ["Series", "Episode"]:
# 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
watched_shows = self.query(
@@ -306,11 +352,6 @@ class JellyfinEmby:
if "Path" in show
else tuple()
)
show_display_name = (
show_guids["title"]
if show_guids["title"]
else show_guids["locations"]
)
show_guids = frozenset(show_guids.items())
@@ -343,26 +384,22 @@ class JellyfinEmby:
if mark_episodes_list:
# Add the show dictionary to the user's watched list
if show_guids not in user_watched[user_name][library_title]:
user_watched[user_name][library_title][show_guids] = []
if show_guids not in user_watched[library_title]:
user_watched[library_title][show_guids] = []
user_watched[user_name][library_title][
show_guids
] = mark_episodes_list
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} {show_display_name} watched list",
1,
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[user_name]:
logger(
f"{self.server_type}: {user_watched[user_name][library_title]}", 3
)
if library_title in user_watched:
logger(f"{self.server_type}: {user_watched[library_title]}", 3)
return user_watched
except Exception as e:
@@ -374,130 +411,64 @@ class JellyfinEmby:
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,
):
def get_watched(self, users, sync_libraries):
try:
# Get all libraries
user_name = user_name.lower()
users_watched = {}
watched = []
libraries = []
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"]
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,
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
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"]
]
)
library_id = library["Identifiers"]["library_id"]
library_title = library["Identifiers"]["library_title"]
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,
# Get all library types excluding "Folder"
types = set(
[
x["Type"]
for x in library["Items"]
if x["Type"] in ["Movie", "Series", "Episode"]
]
)
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:
# Get watched for user
watched.append(
self.get_user_library_watched(
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,
)
)
return watched
except Exception as e:
logger(f"{self.server_type}: 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)
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:
@@ -505,7 +476,7 @@ class JellyfinEmby:
raise Exception(e)
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:
logger(
@@ -561,37 +532,37 @@ class JellyfinEmby:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_video.get("Name"),
)
else:
# Handle partially watched movies not supported in jellyfin < 10.9.0
if self.server_type == "Jellyfin" and self.version < version.parse("10.9.0"):
logger(f"{self.server_type}: Skipping movie {jellyfin_video.get('Name')} as partially watched not supported in Jellyfin < 10.9.0", 4)
else:
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}"
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(
user_name,
library,
jellyfin_video.get("Name"),
duration=floor(movie_status["time"] / 60_000),
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}",
@@ -693,44 +664,44 @@ class JellyfinEmby:
logger(msg, 6)
log_marked(
self.server_type,
self.server_name,
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"),
)
else:
# Handle partially watched episodes not supported in jellyfin < 10.9.0
if self.server_type == "Jellyfin" and self.version < version.parse("10.9.0"):
logger(f"{self.server_type}: Skipping episode {jellyfin_episode.get('Name')} as partially watched not supported in Jellyfin < 10.9.0", 4)
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:
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}"
)
logger(msg, 6)
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(
user_name,
library,
jellyfin_episode.get("SeriesName"),
jellyfin_episode.get("Name"),
duration=floor(episode_status["time"] / 60_000),
)
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}",
@@ -754,6 +725,15 @@ class JellyfinEmby:
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
@@ -826,7 +806,13 @@ class JellyfinEmby:
if library_id:
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:

View File

@@ -1,5 +1,6 @@
from src.functions import (
logger,
match_list,
search_mapping,
)
@@ -129,6 +130,77 @@ def check_whitelist_logic(
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):
try:
show_output_dict = {}
@@ -158,7 +230,6 @@ def show_title_dict(user_list: dict):
return show_output_dict
except Exception:
logger("Skipping show_output_dict ", 1)
return {}
@@ -219,7 +290,6 @@ def episode_title_dict(user_list: dict):
return episode_output_dict
except Exception:
logger("Skipping episode_output_dict", 1)
return {}
@@ -252,7 +322,6 @@ def movies_title_dict(user_list: dict):
return movies_output_dict
except Exception:
logger("Skipping movies_output_dict failed", 1)
return {}

View File

@@ -2,200 +2,21 @@ import os, traceback, json
from dotenv import load_dotenv
from time import sleep, perf_counter
from src.library import setup_libraries
from src.functions import (
logger,
str_to_bool,
)
from src.users import (
generate_user_list,
combine_user_lists,
filter_user_lists,
generate_server_users,
)
from src.users import setup_users
from src.watched import (
cleanup_watched,
)
from src.black_white import setup_black_white_lists
from src.plex import Plex
from src.jellyfin import Jellyfin
from src.emby import Emby
from src.connection import generate_server_connections
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):
sync_from_plex_to_jellyfin = str_to_bool(
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():
logfile = os.getenv("LOGFILE", "log.log")
# Delete logfile if it exists
if os.path.exists(logfile):
os.remove(logfile)
log_file = os.getenv("LOG_FILE", os.getenv("LOGFILE", "log.log"))
# Delete log_file if it exists
if os.path.exists(log_file):
os.remove(log_file)
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
logger(f"Dryrun: {dryrun}", 1)
@@ -333,24 +154,24 @@ def main_loop():
server_1, server_2, blacklist_users, whitelist_users, user_mapping
)
logger("Creating watched lists", 1)
server_1_watched = server_1[1].get_watched(
server_1_users,
server_1_libraries, server_2_libraries = setup_libraries(
server_1[1],
server_2[1],
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library,
whitelist_library_type,
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)
server_2_watched = server_2[1].get_watched(
server_2_users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
server_2_users, server_2_libraries
)
logger("Finished creating watched list server 2", 1)

View File

@@ -19,10 +19,7 @@ from src.functions import (
log_marked,
str_to_bool,
)
from src.library import (
check_skip_logic,
generate_library_guids_dict,
)
from src.library import generate_library_guids_dict
load_dotenv(override=True)
@@ -186,7 +183,7 @@ def get_user_library_watched(user, user_plex, library):
if show_guids and episode_guids:
watched[show_guids] = episode_guids
logger(
f"Plex: Added {episode_guids} to {user_name} {show_guids} watched list",
f"Plex: Added {episode_guids} to {user_name} watched list",
3,
)
@@ -317,7 +314,15 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
else:
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:
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:
@@ -327,6 +332,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6)
log_marked(
"Plex",
user_plex.friendlyName,
user.title,
library,
movies_search.title,
@@ -358,6 +365,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6)
log_marked(
"Plex",
user_plex.friendlyName,
user.title,
library,
show_search.title,
@@ -372,6 +381,8 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
logger(msg, 6)
log_marked(
"Plex",
user_plex.friendlyName,
user.title,
library,
show_search.title,
@@ -466,15 +477,24 @@ class Plex:
logger(f"Plex: Failed to get users, Error: {e}", 2)
raise Exception(e)
def get_watched(
self,
users,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
):
def get_libraries(self):
try:
output = {}
libraries = self.plex.library.sections()
for library in libraries:
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:
# Get all libraries
users_watched = {}
@@ -500,23 +520,7 @@ class Plex:
libraries = user_plex.library.sections()
for library in libraries:
library_title = library.title
library_type = library.type
skip_reason = check_skip_logic(
library_title,
library_type,
blacklist_library,
whitelist_library,
blacklist_library_type,
whitelist_library_type,
library_mapping,
)
if skip_reason:
logger(
f"Plex: Skipping library {library_title}: {skip_reason}", 1
)
if library.title not in sync_libraries:
continue
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
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
def combine_watched_dicts(dicts: list):
# Ensure that the input is a list of dictionaries
if not all(isinstance(d, dict) for d in dicts):
raise ValueError("Input must be a list of dictionaries")
combined_dict = {}
for single_dict in dicts:
for key, value in single_dict.items():
if key not in combined_dict:
combined_dict[key] = {}
for subkey, subvalue in value.items():
if subkey in combined_dict[key]:
# If the subkey already exists in the combined dictionary,
# check if the values are different and raise an exception if they are
if combined_dict[key][subkey] != subvalue:
raise ValueError(
f"Conflicting values for subkey '{subkey}' under key '{key}'"
)
else:
# If the subkey does not exist in the combined dictionary, add it
combined_dict[key][subkey] = subvalue
return combined_dict
def check_remove_entry(video, library, video_index, library_watched_list_2):
if video_index is not None:
if (

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -7,7 +7,7 @@ DRYRUN = "True"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -7,7 +7,7 @@ DRYRUN = "False"
DEBUG = "True"
## 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
RUN_ONLY_ONCE = "True"
@@ -66,7 +66,7 @@ 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 = "mVaCzSyd78uoWkCBzZ_Y"
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

View File

@@ -13,7 +13,7 @@ parent = os.path.dirname(current)
# the sys.path.
sys.path.append(parent)
from src.watched import cleanup_watched, combine_watched_dicts
from src.watched import cleanup_watched
tv_shows_watched_list_1 = {
frozenset(
@@ -541,116 +541,3 @@ def test_mapping_cleanup_watched():
assert return_watched_list_1 == expected_watched_list_1
assert return_watched_list_2 == expected_watched_list_2
def test_combine_watched_dicts():
input_watched = [
{
"test3": {
"Anime Movies": [
{
"title": "Ponyo",
"tmdb": "12429",
"imdb": "tt0876563",
"locations": ("Ponyo (2008) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Spirited Away",
"tmdb": "129",
"imdb": "tt0245429",
"locations": ("Spirited Away (2001) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
{
"title": "Castle in the Sky",
"tmdb": "10515",
"imdb": "tt0092067",
"locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
},
]
}
},
{"test3": {"Anime Shows": {}}},
{"test3": {"Cartoon Shows": {}}},
{
"test3": {
"Shows": {
frozenset(
{
("tmdb", "64464"),
("tvdb", "301824"),
("tvrage", "45210"),
("title", "11.22.63"),
("locations", ("11.22.63",)),
("imdb", "tt2879552"),
}
): [
{
"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"
)
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(
"--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():
args = parse_args()
expected_jellyfin = [
"jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
"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",
"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 = [
"jellyplex_watched/Movies/Tears of Steel",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429",
"JellyUser/Movies/Tears of Steel",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"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 = [
"JellyUser/Movies/Big Buck Bunny",
"JellyUser/Movies/Killers of the Flower Moon/4",
"JellyUser/Shows/Doctor Who/The Unquiet Dead",
"JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"jellyplex_watched/Movies/Big Buck Bunny",
"jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/Movies/Killers of the Flower Moon/4",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
"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",
]
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 = [
"jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/301215",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/300670",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Aftermath",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/300741",
"JellyUser/Movies/Big Buck Bunny",
"JellyUser/Movies/Killers of the Flower Moon/4",
"JellyUser/Shows/Doctor Who/The Unquiet Dead",
"JellyUser/Shows/Doctor Who/Aliens of London (1)/4",
"JellyUser/Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"jellyplex_watched/Movies/Tears of Steel",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Parallels and Interiors/240429",
"jellyplex_watched/Movies/Big Buck Bunny",
"jellyplex_watched/Movies/The Family Plan",
"jellyplex_watched/Movies/Five Nights at Freddy's",
"jellyplex_watched/Movies/The Hunger Games: The Ballad of Songbirds & Snakes/5",
"jellyplex_watched/Movies/Killers of the Flower Moon/4",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Rose",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The End of the World/5",
"jellyplex_watched/TV Shows/Doctor Who (2005)/The Unquiet Dead",
"jellyplex_watched/TV Shows/Doctor Who (2005)/Aliens of London (1)/4",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Departure/5",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/Secrets and Lies",
"jellyplex_watched/TV Shows/Monarch: Legacy of Monsters/The Way Out",
"JellyUser/Movies/Tears of Steel",
"JellyUser/Shows/Monarch: Legacy of Monsters/Parallels and Interiors/4",
"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.dry:
expected_values = expected_dry
if args.guids:
expected_values = expected_guids
elif args.locations:
expected_values = expected_locations
elif args.write:
expected_values = expected_write
elif args.plex:
@@ -164,6 +196,12 @@ def main():
lines = read_marklog()
if not check_marklog(lines, expected_values):
print("Failed to validate marklog")
for line in lines:
# Remove the newline character
line = line.strip()
print(line)
exit(1)
print("Successfully validated marklog")