Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4771f736b0 | ||
|
|
8d7436579e | ||
|
|
43e1df98b1 | ||
|
|
3017030f52 | ||
|
|
348a0b8226 | ||
|
|
4e60c08120 | ||
|
|
10b58379cd | ||
|
|
fa9201b20f | ||
|
|
86f72997b4 | ||
|
|
62d0319aad | ||
|
|
a096a09eb7 | ||
|
|
7294241fed | ||
|
|
a5995d3999 | ||
|
|
30f31b2f3f | ||
|
|
bc09c873e9 | ||
|
|
8428be9dda | ||
|
|
6a45ad18f9 | ||
|
|
023b638729 | ||
|
|
7e13c14636 | ||
|
|
0c218fa9dd | ||
|
|
b3b0ccac73 | ||
|
|
fa0134551f | ||
|
|
34d62c9021 | ||
|
|
920bbbb3be | ||
|
|
4a4c9f9ccf |
@@ -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
15
.github/FUNDING.yml
vendored
Normal 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']
|
||||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM python:3.11-alpine
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
ENV PUID=1000
|
ENV PUID=1000
|
||||||
ENV PGID=1000
|
ENV PGID=1000
|
||||||
ENV GOSU_VERSION 1.17
|
ENV GOSU_VERSION=1.17
|
||||||
|
|
||||||
RUN apk add --no-cache tini
|
RUN apk add --no-cache tini dos2unix
|
||||||
|
|
||||||
# Install gosu
|
# Install gosu
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
@@ -42,7 +42,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN chmod +x *.sh
|
RUN chmod +x *.sh && \
|
||||||
|
dos2unix *.sh
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
|
ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
|
||||||
CMD ["python", "-u", "main.py"]
|
CMD ["python", "-u", "main.py"]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
ENV PUID=1000
|
ENV PUID=1000
|
||||||
ENV PGID=1000
|
ENV PGID=1000
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install tini gosu --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/*
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN chmod +x *.sh
|
RUN chmod +x *.sh && \
|
||||||
|
dos2unix *.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/tini", "--", "/app/entrypoint.sh"]
|
ENTRYPOINT ["/bin/tini", "--", "/app/entrypoint.sh"]
|
||||||
CMD ["python", "-u", "main.py"]
|
CMD ["python", "-u", "main.py"]
|
||||||
|
|||||||
@@ -2,29 +2,39 @@
|
|||||||
|
|
||||||
set -e
|
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
|
# Create group and user based on environment variables
|
||||||
if [ ! "$(getent group "$PGID")" ]; then
|
if [ ! "$(getent group "$PGID")" ]; then
|
||||||
# If groupadd exists, use it
|
# If groupadd exists, use it
|
||||||
if command -v groupadd > /dev/null; then
|
if command -v groupadd > /dev/null; then
|
||||||
groupadd -g "$PGID" jellyplex_group
|
groupadd -g "$PGID" jellyplex_watched
|
||||||
else
|
elif command -v addgroup > /dev/null; then
|
||||||
addgroup -g "$PGID" jellyplex_group
|
addgroup -g "$PGID" jellyplex_watched
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# If user id does not exist, create the user
|
||||||
if [ ! "$(getent passwd "$PUID")" ]; then
|
if [ ! "$(getent passwd "$PUID")" ]; then
|
||||||
# If useradd exists, use it
|
|
||||||
if command -v useradd > /dev/null; then
|
if command -v useradd > /dev/null; then
|
||||||
useradd --no-create-home -u "$PUID" -g "$PGID" jellyplex_user
|
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
|
else
|
||||||
adduser -D -H -u "$PUID" -G jellyplex_group jellyplex_user
|
# If user is not root, set the PUID and PGID to the current user
|
||||||
fi
|
PUID=$(id -u)
|
||||||
|
PGID=$(id -g)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Adjust ownership of the application directory
|
# Get directory of log and mark file to create base folder if it doesnt exist
|
||||||
chown -R "$PUID:$PGID" /app
|
|
||||||
|
|
||||||
# Get directory of log and mark file to create base folder if it doesnt exist and change permissions
|
|
||||||
LOG_DIR=$(dirname "$LOG_FILE")
|
LOG_DIR=$(dirname "$LOG_FILE")
|
||||||
# If LOG_DIR is set, create the directory
|
# If LOG_DIR is set, create the directory
|
||||||
if [ -n "$LOG_DIR" ]; then
|
if [ -n "$LOG_DIR" ]; then
|
||||||
@@ -36,8 +46,16 @@ if [ -n "$MARK_DIR" ]; then
|
|||||||
mkdir -p "$MARK_DIR"
|
mkdir -p "$MARK_DIR"
|
||||||
fi
|
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" "$LOG_DIR"
|
||||||
chown -R "$PUID:$PGID" "$MARK_DIR"
|
chown -R "$PUID:$PGID" "$MARK_DIR"
|
||||||
|
|
||||||
# Run the application as the created user
|
# Run the application as the created user
|
||||||
exec gosu "$PUID:$PGID" "$@"
|
exec gosu "$PUID:$PGID" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the application as the current user
|
||||||
|
exec "$@"
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
139
src/connection.py
Normal file
139
src/connection.py
Normal 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
|
||||||
@@ -37,12 +37,18 @@ def logger(message: str, log_type=0):
|
|||||||
|
|
||||||
|
|
||||||
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 mark_file 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}"
|
||||||
@@ -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
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -13,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)
|
||||||
|
|
||||||
@@ -112,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
|
||||||
@@ -127,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:
|
||||||
@@ -178,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
|
||||||
@@ -223,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}",
|
||||||
@@ -238,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",
|
||||||
@@ -274,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,
|
||||||
@@ -283,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(
|
||||||
@@ -315,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())
|
||||||
|
|
||||||
@@ -352,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:
|
||||||
@@ -383,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,
|
||||||
@@ -423,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(
|
||||||
[
|
[
|
||||||
@@ -432,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:
|
||||||
@@ -570,6 +532,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"),
|
||||||
@@ -592,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"),
|
||||||
@@ -698,6 +664,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"),
|
||||||
@@ -726,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"),
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
205
src/main.py
205
src/main.py
@@ -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")
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
68
src/plex.py
68
src/plex.py
@@ -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)
|
||||||
|
|||||||
42
src/users.py
42
src/users.py
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -66,7 +66,7 @@ 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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user