Compare commits
58 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a1adf97bfb | |
|
|
8dea290dd1 | |
|
|
90358f0176 | |
|
|
96239d6704 | |
|
|
e8e6d6469f | |
|
|
0dc338df6f | |
|
|
72df1cd0ad | |
|
|
8af7fe00f9 | |
|
|
6eefedfc40 | |
|
|
2914dbb81c | |
|
|
52c780d8a7 | |
|
|
0276e7c8eb | |
|
|
bf50defcb5 | |
|
|
71d753878e | |
|
|
21c530d956 | |
|
|
142c9df6e9 | |
|
|
629f50ecdc | |
|
|
3e2450b5fd | |
|
|
0de5e86837 | |
|
|
33a719f693 | |
|
|
9ff985a848 | |
|
|
5501e21aa8 | |
|
|
2208d91d07 | |
|
|
75f7f576ac | |
|
|
24f56769f9 | |
|
|
29e4f224dc | |
|
|
bdb58918e7 | |
|
|
c3be980eea | |
|
|
c1a26dd73b | |
|
|
e5d5f11f33 | |
|
|
616ca92d5e | |
|
|
b2b214c987 | |
|
|
07542b498e | |
|
|
9e53c0f8e2 | |
|
|
98266de678 | |
|
|
9d4f3dd432 | |
|
|
cc9b84fefa | |
|
|
c76bb3b355 | |
|
|
544649effd | |
|
|
46b60bb866 | |
|
|
5670c3ad97 | |
|
|
7e0f4babda | |
|
|
d5c36c61ec | |
|
|
69cd73d965 | |
|
|
229ab59b44 | |
|
|
3e474a4593 | |
|
|
69958a257b | |
|
|
64c1823e5b | |
|
|
446f6df470 | |
|
|
91ea5d76f6 | |
|
|
dc26b9a7b1 | |
|
|
d98b7c3e09 | |
|
|
93d9471333 | |
|
|
e6fa8ae745 | |
|
|
5b644a54a2 | |
|
|
5a17c5f7a1 | |
|
|
61e3dddd6b | |
|
|
aaaa7eba70 |
|
|
@ -0,0 +1,117 @@
|
|||
# Global Settings
|
||||
|
||||
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||
DRYRUN = "False"
|
||||
|
||||
## Debugging level, "info" is default, "debug" is more verbose
|
||||
#DEBUG_LEVEL = "DEBUG"
|
||||
DEBUG_LEVEL = "INFO"
|
||||
|
||||
## If set to true then the script will only run once and then exit
|
||||
RUN_ONLY_ONCE = "True"
|
||||
|
||||
## How often to run the script in seconds
|
||||
SLEEP_DURATION = "60"
|
||||
|
||||
## Log file where all output will be written to
|
||||
LOG_FILE = "/mnt/log.log"
|
||||
|
||||
## Mark file where all shows/movies that have been marked as played will be written to
|
||||
MARK_FILE = "/mnt/mark.log"
|
||||
|
||||
## Timeout for requests for jellyfin
|
||||
REQUEST_TIMEOUT = 300
|
||||
|
||||
## Max threads for processing
|
||||
MAX_THREADS = 1
|
||||
|
||||
## Generate guids/locations
|
||||
## These are slow processes, so this is a way to speed things up
|
||||
## If media servers are using the same files then you can enable only generate locations
|
||||
## If media servers are using different files then you can enable only generate guids
|
||||
## Default is to generate both
|
||||
GENERATE_GUIDS = "True"
|
||||
GENERATE_LOCATIONS = "True"
|
||||
|
||||
## Map usernames between servers in the event that they are different, order does not matter
|
||||
## Comma separated for multiple options
|
||||
# jellyfin: plex,plex
|
||||
#USER_MAPPING = { "belandbroc": "debila,belan49", "debila,belan49": "belandbroc", "debila": "belandbroc", "belan49": "belandbroc" }
|
||||
USER_MAPPING = { "belandbroc":"debila", "debila":"belandbroc", "debila":"belandbroc" }
|
||||
|
||||
## Map libraries between servers in the event that they are different, order does not matter
|
||||
## Comma separated for multiple options
|
||||
LIBRARY_MAPPING = { "TV Shows": "Scratch TV Shows", "Scratch TV Shows": "TV Shows" }
|
||||
|
||||
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
|
||||
## Comma separated for multiple options
|
||||
#BLACKLIST_LIBRARY = ""
|
||||
WHITELIST_LIBRARY = "TV Shows,Scratch TV Shows,Movies"
|
||||
#BLACKLIST_LIBRARY_TYPE = ""
|
||||
#WHITELIST_LIBRARY_TYPE = ""
|
||||
#BLACKLIST_USERS = ""
|
||||
WHITELIST_USERS = "belandbroc,debila"
|
||||
|
||||
|
||||
# Plex
|
||||
|
||||
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
|
||||
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
|
||||
## Comma separated list for multiple servers
|
||||
PLEX_BASEURL = "http://192.168.0.86:32400"
|
||||
|
||||
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
||||
## Comma separated list for multiple servers
|
||||
# PLEX_TOKEN = "vPGyuy6zWVCz6ZFyy8x1"
|
||||
# # debila=debilapointe@gmail
|
||||
PLEX_TOKEN = "S7gbVzAzH4ypN-4K7ta5"
|
||||
# me
|
||||
|
||||
## If not using plex token then use username and password of the server admin along with the servername
|
||||
## Comma separated for multiple options
|
||||
#PLEX_USERNAME = "squeaky2x3@gmail.com"
|
||||
#PLEX_PASSWORD = "qoDuGNsGsWRurOd5QFdRy2@"
|
||||
#PLEX_SERVERNAME = "Scratch"
|
||||
|
||||
## Skip hostname validation for ssl certificates.
|
||||
## Set to True if running into ssl certificate errors
|
||||
SSL_BYPASS = "True"
|
||||
|
||||
|
||||
# Jellyfin
|
||||
|
||||
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
|
||||
## Comma separated list for multiple servers
|
||||
JELLYFIN_BASEURL = "https://jellyfin.home.blapointe.com"
|
||||
|
||||
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
||||
## Comma separated list for multiple servers
|
||||
JELLYFIN_TOKEN = "1dc766ce6ca44c53b773263a06889b96"
|
||||
|
||||
|
||||
# # Emby
|
||||
#
|
||||
# ## Emby server URL, use hostname or IP address if the hostname is not resolving correctly
|
||||
# ## Comma seperated list for multiple servers
|
||||
# EMBY_BASEURL = "http://localhost:8097"
|
||||
#
|
||||
# ## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
|
||||
# ## Comma seperated list for multiple servers
|
||||
# EMBY_TOKEN = "SuperSecretToken"
|
||||
|
||||
|
||||
# Syncing Options
|
||||
|
||||
## control the direction of syncing. e.g. SYNC_FROM_PLEX_TO_JELLYFIN set to true will cause the updates from plex
|
||||
## to be updated in jellyfin. SYNC_FROM_PLEX_TO_PLEX set to true will sync updates between multiple plex servers
|
||||
SYNC_FROM_PLEX_TO_JELLYFIN = "True"
|
||||
SYNC_FROM_PLEX_TO_PLEX = "False"
|
||||
#SYNC_FROM_PLEX_TO_EMBY = "False"
|
||||
|
||||
SYNC_FROM_JELLYFIN_TO_PLEX = "False"
|
||||
SYNC_FROM_JELLYFIN_TO_JELLYFIN = "False"
|
||||
#SYNC_FROM_JELLYFIN_TO_EMBY = "False"
|
||||
|
||||
#SYNC_FROM_EMBY_TO_PLEX = "False"
|
||||
#SYNC_FROM_EMBY_TO_JELLYFIN = "False"
|
||||
#SYNC_FROM_EMBY_TO_EMBY = "False"
|
||||
37
.env.sample
37
.env.sample
|
|
@ -3,11 +3,8 @@
|
|||
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||
DRYRUN = "True"
|
||||
|
||||
## Additional logging information
|
||||
DEBUG = "False"
|
||||
|
||||
## Debugging level, "info" is default, "debug" is more verbose
|
||||
DEBUG_LEVEL = "info"
|
||||
DEBUG_LEVEL = "INFO"
|
||||
|
||||
## If set to true then the script will only run once and then exit
|
||||
RUN_ONLY_ONCE = "False"
|
||||
|
|
@ -16,7 +13,7 @@ RUN_ONLY_ONCE = "False"
|
|||
SLEEP_DURATION = "3600"
|
||||
|
||||
## Log file where all output will be written to
|
||||
LOGFILE = "log.log"
|
||||
LOG_FILE = "log.log"
|
||||
|
||||
## Mark file where all shows/movies that have been marked as played will be written to
|
||||
MARK_FILE = "mark.log"
|
||||
|
|
@ -24,26 +21,24 @@ MARK_FILE = "mark.log"
|
|||
## Timeout for requests for jellyfin
|
||||
REQUEST_TIMEOUT = 300
|
||||
|
||||
## Generate guids
|
||||
## Generating guids is a slow process, so this is a way to speed up the process
|
||||
## by using the location only, useful when using same files on multiple servers
|
||||
GENERATE_GUIDS = "True"
|
||||
|
||||
## Generate locations
|
||||
## Generating locations is a slow process, so this is a way to speed up the process
|
||||
## by using the guid only, useful when using different files on multiple servers
|
||||
GENERATE_LOCATIONS = "True"
|
||||
|
||||
## Max threads for processing
|
||||
MAX_THREADS = 2
|
||||
MAX_THREADS = 1
|
||||
|
||||
## Generate guids/locations
|
||||
## These are slow processes, so this is a way to speed things up
|
||||
## If media servers are using the same files then you can enable only generate locations
|
||||
## If media servers are using different files then you can enable only generate guids
|
||||
## Default is to generate both
|
||||
GENERATE_GUIDS = "True"
|
||||
GENERATE_LOCATIONS = "True"
|
||||
|
||||
## Map usernames between servers in the event that they are different, order does not matter
|
||||
## Comma separated for multiple options
|
||||
#USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
|
||||
USER_MAPPING = { "Username": "User", "Second User": "User Dos" }
|
||||
|
||||
## Map libraries between servers in the event that they are different, order does not matter
|
||||
## Comma separated for multiple options
|
||||
#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
|
||||
LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
|
||||
|
||||
## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
|
||||
## Comma separated for multiple options
|
||||
|
|
@ -52,7 +47,7 @@ MAX_THREADS = 2
|
|||
#BLACKLIST_LIBRARY_TYPE = ""
|
||||
#WHITELIST_LIBRARY_TYPE = ""
|
||||
#BLACKLIST_USERS = ""
|
||||
WHITELIST_USERS = "testuser1,testuser2"
|
||||
#WHITELIST_USERS = ""
|
||||
|
||||
|
||||
# Plex
|
||||
|
|
@ -96,7 +91,7 @@ EMBY_BASEURL = "http://localhost:8097"
|
|||
|
||||
## Emby api token, created manually by logging in to the Emby server admin dashboard and creating an api key
|
||||
## Comma seperated list for multiple servers
|
||||
EMBY_TOKEN = "ed9507cba8d14d469ae4d58e33afc515"
|
||||
EMBY_TOKEN = "SuperSecretToken"
|
||||
|
||||
|
||||
# Syncing Options
|
||||
|
|
@ -113,4 +108,4 @@ SYNC_FROM_JELLYFIN_TO_EMBY = "True"
|
|||
|
||||
SYNC_FROM_EMBY_TO_PLEX = "True"
|
||||
SYNC_FROM_EMBY_TO_JELLYFIN = "True"
|
||||
SYNC_FROM_EMBY_TO_EMBY = "True"
|
||||
SYNC_FROM_EMBY_TO_EMBY = "True"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
python-version-file: ".python-version"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: uv sync --all-extras --dev
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: "Run tests"
|
||||
run: uv run pytest -vvv
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
uv sync --all-extras --dev
|
||||
uv sync --frozen
|
||||
sudo apt update && sudo apt install -y docker-compose
|
||||
|
||||
- name: "Checkout JellyPlex-Watched-CI"
|
||||
|
|
@ -70,40 +70,35 @@ jobs:
|
|||
|
||||
- name: "Test Plex"
|
||||
run: |
|
||||
mv test/ci_plex.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_plex.env" uv run main.py
|
||||
uv run test/validate_ci_marklog.py --plex
|
||||
|
||||
rm mark.log
|
||||
|
||||
- name: "Test Jellyfin"
|
||||
run: |
|
||||
mv test/ci_jellyfin.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_jellyfin.env" uv run main.py
|
||||
uv run test/validate_ci_marklog.py --jellyfin
|
||||
|
||||
rm mark.log
|
||||
|
||||
- name: "Test Emby"
|
||||
run: |
|
||||
mv test/ci_emby.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_emby.env" uv run main.py
|
||||
uv run test/validate_ci_marklog.py --emby
|
||||
|
||||
rm mark.log
|
||||
|
||||
- name: "Test Guids"
|
||||
run: |
|
||||
mv test/ci_guids.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_guids.env" uv run main.py
|
||||
uv run test/validate_ci_marklog.py --guids
|
||||
|
||||
rm mark.log
|
||||
|
||||
- name: "Test Locations"
|
||||
run: |
|
||||
mv test/ci_locations.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_locations.env" uv run main.py
|
||||
uv run test/validate_ci_marklog.py --locations
|
||||
|
||||
rm mark.log
|
||||
|
|
@ -111,11 +106,10 @@ jobs:
|
|||
- name: "Test writing to the servers"
|
||||
run: |
|
||||
# Test writing to the servers
|
||||
mv test/ci_write.env .env
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_write.env" uv run main.py
|
||||
|
||||
# Test again to test if it can handle existing data
|
||||
uv run main.py
|
||||
ENV_FILE="test/ci_write.env" uv run main.py
|
||||
|
||||
uv run test/validate_ci_marklog.py --write
|
||||
|
||||
|
|
@ -178,6 +172,7 @@ jobs:
|
|||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
if: "${{ env.DOCKER_USERNAME != '' }}"
|
||||
id: docker_login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
|
@ -191,26 +186,14 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
if: "${{ steps.docker_meta.outputs.tags == '' }}"
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: false
|
||||
tags: jellyplex-watched:action
|
||||
|
||||
- name: Build Push
|
||||
id: build_push
|
||||
if: "${{ steps.docker_meta.outputs.tags != '' }}"
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
push: ${{ steps.docker_login.outcome == 'success' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -19,6 +19,7 @@ Keep in sync all your users watched history between jellyfin, plex and emby serv
|
|||
- \[x] One way/multi way sync
|
||||
- \[x] Sync watched
|
||||
- \[x] Sync in progress
|
||||
- \[ ] Sync view dates
|
||||
|
||||
### Jellyfin
|
||||
|
||||
|
|
@ -29,6 +30,8 @@ Keep in sync all your users watched history between jellyfin, plex and emby serv
|
|||
- \[x] One way/multi way sync
|
||||
- \[x] Sync watched
|
||||
- \[x] Sync in progress
|
||||
- \[x] Sync view dates
|
||||
|
||||
|
||||
### Emby
|
||||
|
||||
|
|
@ -39,6 +42,8 @@ Keep in sync all your users watched history between jellyfin, plex and emby serv
|
|||
- \[x] One way/multi way sync
|
||||
- \[x] Sync watched
|
||||
- \[x] Sync in progress
|
||||
- \[x] Sync view dates
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -50,7 +55,7 @@ Full list of configuration options can be found in the [.env.sample](.env.sample
|
|||
|
||||
- [Install uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
|
||||
- Create a .env file similar to .env.sample; fill in baseurls and tokens, **remember to uncomment anything you wish to use** (e.g., user mapping, library mapping, black/whitelist, etc.)
|
||||
- Create a .env file similar to .env.sample; fill in baseurls and tokens, **remember to uncomment anything you wish to use** (e.g., user mapping, library mapping, black/whitelist, etc.). If you want to store your .env file anywhere else or under a different name you can use ENV_FILE variable to specify the location.
|
||||
|
||||
- Run
|
||||
|
||||
|
|
@ -58,6 +63,10 @@ Full list of configuration options can be found in the [.env.sample](.env.sample
|
|||
uv run main.py
|
||||
```
|
||||
|
||||
```bash
|
||||
ENV_FILE="Test.env" uv run main.py
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
- Build docker image
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
[project]
|
||||
name = "jellyplex-watched"
|
||||
version = "6.1.2"
|
||||
version = "8.3.0"
|
||||
description = "Sync watched between media servers locally"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"loguru>=0.7.3",
|
||||
"packaging==24.2",
|
||||
"plexapi==4.16.1",
|
||||
"pydantic==2.10.6",
|
||||
"python-dotenv==1.0.0",
|
||||
"requests==2.32.3",
|
||||
"packaging==25.0",
|
||||
"plexapi==4.17.1",
|
||||
"pydantic==2.11.7",
|
||||
"python-dotenv==1.1.1",
|
||||
"requests==2.32.5",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
lint = [
|
||||
"ruff>=0.9.6",
|
||||
"ruff>=0.12.3",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=8.3.4",
|
||||
"mypy>=1.16.1",
|
||||
"pytest>=8.4.1",
|
||||
"types-requests>=2.32.0.20250611",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
#! /usr/bin/env bash
|
||||
|
||||
d=/tmp/jellyplex.d
|
||||
mkdir -p $d
|
||||
docker run --rm -it -v "$d":/mnt $(
|
||||
if [ "${PWD##*/}" == JellyPlex-Watched ]; then
|
||||
echo "-v $PWD/src:/app/src"
|
||||
fi
|
||||
) -v $PWD/.env:/app/.env \
|
||||
luigi311/jellyplex-watched:latest
|
||||
|
|
@ -12,7 +12,7 @@ def setup_black_white_lists(
|
|||
whitelist_users: list[str] | None,
|
||||
library_mapping: dict[str, str] | None = None,
|
||||
user_mapping: dict[str, str] | None = None,
|
||||
):
|
||||
) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str]]:
|
||||
blacklist_library, blacklist_library_type, blacklist_users = setup_x_lists(
|
||||
blacklist_library,
|
||||
blacklist_library_type,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import os
|
||||
from typing import Literal
|
||||
from dotenv import load_dotenv
|
||||
from loguru import logger
|
||||
|
||||
from src.functions import str_to_bool
|
||||
from src.functions import str_to_bool, get_env_value
|
||||
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: str, server_token: str, server_type: Literal["jellyfin", "emby"]
|
||||
env,
|
||||
server_baseurl: str,
|
||||
server_token: str,
|
||||
server_type: Literal["jellyfin", "emby"],
|
||||
) -> list[Jellyfin | Emby]:
|
||||
servers: list[Jellyfin | Emby] = []
|
||||
server: Jellyfin | Emby
|
||||
|
|
@ -25,17 +24,19 @@ def jellyfin_emby_server_connection(
|
|||
f"{server_type.upper()}_BASEURL and {server_type.upper()}_TOKEN must have the same number of entries"
|
||||
)
|
||||
|
||||
for i, baseurl in enumerate(server_baseurls):
|
||||
baseurl = baseurl.strip()
|
||||
if baseurl[-1] == "/":
|
||||
baseurl = baseurl[:-1]
|
||||
for i, base_url in enumerate(server_baseurls):
|
||||
base_url = base_url.strip()
|
||||
if base_url[-1] == "/":
|
||||
base_url = base_url[:-1]
|
||||
|
||||
if server_type == "jellyfin":
|
||||
server = Jellyfin(baseurl=baseurl, token=server_tokens[i].strip())
|
||||
server = Jellyfin(
|
||||
env=env, base_url=base_url, token=server_tokens[i].strip()
|
||||
)
|
||||
servers.append(server)
|
||||
|
||||
elif server_type == "emby":
|
||||
server = Emby(baseurl=baseurl, token=server_tokens[i].strip())
|
||||
server = Emby(env=env, base_url=base_url, token=server_tokens[i].strip())
|
||||
servers.append(server)
|
||||
else:
|
||||
raise Exception("Unknown server type")
|
||||
|
|
@ -45,16 +46,17 @@ def jellyfin_emby_server_connection(
|
|||
return servers
|
||||
|
||||
|
||||
def generate_server_connections() -> list[Plex | Jellyfin | Emby]:
|
||||
def generate_server_connections(env) -> list[Plex | Jellyfin | Emby]:
|
||||
servers: list[Plex | Jellyfin | Emby] = []
|
||||
|
||||
plex_baseurl_str: str | None = os.getenv("PLEX_BASEURL", None)
|
||||
plex_token_str: str | None = os.getenv("PLEX_TOKEN", None)
|
||||
plex_username_str: str | None = os.getenv("PLEX_USERNAME", None)
|
||||
plex_password_str: str | None = os.getenv("PLEX_PASSWORD", None)
|
||||
plex_servername_str: str | None = os.getenv("PLEX_SERVERNAME", None)
|
||||
ssl_bypass = str_to_bool(os.getenv("SSL_BYPASS", "False"))
|
||||
plex_baseurl_str: str | None = get_env_value(env, "PLEX_BASEURL", None)
|
||||
plex_token_str: str | None = get_env_value(env, "PLEX_TOKEN", None)
|
||||
plex_username_str: str | None = get_env_value(env, "PLEX_USERNAME", None)
|
||||
plex_password_str: str | None = get_env_value(env, "PLEX_PASSWORD", None)
|
||||
plex_servername_str: str | None = get_env_value(env, "PLEX_SERVERNAME", None)
|
||||
ssl_bypass = str_to_bool(get_env_value(env, "SSL_BYPASS", "False"))
|
||||
|
||||
print(f"if plex_baseurl_str={plex_baseurl_str} and plex_token_str={plex_token_str}")
|
||||
if plex_baseurl_str and plex_token_str:
|
||||
plex_baseurl = plex_baseurl_str.split(",")
|
||||
plex_token = plex_token_str.split(",")
|
||||
|
|
@ -65,12 +67,14 @@ def generate_server_connections() -> list[Plex | Jellyfin | Emby]:
|
|||
)
|
||||
|
||||
for i, url in enumerate(plex_baseurl):
|
||||
print(f"Plex({url.strip()}, {plex_token[i].strip()})")
|
||||
server = Plex(
|
||||
baseurl=url.strip(),
|
||||
env,
|
||||
base_url=url.strip(),
|
||||
token=plex_token[i].strip(),
|
||||
username=None,
|
||||
user_name=None,
|
||||
password=None,
|
||||
servername=None,
|
||||
server_name=None,
|
||||
ssl_bypass=ssl_bypass,
|
||||
)
|
||||
|
||||
|
|
@ -92,31 +96,32 @@ def generate_server_connections() -> list[Plex | Jellyfin | Emby]:
|
|||
|
||||
for i, username in enumerate(plex_username):
|
||||
server = Plex(
|
||||
baseurl=None,
|
||||
env,
|
||||
base_url=None,
|
||||
token=None,
|
||||
username=username.strip(),
|
||||
user_name=username.strip(),
|
||||
password=plex_password[i].strip(),
|
||||
servername=plex_servername[i].strip(),
|
||||
server_name=plex_servername[i].strip(),
|
||||
ssl_bypass=ssl_bypass,
|
||||
)
|
||||
|
||||
logger.debug(f"Plex Server {i} info: {server.info()}")
|
||||
servers.append(server)
|
||||
|
||||
jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None)
|
||||
jellyfin_token = os.getenv("JELLYFIN_TOKEN", None)
|
||||
jellyfin_baseurl = get_env_value(env, "JELLYFIN_BASEURL", None)
|
||||
jellyfin_token = get_env_value(env, "JELLYFIN_TOKEN", None)
|
||||
if jellyfin_baseurl and jellyfin_token:
|
||||
servers.extend(
|
||||
jellyfin_emby_server_connection(
|
||||
jellyfin_baseurl, jellyfin_token, "jellyfin"
|
||||
env, jellyfin_baseurl, jellyfin_token, "jellyfin"
|
||||
)
|
||||
)
|
||||
|
||||
emby_baseurl = os.getenv("EMBY_BASEURL", None)
|
||||
emby_token = os.getenv("EMBY_TOKEN", None)
|
||||
emby_baseurl = get_env_value(env, "EMBY_BASEURL", None)
|
||||
emby_token = get_env_value(env, "EMBY_TOKEN", None)
|
||||
if emby_baseurl and emby_token:
|
||||
servers.extend(
|
||||
jellyfin_emby_server_connection(emby_baseurl, emby_token, "emby")
|
||||
jellyfin_emby_server_connection(env, emby_baseurl, emby_token, "emby")
|
||||
)
|
||||
|
||||
return servers
|
||||
|
|
|
|||
13
src/emby.py
13
src/emby.py
|
|
@ -1,9 +1,10 @@
|
|||
from src.jellyfin_emby import JellyfinEmby
|
||||
from packaging.version import parse, Version
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class Emby(JellyfinEmby):
|
||||
def __init__(self, baseurl, token):
|
||||
def __init__(self, env, base_url: str, token: str) -> None:
|
||||
authorization = (
|
||||
"Emby , "
|
||||
'Client="JellyPlex-Watched", '
|
||||
|
|
@ -18,8 +19,14 @@ class Emby(JellyfinEmby):
|
|||
}
|
||||
|
||||
super().__init__(
|
||||
server_type="Emby", baseurl=baseurl, token=token, headers=headers
|
||||
env, server_type="Emby", base_url=base_url, token=token, headers=headers
|
||||
)
|
||||
|
||||
def is_partial_update_supported(self, server_version: Version) -> bool:
|
||||
return server_version > parse("4.4")
|
||||
if not server_version >= parse("4.4"):
|
||||
logger.info(
|
||||
f"{self.server_type}: Server version {server_version} does not support updating playback position.",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import os
|
|||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from typing import Any, Callable
|
||||
from dotenv import load_dotenv
|
||||
import re
|
||||
from pathlib import PureWindowsPath, PurePosixPath
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
mark_file = os.getenv("MARK_FILE", os.getenv("MARKFILE", "mark.log"))
|
||||
|
||||
|
||||
def log_marked(
|
||||
server_type: str,
|
||||
|
|
@ -16,7 +16,8 @@ def log_marked(
|
|||
movie_show: str,
|
||||
episode: str | None = None,
|
||||
duration: float | None = None,
|
||||
):
|
||||
mark_file: str = "mark.log",
|
||||
) -> None:
|
||||
output = f"{server_type}/{server_name}/{username}/{library}/{movie_show}"
|
||||
|
||||
if episode:
|
||||
|
|
@ -25,13 +26,22 @@ def log_marked(
|
|||
if duration:
|
||||
output += f"/{duration}"
|
||||
|
||||
with open(f"{mark_file}", "a", encoding="utf-8") as file:
|
||||
with open(mark_file, "a", encoding="utf-8") as file:
|
||||
file.write(output + "\n")
|
||||
|
||||
|
||||
def get_env_value(env, key: str, default: Any = None):
|
||||
if env and key in env:
|
||||
return env[key]
|
||||
elif os.getenv(key):
|
||||
return os.getenv(key)
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
# Reimplementation of distutils.util.strtobool due to it being deprecated
|
||||
# Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668
|
||||
def str_to_bool(value: str) -> bool:
|
||||
def str_to_bool(value: str | None) -> bool:
|
||||
if not value:
|
||||
return False
|
||||
return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
|
||||
|
|
@ -73,13 +83,13 @@ def future_thread_executor(
|
|||
args: list[tuple[Callable[..., Any], ...]],
|
||||
threads: int | None = None,
|
||||
override_threads: bool = False,
|
||||
max_threads: int | None = None,
|
||||
) -> list[Any]:
|
||||
results: list[Any] = []
|
||||
|
||||
# Determine the number of workers, defaulting to 1 if os.cpu_count() returns None
|
||||
max_threads_env: int = int(os.getenv("MAX_THREADS", 32))
|
||||
cpu_threads: int = os.cpu_count() or 1 # Default to 1 if os.cpu_count() is None
|
||||
workers: int = min(max_threads_env, cpu_threads * 2)
|
||||
workers: int = min(max_threads, cpu_threads * 2) if max_threads else cpu_threads * 2
|
||||
|
||||
# Adjust workers based on threads parameter and override_threads flag
|
||||
if threads is not None:
|
||||
|
|
@ -116,3 +126,13 @@ def parse_string_to_list(string: str | None) -> list[str]:
|
|||
output = string.split(",")
|
||||
|
||||
return output
|
||||
|
||||
|
||||
_WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:") # e.g. C: D:
|
||||
|
||||
|
||||
def filename_from_any_path(p: str) -> str:
|
||||
# Windows-y if UNC (\\server\share), drive letter, or has backslashes
|
||||
if p.startswith("\\\\") or _WINDOWS_DRIVE.match(p) or ("\\" in p and "/" not in p):
|
||||
return PureWindowsPath(p).name
|
||||
return PurePosixPath(p).name
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from src.jellyfin_emby import JellyfinEmby
|
||||
from packaging.version import parse, Version
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class Jellyfin(JellyfinEmby):
|
||||
def __init__(self, baseurl, token):
|
||||
def __init__(self, env, base_url: str, token: str) -> None:
|
||||
authorization = (
|
||||
"MediaBrowser , "
|
||||
'Client="JellyPlex-Watched", '
|
||||
|
|
@ -18,8 +19,14 @@ class Jellyfin(JellyfinEmby):
|
|||
}
|
||||
|
||||
super().__init__(
|
||||
server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers
|
||||
env, server_type="Jellyfin", base_url=base_url, token=token, headers=headers
|
||||
)
|
||||
|
||||
def is_partial_update_supported(self, server_version: Version) -> bool:
|
||||
return server_version >= parse("10.9.0")
|
||||
if not server_version >= parse("10.9.0"):
|
||||
logger.info(
|
||||
f"{self.server_type}: Server version {server_version} does not support updating playback position.",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,10 @@ from src.functions import (
|
|||
search_mapping,
|
||||
)
|
||||
|
||||
from src.emby import Emby
|
||||
from src.jellyfin import Jellyfin
|
||||
from src.plex import Plex
|
||||
|
||||
|
||||
def check_skip_logic(
|
||||
library_title: str,
|
||||
|
|
@ -54,7 +58,7 @@ def check_blacklist_logic(
|
|||
blacklist_library: list[str],
|
||||
blacklist_library_type: list[str],
|
||||
library_other: str | None = None,
|
||||
):
|
||||
) -> str | None:
|
||||
skip_reason = None
|
||||
if isinstance(library_type, (list, tuple, set)):
|
||||
for library_type_item in library_type:
|
||||
|
|
@ -90,7 +94,7 @@ def check_whitelist_logic(
|
|||
whitelist_library: list[str],
|
||||
whitelist_library_type: list[str],
|
||||
library_other: str | None = None,
|
||||
):
|
||||
) -> str | None:
|
||||
skip_reason = None
|
||||
if len(whitelist_library_type) > 0:
|
||||
if isinstance(library_type, (list, tuple, set)):
|
||||
|
|
@ -161,8 +165,8 @@ def filter_libaries(
|
|||
|
||||
|
||||
def setup_libraries(
|
||||
server_1,
|
||||
server_2,
|
||||
server_1: Plex | Jellyfin | Emby,
|
||||
server_2: Plex | Jellyfin | Emby,
|
||||
blacklist_library: list[str],
|
||||
blacklist_library_type: list[str],
|
||||
whitelist_library: list[str],
|
||||
|
|
|
|||
132
src/main.py
132
src/main.py
|
|
@ -2,7 +2,7 @@ import os
|
|||
import traceback
|
||||
import json
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
from dotenv import dotenv_values
|
||||
from time import sleep, perf_counter
|
||||
from loguru import logger
|
||||
|
||||
|
|
@ -13,61 +13,69 @@ from src.library import setup_libraries
|
|||
from src.functions import (
|
||||
parse_string_to_list,
|
||||
str_to_bool,
|
||||
get_env_value,
|
||||
)
|
||||
from src.users import setup_users
|
||||
from src.watched import (
|
||||
cleanup_watched,
|
||||
merge_server_watched,
|
||||
)
|
||||
from src.black_white import setup_black_white_lists
|
||||
from src.connection import generate_server_connections
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
log_file = os.getenv("LOG_FILE", os.getenv("LOGFILE", "log.log"))
|
||||
level = os.getenv("DEBUG_LEVEL", "INFO").upper()
|
||||
|
||||
|
||||
def configure_logger():
|
||||
def configure_logger(log_file: str = "log.log", debug_level: str = "INFO") -> None:
|
||||
# Remove default logger to configure our own
|
||||
logger.remove()
|
||||
|
||||
# Choose log level based on environment
|
||||
# If in debug mode with a "debug" level, use DEBUG; otherwise, default to INFO.
|
||||
|
||||
if level not in ["INFO", "DEBUG", "TRACE"]:
|
||||
if debug_level not in ["INFO", "DEBUG", "TRACE"]:
|
||||
logger.add(sys.stdout)
|
||||
raise Exception("Invalid DEBUG_LEVEL, please choose between INFO, DEBUG, TRACE")
|
||||
raise Exception(
|
||||
f"Invalid DEBUG_LEVEL {debug_level}, please choose between INFO, DEBUG, TRACE"
|
||||
)
|
||||
|
||||
# Add a sink for file logging and the console.
|
||||
logger.add(log_file, level=level, mode="w")
|
||||
logger.add(sys.stdout, level=level)
|
||||
logger.add(log_file, level=debug_level, mode="w")
|
||||
logger.add(sys.stdout, level=debug_level)
|
||||
|
||||
|
||||
def should_sync_server(
|
||||
env,
|
||||
server_1: Plex | Jellyfin | Emby,
|
||||
server_2: Plex | Jellyfin | Emby,
|
||||
) -> bool:
|
||||
sync_from_plex_to_jellyfin = str_to_bool(
|
||||
os.getenv("SYNC_FROM_PLEX_TO_JELLYFIN", "True")
|
||||
get_env_value(env, "SYNC_FROM_PLEX_TO_JELLYFIN", "True")
|
||||
)
|
||||
sync_from_plex_to_plex = str_to_bool(
|
||||
get_env_value(env, "SYNC_FROM_PLEX_TO_PLEX", "True")
|
||||
)
|
||||
sync_from_plex_to_emby = str_to_bool(
|
||||
get_env_value(env, "SYNC_FROM_PLEX_TO_EMBY", "True")
|
||||
)
|
||||
sync_from_plex_to_plex = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_PLEX", "True"))
|
||||
sync_from_plex_to_emby = str_to_bool(os.getenv("SYNC_FROM_PLEX_TO_EMBY", "True"))
|
||||
|
||||
sync_from_jelly_to_plex = str_to_bool(
|
||||
os.getenv("SYNC_FROM_JELLYFIN_TO_PLEX", "True")
|
||||
get_env_value(env, "SYNC_FROM_JELLYFIN_TO_PLEX", "True")
|
||||
)
|
||||
sync_from_jelly_to_jellyfin = str_to_bool(
|
||||
os.getenv("SYNC_FROM_JELLYFIN_TO_JELLYFIN", "True")
|
||||
get_env_value(env, "SYNC_FROM_JELLYFIN_TO_JELLYFIN", "True")
|
||||
)
|
||||
sync_from_jelly_to_emby = str_to_bool(
|
||||
os.getenv("SYNC_FROM_JELLYFIN_TO_EMBY", "True")
|
||||
get_env_value(env, "SYNC_FROM_JELLYFIN_TO_EMBY", "True")
|
||||
)
|
||||
|
||||
sync_from_emby_to_plex = str_to_bool(os.getenv("SYNC_FROM_EMBY_TO_PLEX", "True"))
|
||||
sync_from_emby_to_jellyfin = str_to_bool(
|
||||
os.getenv("SYNC_FROM_EMBY_TO_JELLYFIN", "True")
|
||||
sync_from_emby_to_plex = str_to_bool(
|
||||
get_env_value(env, "SYNC_FROM_EMBY_TO_PLEX", "True")
|
||||
)
|
||||
sync_from_emby_to_jellyfin = str_to_bool(
|
||||
get_env_value(env, "SYNC_FROM_EMBY_TO_JELLYFIN", "True")
|
||||
)
|
||||
sync_from_emby_to_emby = str_to_bool(
|
||||
get_env_value(env, "SYNC_FROM_EMBY_TO_EMBY", "True")
|
||||
)
|
||||
sync_from_emby_to_emby = str_to_bool(os.getenv("SYNC_FROM_EMBY_TO_EMBY", "True"))
|
||||
|
||||
if isinstance(server_1, Plex):
|
||||
if isinstance(server_2, Jellyfin) and not sync_from_plex_to_jellyfin:
|
||||
|
|
@ -111,32 +119,38 @@ def should_sync_server(
|
|||
return True
|
||||
|
||||
|
||||
def main_loop():
|
||||
dryrun = str_to_bool(os.getenv("DRYRUN", "False"))
|
||||
def main_loop(env) -> None:
|
||||
dryrun = str_to_bool(get_env_value(env, "DRYRUN", "False"))
|
||||
logger.info(f"Dryrun: {dryrun}")
|
||||
|
||||
user_mapping = os.getenv("USER_MAPPING", None)
|
||||
if user_mapping:
|
||||
user_mapping = json.loads(user_mapping.lower())
|
||||
user_mapping_env = get_env_value(env, "USER_MAPPING", None)
|
||||
user_mapping = None
|
||||
if user_mapping_env:
|
||||
user_mapping = json.loads(user_mapping_env.lower())
|
||||
logger.info(f"User Mapping: {user_mapping}")
|
||||
|
||||
library_mapping = os.getenv("LIBRARY_MAPPING", None)
|
||||
if library_mapping:
|
||||
library_mapping = json.loads(library_mapping)
|
||||
library_mapping_env = get_env_value(env, "LIBRARY_MAPPING", None)
|
||||
library_mapping = None
|
||||
if library_mapping_env:
|
||||
library_mapping = json.loads(library_mapping_env)
|
||||
logger.info(f"Library Mapping: {library_mapping}")
|
||||
|
||||
# Create (black/white)lists
|
||||
logger.info("Creating (black/white)lists")
|
||||
blacklist_library = parse_string_to_list(os.getenv("BLACKLIST_LIBRARY", None))
|
||||
whitelist_library = parse_string_to_list(os.getenv("WHITELIST_LIBRARY", None))
|
||||
blacklist_library = parse_string_to_list(
|
||||
get_env_value(env, "BLACKLIST_LIBRARY", None)
|
||||
)
|
||||
whitelist_library = parse_string_to_list(
|
||||
get_env_value(env, "WHITELIST_LIBRARY", None)
|
||||
)
|
||||
blacklist_library_type = parse_string_to_list(
|
||||
os.getenv("BLACKLIST_LIBRARY_TYPE", None)
|
||||
get_env_value(env, "BLACKLIST_LIBRARY_TYPE", None)
|
||||
)
|
||||
whitelist_library_type = parse_string_to_list(
|
||||
os.getenv("WHITELIST_LIBRARY_TYPE", None)
|
||||
get_env_value(env, "WHITELIST_LIBRARY_TYPE", None)
|
||||
)
|
||||
blacklist_users = parse_string_to_list(os.getenv("BLACKLIST_USERS", None))
|
||||
whitelist_users = parse_string_to_list(os.getenv("WHITELIST_USERS", None))
|
||||
blacklist_users = parse_string_to_list(get_env_value(env, "BLACKLIST_USERS", None))
|
||||
whitelist_users = parse_string_to_list(get_env_value(env, "WHITELIST_USERS", None))
|
||||
|
||||
(
|
||||
blacklist_library,
|
||||
|
|
@ -158,19 +172,22 @@ def main_loop():
|
|||
|
||||
# Create server connections
|
||||
logger.info("Creating server connections")
|
||||
servers = generate_server_connections()
|
||||
servers = generate_server_connections(env)
|
||||
|
||||
for server_1 in servers:
|
||||
# If server is the final server in the list, then we are done with the loop
|
||||
if server_1 == servers[-1]:
|
||||
break
|
||||
|
||||
# Store a copy of server_1_watched that way it can be used multiple times without having to regather everyones watch history every single time
|
||||
server_1_watched = None
|
||||
|
||||
# Start server_2 at the next server in the list
|
||||
for server_2 in servers[servers.index(server_1) + 1 :]:
|
||||
# Check if server 1 and server 2 are going to be synced in either direction, skip if not
|
||||
if not should_sync_server(server_1, server_2) and not should_sync_server(
|
||||
server_2, server_1
|
||||
):
|
||||
if not should_sync_server(
|
||||
env, server_1, server_2
|
||||
) and not should_sync_server(env, server_2, server_1):
|
||||
continue
|
||||
|
||||
logger.info(f"Server 1: {type(server_1)}: {server_1.info()}")
|
||||
|
|
@ -195,7 +212,9 @@ def main_loop():
|
|||
logger.info(f"Server 2 syncing libraries: {server_2_libraries}")
|
||||
|
||||
logger.info("Creating watched lists", 1)
|
||||
server_1_watched = server_1.get_watched(server_1_users, server_1_libraries)
|
||||
server_1_watched = server_1.get_watched(
|
||||
server_1_users, server_1_libraries, server_1_watched
|
||||
)
|
||||
logger.info("Finished creating watched list server 1")
|
||||
|
||||
server_2_watched = server_2.get_watched(server_2_users, server_2_libraries)
|
||||
|
|
@ -221,8 +240,18 @@ def main_loop():
|
|||
f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}",
|
||||
)
|
||||
|
||||
if should_sync_server(server_2, server_1):
|
||||
if should_sync_server(env, server_2, server_1):
|
||||
logger.info(f"Syncing {server_2.info()} -> {server_1.info()}")
|
||||
|
||||
# Add server_2_watched_filtered to server_1_watched that way the stored version isn't stale for the next server
|
||||
if not dryrun:
|
||||
server_1_watched = merge_server_watched(
|
||||
server_1_watched,
|
||||
server_2_watched_filtered,
|
||||
user_mapping,
|
||||
library_mapping,
|
||||
)
|
||||
|
||||
server_1.update_watched(
|
||||
server_2_watched_filtered,
|
||||
user_mapping,
|
||||
|
|
@ -230,7 +259,7 @@ def main_loop():
|
|||
dryrun,
|
||||
)
|
||||
|
||||
if should_sync_server(server_1, server_2):
|
||||
if should_sync_server(env, server_1, server_2):
|
||||
logger.info(f"Syncing {server_1.info()} -> {server_2.info()}")
|
||||
server_2.update_watched(
|
||||
server_1_watched_filtered,
|
||||
|
|
@ -241,16 +270,25 @@ def main_loop():
|
|||
|
||||
|
||||
@logger.catch
|
||||
def main():
|
||||
run_only_once = str_to_bool(os.getenv("RUN_ONLY_ONCE", "False"))
|
||||
sleep_duration = float(os.getenv("SLEEP_DURATION", "3600"))
|
||||
def main() -> None:
|
||||
# Get environment variables
|
||||
env_file = get_env_value(None, "ENV_FILE", ".env")
|
||||
env = dotenv_values(env_file)
|
||||
|
||||
run_only_once = str_to_bool(get_env_value(env, "RUN_ONLY_ONCE", "False"))
|
||||
sleep_duration = float(get_env_value(env, "SLEEP_DURATION", "3600"))
|
||||
log_file = get_env_value(env, "LOG_FILE", "log.log")
|
||||
debug_level = get_env_value(env, "DEBUG_LEVEL", "INFO")
|
||||
if debug_level:
|
||||
debug_level = debug_level.upper()
|
||||
|
||||
times: list[float] = []
|
||||
while True:
|
||||
try:
|
||||
start = perf_counter()
|
||||
# Reconfigure the logger on each loop so the logs are rotated on each run
|
||||
configure_logger()
|
||||
main_loop()
|
||||
configure_logger(log_file, debug_level)
|
||||
main_loop(env)
|
||||
end = perf_counter()
|
||||
times.append(end - start)
|
||||
|
||||
|
|
|
|||
663
src/plex.py
663
src/plex.py
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from loguru import logger
|
||||
|
||||
from urllib3.poolmanager import PoolManager
|
||||
|
|
@ -10,12 +9,15 @@ from requests.adapters import HTTPAdapter as RequestsHTTPAdapter
|
|||
|
||||
from plexapi.video import Show, Episode, Movie
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.myplex import MyPlexAccount, MyPlexUser
|
||||
from plexapi.library import MovieSection, ShowSection
|
||||
|
||||
from src.functions import (
|
||||
filename_from_any_path,
|
||||
search_mapping,
|
||||
log_marked,
|
||||
str_to_bool,
|
||||
get_env_value,
|
||||
)
|
||||
from src.watched import (
|
||||
LibraryData,
|
||||
|
|
@ -27,15 +29,12 @@ from src.watched import (
|
|||
check_same_identifiers,
|
||||
)
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
generate_guids = str_to_bool(os.getenv("GENERATE_GUIDS", "True"))
|
||||
generate_locations = str_to_bool(os.getenv("GENERATE_LOCATIONS", "True"))
|
||||
|
||||
|
||||
# Bypass hostname validation for ssl. Taken from https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
||||
class HostNameIgnoringAdapter(RequestsHTTPAdapter):
|
||||
def init_poolmanager(self, connections, maxsize, block=..., **pool_kwargs):
|
||||
def init_poolmanager(
|
||||
self, connections: int, maxsize: int | None, block=..., **pool_kwargs
|
||||
) -> None:
|
||||
self.poolmanager = PoolManager(
|
||||
num_pools=connections,
|
||||
maxsize=maxsize,
|
||||
|
|
@ -45,7 +44,9 @@ class HostNameIgnoringAdapter(RequestsHTTPAdapter):
|
|||
)
|
||||
|
||||
|
||||
def extract_guids_from_item(item: Movie | Show | Episode) -> dict[str, str]:
|
||||
def extract_guids_from_item(
|
||||
item: Movie | Show | Episode, generate_guids: bool
|
||||
) -> dict[str, str]:
|
||||
# If GENERATE_GUIDS is set to False, then return an empty dict
|
||||
if not generate_guids:
|
||||
return {}
|
||||
|
|
@ -53,209 +54,127 @@ def extract_guids_from_item(item: Movie | Show | Episode) -> dict[str, str]:
|
|||
guids: dict[str, str] = dict(
|
||||
guid.id.split("://")
|
||||
for guid in item.guids
|
||||
if guid.id is not None and len(guid.id.strip()) > 0
|
||||
if guid.id and len(guid.id.strip()) > 0
|
||||
)
|
||||
|
||||
return guids
|
||||
|
||||
|
||||
def extract_identifiers_from_item(item: Movie | Show | Episode) -> MediaIdentifiers:
|
||||
guids = extract_guids_from_item(item)
|
||||
def extract_identifiers_from_item(
|
||||
item: Movie | Show | Episode,
|
||||
generate_guids: bool,
|
||||
generate_locations: bool,
|
||||
) -> MediaIdentifiers:
|
||||
guids = extract_guids_from_item(item, generate_guids)
|
||||
locations = (
|
||||
tuple([filename_from_any_path(loc) for loc in item.locations])
|
||||
if generate_locations
|
||||
else tuple()
|
||||
)
|
||||
|
||||
if generate_guids:
|
||||
if not guids:
|
||||
logger.debug(
|
||||
f"Plex: {item.title} has no guids{f', locations: {" ".join(item.locations)}' if generate_locations else ''}",
|
||||
)
|
||||
|
||||
if generate_locations:
|
||||
if not locations:
|
||||
logger.debug(
|
||||
f"Plex: {item.title} has no locations{f', guids: {guids}' if generate_guids else ''}",
|
||||
)
|
||||
|
||||
return MediaIdentifiers(
|
||||
title=item.title,
|
||||
locations=(
|
||||
tuple([location.split("/")[-1] for location in item.locations])
|
||||
if generate_locations
|
||||
else tuple()
|
||||
),
|
||||
imdb_id=guids.get("imdb", None),
|
||||
tvdb_id=guids.get("tvdb", None),
|
||||
tmdb_id=guids.get("tmdb", None),
|
||||
locations=locations,
|
||||
imdb_id=guids.get("imdb"),
|
||||
tvdb_id=guids.get("tvdb"),
|
||||
tmdb_id=guids.get("tmdb"),
|
||||
)
|
||||
|
||||
|
||||
def get_mediaitem(item: Movie | Episode, completed=True) -> MediaItem:
|
||||
def get_mediaitem(
|
||||
item: Movie | Episode,
|
||||
completed: bool,
|
||||
generate_guids: bool = True,
|
||||
generate_locations: bool = True,
|
||||
) -> MediaItem:
|
||||
last_viewed_at = item.lastViewedAt
|
||||
viewed_date = datetime.today()
|
||||
|
||||
if last_viewed_at:
|
||||
viewed_date = last_viewed_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
return MediaItem(
|
||||
identifiers=extract_identifiers_from_item(item),
|
||||
status=WatchedStatus(completed=completed, time=item.viewOffset),
|
||||
identifiers=extract_identifiers_from_item(
|
||||
item, generate_guids, generate_locations
|
||||
),
|
||||
status=WatchedStatus(
|
||||
completed=completed, time=item.viewOffset, viewed_date=viewed_date
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def update_user_watched(
|
||||
user: MyPlexAccount,
|
||||
user_plex: PlexServer,
|
||||
library_data: LibraryData,
|
||||
library_name: str,
|
||||
dryrun: bool,
|
||||
):
|
||||
try:
|
||||
# If there are no movies or shows to update, exit early.
|
||||
if not library_data.series and not library_data.movies:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Plex: Updating watched for {user.title} in library {library_name}"
|
||||
)
|
||||
library_section = user_plex.library.section(library_name)
|
||||
|
||||
# Update movies.
|
||||
if library_data.movies:
|
||||
# Search for Plex movies that are currently marked as unwatched.
|
||||
for plex_movie in library_section.search(unwatched=True):
|
||||
plex_identifiers = extract_identifiers_from_item(plex_movie)
|
||||
# Check each stored movie for a match.
|
||||
for stored_movie in library_data.movies:
|
||||
if check_same_identifiers(
|
||||
plex_identifiers, stored_movie.identifiers
|
||||
):
|
||||
# If the stored movie is marked as watched (or has enough progress),
|
||||
# update the Plex movie accordingly.
|
||||
if stored_movie.status.completed:
|
||||
msg = f"Plex: {plex_movie.title} as watched for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
plex_movie.markWatched()
|
||||
logger.success(f"{'[DRYRUN] ' if dryrun else ''}{msg}")
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_movie.title,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
else:
|
||||
msg = f"Plex: {plex_movie.title} as partially watched for {floor(stored_movie.status.time / 60_000)} minutes for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
plex_movie.updateTimeline(stored_movie.status.time)
|
||||
|
||||
logger.success(f"{'[DRYRUN] ' if dryrun else ''}{msg}")
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_movie.title,
|
||||
duration=stored_movie.status.time,
|
||||
)
|
||||
# Once matched, no need to check further.
|
||||
break
|
||||
|
||||
# Update TV Shows (series/episodes).
|
||||
if library_data.series:
|
||||
# For each Plex show in the library section:
|
||||
plex_shows = library_section.search(unwatched=True)
|
||||
for plex_show in plex_shows:
|
||||
# Extract identifiers from the Plex show.
|
||||
plex_show_identifiers = extract_identifiers_from_item(plex_show)
|
||||
# Try to find a matching series in your stored library.
|
||||
for stored_series in library_data.series:
|
||||
if check_same_identifiers(
|
||||
plex_show_identifiers, stored_series.identifiers
|
||||
):
|
||||
logger.info(f"Found matching show for '{plex_show.title}'")
|
||||
# Now update episodes.
|
||||
# Get the list of Plex episodes for this show.
|
||||
plex_episodes = plex_show.episodes()
|
||||
for plex_episode in plex_episodes:
|
||||
plex_episode_identifiers = extract_identifiers_from_item(
|
||||
plex_episode
|
||||
)
|
||||
for stored_ep in stored_series.episodes:
|
||||
if check_same_identifiers(
|
||||
plex_episode_identifiers, stored_ep.identifiers
|
||||
):
|
||||
if stored_ep.status.completed:
|
||||
msg = f"Plex: {plex_show.title} {plex_episode.title} as watched for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
plex_episode.markWatched()
|
||||
|
||||
logger.success(
|
||||
f"{'[DRYRUN] ' if dryrun else ''}{msg}"
|
||||
)
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_show.title,
|
||||
plex_episode.title,
|
||||
)
|
||||
else:
|
||||
msg = f"Plex: {plex_show.title} {plex_episode.title} as partially watched for {floor(stored_ep.status.time / 60_000)} minutes for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
plex_episode.updateTimeline(
|
||||
stored_ep.status.time
|
||||
)
|
||||
|
||||
logger.success(
|
||||
f"{'[DRYRUN] ' if dryrun else ''}{msg}"
|
||||
)
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_show.title,
|
||||
plex_episode.title,
|
||||
stored_ep.status.time,
|
||||
)
|
||||
break # Found a matching episode.
|
||||
break # Found a matching show.
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to update watched for {user.title} in library {library_name}, Error: {e}",
|
||||
2,
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
# class plex accept base url and token and username and password but default with none
|
||||
class Plex:
|
||||
def __init__(
|
||||
self,
|
||||
baseurl=None,
|
||||
token=None,
|
||||
username=None,
|
||||
password=None,
|
||||
servername=None,
|
||||
ssl_bypass=False,
|
||||
session=None,
|
||||
):
|
||||
self.server_type = "Plex"
|
||||
self.baseurl = baseurl
|
||||
self.token = token
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.servername = servername
|
||||
self.ssl_bypass = ssl_bypass
|
||||
env,
|
||||
base_url: str | None = None,
|
||||
token: str | None = None,
|
||||
user_name: str | None = None,
|
||||
password: str | None = None,
|
||||
server_name: str | None = None,
|
||||
ssl_bypass: bool = False,
|
||||
session: requests.Session | None = None,
|
||||
) -> None:
|
||||
self.env = env
|
||||
|
||||
self.server_type: str = "Plex"
|
||||
self.ssl_bypass: bool = ssl_bypass
|
||||
if ssl_bypass:
|
||||
# Session for ssl bypass
|
||||
session = requests.Session()
|
||||
# By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
||||
session.mount("https://", HostNameIgnoringAdapter())
|
||||
self.session = session
|
||||
self.plex = self.login(self.baseurl, self.token)
|
||||
self.admin_user = self.plex.myPlexAccount()
|
||||
self.users = self.get_users()
|
||||
self.plex: PlexServer = self.login(
|
||||
base_url, token, user_name, password, server_name
|
||||
)
|
||||
|
||||
def login(self, baseurl, token):
|
||||
self.base_url: str = self.plex._baseurl
|
||||
|
||||
self.admin_user: MyPlexAccount = self.plex.myPlexAccount()
|
||||
self.users: list[MyPlexUser | MyPlexAccount] = self.get_users()
|
||||
self.generate_guids: bool = str_to_bool(
|
||||
get_env_value(self.env, "GENERATE_GUIDS", "True")
|
||||
)
|
||||
self.generate_locations: bool = str_to_bool(
|
||||
get_env_value(self.env, "GENERATE_LOCATIONS", "True")
|
||||
)
|
||||
|
||||
def login(
|
||||
self,
|
||||
base_url: str | None,
|
||||
token: str | None,
|
||||
user_name: str | None,
|
||||
password: str | None,
|
||||
server_name: str | None,
|
||||
) -> PlexServer:
|
||||
try:
|
||||
if baseurl and token:
|
||||
plex = PlexServer(baseurl, token, session=self.session)
|
||||
elif self.username and self.password and self.servername:
|
||||
if base_url and token:
|
||||
plex: PlexServer = PlexServer(base_url, token, session=self.session)
|
||||
elif user_name and password and server_name:
|
||||
# Login via plex account
|
||||
account = MyPlexAccount(self.username, self.password)
|
||||
plex = account.resource(self.servername).connect()
|
||||
account = MyPlexAccount(user_name, password)
|
||||
plex = account.resource(server_name).connect()
|
||||
else:
|
||||
raise Exception("No complete plex credentials provided")
|
||||
|
||||
return plex
|
||||
except Exception as e:
|
||||
if self.username:
|
||||
msg = f"Failed to login via plex account {self.username}"
|
||||
if user_name:
|
||||
msg = f"Failed to login via plex account {user_name}"
|
||||
logger.error(f"Plex: Failed to login, {msg}, Error: {e}")
|
||||
else:
|
||||
logger.error(f"Plex: Failed to login, Error: {e}")
|
||||
|
|
@ -264,9 +183,9 @@ class Plex:
|
|||
def info(self) -> str:
|
||||
return f"Plex {self.plex.friendlyName}: {self.plex.version}"
|
||||
|
||||
def get_users(self):
|
||||
def get_users(self) -> list[MyPlexUser | MyPlexAccount]:
|
||||
try:
|
||||
users = self.plex.myPlexAccount().users()
|
||||
users: list[MyPlexUser | MyPlexAccount] = self.plex.myPlexAccount().users()
|
||||
|
||||
# append self to users
|
||||
users.append(self.plex.myPlexAccount())
|
||||
|
|
@ -302,7 +221,9 @@ class Plex:
|
|||
logger.error(f"Plex: Failed to get libraries, Error: {e}")
|
||||
raise Exception(e)
|
||||
|
||||
def get_user_library_watched(self, user_name, user_plex, library) -> LibraryData:
|
||||
def get_user_library_watched(
|
||||
self, user_name: str, user_plex: PlexServer, library: MovieSection | ShowSection
|
||||
) -> LibraryData:
|
||||
try:
|
||||
logger.info(
|
||||
f"Plex: Generating watched for {user_name} in library {library.title}",
|
||||
|
|
@ -316,7 +237,14 @@ class Plex:
|
|||
unwatched=False
|
||||
) + library_videos.search(inProgress=True):
|
||||
if video.isWatched or video.viewOffset >= 60000:
|
||||
watched.movies.append(get_mediaitem(video, video.isWatched))
|
||||
watched.movies.append(
|
||||
get_mediaitem(
|
||||
video,
|
||||
video.isWatched,
|
||||
self.generate_guids,
|
||||
self.generate_locations,
|
||||
)
|
||||
)
|
||||
|
||||
elif library.type == "show":
|
||||
# Keep track of processed shows to reduce duplicate shows
|
||||
|
|
@ -327,7 +255,7 @@ class Plex:
|
|||
if show.key in processed_shows:
|
||||
continue
|
||||
processed_shows.append(show.key)
|
||||
show_guids = extract_guids_from_item(show)
|
||||
show_guids = extract_guids_from_item(show, self.generate_guids)
|
||||
episode_mediaitem = []
|
||||
|
||||
# Fetch watched or partially watched episodes
|
||||
|
|
@ -335,7 +263,12 @@ class Plex:
|
|||
viewOffset__gte=60_000
|
||||
):
|
||||
episode_mediaitem.append(
|
||||
get_mediaitem(episode, episode.isWatched)
|
||||
get_mediaitem(
|
||||
episode,
|
||||
episode.isWatched,
|
||||
self.generate_guids,
|
||||
self.generate_locations,
|
||||
)
|
||||
)
|
||||
|
||||
if episode_mediaitem:
|
||||
|
|
@ -346,16 +279,16 @@ class Plex:
|
|||
locations=(
|
||||
tuple(
|
||||
[
|
||||
location.split("/")[-1]
|
||||
filename_from_any_path(location)
|
||||
for location in show.locations
|
||||
]
|
||||
)
|
||||
if generate_locations
|
||||
if self.generate_locations
|
||||
else tuple()
|
||||
),
|
||||
imdb_id=show_guids.get("imdb", None),
|
||||
tvdb_id=show_guids.get("tvdb", None),
|
||||
tmdb_id=show_guids.get("tmdb", None),
|
||||
imdb_id=show_guids.get("imdb"),
|
||||
tvdb_id=show_guids.get("tvdb"),
|
||||
tmdb_id=show_guids.get("tmdb"),
|
||||
),
|
||||
episodes=episode_mediaitem,
|
||||
)
|
||||
|
|
@ -369,9 +302,15 @@ class Plex:
|
|||
)
|
||||
return LibraryData(title=library.title)
|
||||
|
||||
def get_watched(self, users, sync_libraries) -> dict[str, UserData]:
|
||||
def get_watched(
|
||||
self,
|
||||
users: list[MyPlexUser | MyPlexAccount],
|
||||
sync_libraries: list[str],
|
||||
users_watched: dict[str, UserData] = None,
|
||||
) -> dict[str, UserData]:
|
||||
try:
|
||||
users_watched: dict[str, UserData] = {}
|
||||
if not users_watched:
|
||||
users_watched: dict[str, UserData] = {}
|
||||
|
||||
for user in users:
|
||||
if self.admin_user == user:
|
||||
|
|
@ -379,10 +318,7 @@ class Plex:
|
|||
else:
|
||||
token = user.get_token(self.plex.machineIdentifier)
|
||||
if token:
|
||||
user_plex = self.login(
|
||||
self.plex._baseurl,
|
||||
token,
|
||||
)
|
||||
user_plex = self.login(self.base_url, token, None, None, None)
|
||||
else:
|
||||
logger.error(
|
||||
f"Plex: Failed to get token for {user.title}, skipping",
|
||||
|
|
@ -399,107 +335,288 @@ class Plex:
|
|||
if library.title not in sync_libraries:
|
||||
continue
|
||||
|
||||
if user_name not in users_watched:
|
||||
users_watched[user_name] = UserData()
|
||||
|
||||
if library.title in users_watched[user_name].libraries:
|
||||
logger.info(
|
||||
f"Plex: {user_name} {library.title} watched history has already been gathered, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
library_data = self.get_user_library_watched(
|
||||
user_name, user_plex, library
|
||||
)
|
||||
|
||||
if user_name not in users_watched:
|
||||
users_watched[user_name] = UserData()
|
||||
|
||||
users_watched[user_name].libraries[library.title] = library_data
|
||||
|
||||
return users_watched
|
||||
except Exception as e:
|
||||
logger.error(f"Plex: Failed to get watched, Error: {e}")
|
||||
raise Exception(e)
|
||||
logger.error(f"Plex: Failed to get users watched, Error: {e}")
|
||||
return {}
|
||||
|
||||
def update_user_watched(
|
||||
self,
|
||||
user: MyPlexAccount,
|
||||
user_plex: PlexServer,
|
||||
library_data: LibraryData,
|
||||
library_name: str,
|
||||
dryrun: bool,
|
||||
) -> None:
|
||||
# If there are no movies or shows to update, exit early.
|
||||
if not library_data.series and not library_data.movies:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Plex: Updating watched for {user.title} in library {library_name}"
|
||||
)
|
||||
library_section = user_plex.library.section(library_name)
|
||||
if not library_section:
|
||||
logger.error(
|
||||
f"Plex: Library {library_name} not found for {user.title}, skipping",
|
||||
)
|
||||
return
|
||||
|
||||
# Update movies.
|
||||
if library_data.movies:
|
||||
# Search for Plex movies that are currently marked as unwatched.
|
||||
for plex_movie in library_section.search(unwatched=True):
|
||||
plex_identifiers = extract_identifiers_from_item(
|
||||
plex_movie, self.generate_guids, self.generate_locations
|
||||
)
|
||||
# Check each stored movie for a match.
|
||||
for stored_movie in library_data.movies:
|
||||
if check_same_identifiers(
|
||||
plex_identifiers, stored_movie.identifiers
|
||||
):
|
||||
# If the stored movie is marked as watched (or has enough progress),
|
||||
# update the Plex movie accordingly.
|
||||
if stored_movie.status.completed:
|
||||
msg = f"Plex: {plex_movie.title} as watched for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
try:
|
||||
plex_movie.markWatched()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to mark {plex_movie.title} as watched, Error: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.success(f"{'[DRYRUN] ' if dryrun else ''}{msg}")
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_movie.title,
|
||||
None,
|
||||
None,
|
||||
mark_file=get_env_value(
|
||||
self.env, "MARK_FILE", "mark.log"
|
||||
),
|
||||
)
|
||||
else:
|
||||
msg = f"Plex: {plex_movie.title} as partially watched for {floor(stored_movie.status.time / 60_000)} minutes for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
try:
|
||||
plex_movie.updateTimeline(stored_movie.status.time)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to update {plex_movie.title} timeline, Error: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.success(f"{'[DRYRUN] ' if dryrun else ''}{msg}")
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_movie.title,
|
||||
duration=stored_movie.status.time,
|
||||
mark_file=get_env_value(
|
||||
self.env, "MARK_FILE", "mark.log"
|
||||
),
|
||||
)
|
||||
# Once matched, no need to check further.
|
||||
break
|
||||
|
||||
# Update TV Shows (series/episodes).
|
||||
if library_data.series:
|
||||
# For each Plex show in the library section:
|
||||
plex_shows = library_section.search(unwatched=True)
|
||||
for plex_show in plex_shows:
|
||||
# Extract identifiers from the Plex show.
|
||||
plex_show_identifiers = extract_identifiers_from_item(
|
||||
plex_show, self.generate_guids, self.generate_locations
|
||||
)
|
||||
# Try to find a matching series in your stored library.
|
||||
for stored_series in library_data.series:
|
||||
if check_same_identifiers(
|
||||
plex_show_identifiers, stored_series.identifiers
|
||||
):
|
||||
logger.trace(f"Found matching show for '{plex_show.title}'")
|
||||
# Now update episodes.
|
||||
# Get the list of Plex episodes for this show.
|
||||
plex_episodes = plex_show.episodes()
|
||||
for plex_episode in plex_episodes:
|
||||
plex_episode_identifiers = extract_identifiers_from_item(
|
||||
plex_episode,
|
||||
self.generate_guids,
|
||||
self.generate_locations,
|
||||
)
|
||||
for stored_ep in stored_series.episodes:
|
||||
if check_same_identifiers(
|
||||
plex_episode_identifiers, stored_ep.identifiers
|
||||
):
|
||||
if stored_ep.status.completed:
|
||||
msg = f"Plex: {plex_show.title} {plex_episode.title} as watched for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
try:
|
||||
plex_episode.markWatched()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to mark {plex_show.title} {plex_episode.title} as watched, Error: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.success(
|
||||
f"{'[DRYRUN] ' if dryrun else ''}{msg}"
|
||||
)
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_show.title,
|
||||
plex_episode.title,
|
||||
mark_file=get_env_value(
|
||||
self.env, "MARK_FILE", "mark.log"
|
||||
),
|
||||
)
|
||||
else:
|
||||
msg = f"Plex: {plex_show.title} {plex_episode.title} as partially watched for {floor(stored_ep.status.time / 60_000)} minutes for {user.title} in {library_name}"
|
||||
if not dryrun:
|
||||
try:
|
||||
plex_episode.updateTimeline(
|
||||
stored_ep.status.time
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to update {plex_show.title} {plex_episode.title} timeline, Error: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.success(
|
||||
f"{'[DRYRUN] ' if dryrun else ''}{msg}"
|
||||
)
|
||||
log_marked(
|
||||
"Plex",
|
||||
user_plex.friendlyName,
|
||||
user.title,
|
||||
library_name,
|
||||
plex_show.title,
|
||||
plex_episode.title,
|
||||
stored_ep.status.time,
|
||||
mark_file=get_env_value(
|
||||
self.env, "MARK_FILE", "mark.log"
|
||||
),
|
||||
)
|
||||
break # Found a matching episode.
|
||||
break # Found a matching show.
|
||||
|
||||
def update_watched(
|
||||
self,
|
||||
watched_list: dict[str, UserData],
|
||||
user_mapping=None,
|
||||
library_mapping=None,
|
||||
dryrun=False,
|
||||
):
|
||||
try:
|
||||
for user, user_data in watched_list.items():
|
||||
user_other = None
|
||||
# If type of user is dict
|
||||
if user_mapping:
|
||||
user_other = search_mapping(user_mapping, user)
|
||||
user_mapping: dict[str, str] | None = None,
|
||||
library_mapping: dict[str, str] | None = None,
|
||||
dryrun: bool = False,
|
||||
) -> None:
|
||||
for user, user_data in watched_list.items():
|
||||
user_other = None
|
||||
# If type of user is dict
|
||||
if user_mapping:
|
||||
user_other = search_mapping(user_mapping, user)
|
||||
|
||||
for index, value in enumerate(self.users):
|
||||
username_title = (
|
||||
value.username.lower()
|
||||
if value.username
|
||||
else value.title.lower()
|
||||
for index, value in enumerate(self.users):
|
||||
username_title = (
|
||||
value.username.lower() if value.username else value.title.lower()
|
||||
)
|
||||
|
||||
if user.lower() == username_title:
|
||||
user = self.users[index]
|
||||
break
|
||||
elif user_other and user_other.lower() == username_title:
|
||||
user = self.users[index]
|
||||
break
|
||||
|
||||
if self.admin_user == user:
|
||||
user_plex = self.plex
|
||||
else:
|
||||
if isinstance(user, str):
|
||||
logger.debug(
|
||||
f"Plex: {user} is not a plex object, attempting to get object for user",
|
||||
)
|
||||
user = self.plex.myPlexAccount().user(user)
|
||||
|
||||
if user.lower() == username_title:
|
||||
user = self.users[index]
|
||||
break
|
||||
elif user_other and user_other.lower() == username_title:
|
||||
user = self.users[index]
|
||||
break
|
||||
if not isinstance(user, MyPlexUser):
|
||||
logger.error(f"Plex: {user} failed to get PlexUser")
|
||||
continue
|
||||
|
||||
if self.admin_user == user:
|
||||
user_plex = self.plex
|
||||
token = user.get_token(self.plex.machineIdentifier)
|
||||
if token:
|
||||
user_plex = PlexServer(
|
||||
self.base_url,
|
||||
token,
|
||||
session=self.session,
|
||||
)
|
||||
else:
|
||||
if isinstance(user, str):
|
||||
logger.warning(
|
||||
f"Plex: {user} is not a plex object, attempting to get object for user",
|
||||
)
|
||||
user = self.plex.myPlexAccount().user(user)
|
||||
logger.error(
|
||||
f"Plex: Failed to get token for {user.title}, skipping",
|
||||
)
|
||||
continue
|
||||
|
||||
token = user.get_token(self.plex.machineIdentifier)
|
||||
if token:
|
||||
user_plex = PlexServer(
|
||||
self.plex._baseurl,
|
||||
token,
|
||||
session=self.session,
|
||||
)
|
||||
if not user_plex:
|
||||
logger.error(f"Plex: {user} Failed to get PlexServer")
|
||||
continue
|
||||
|
||||
for library_name in user_data.libraries:
|
||||
library_data = user_data.libraries[library_name]
|
||||
library_other = None
|
||||
if library_mapping:
|
||||
library_other = search_mapping(library_mapping, library_name)
|
||||
# if library in plex library list
|
||||
library_list = user_plex.library.sections()
|
||||
if library_name.lower() not in [x.title.lower() for x in library_list]:
|
||||
if library_other:
|
||||
if library_other.lower() in [
|
||||
x.title.lower() for x in library_list
|
||||
]:
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} not found, but {library_other} found, using {library_other}",
|
||||
)
|
||||
library_name = library_other
|
||||
else:
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} or {library_other} not found in library list",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
logger.error(
|
||||
f"Plex: Failed to get token for {user.title}, skipping",
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} not found in library list",
|
||||
)
|
||||
continue
|
||||
|
||||
for library_name in user_data.libraries:
|
||||
library_data = user_data.libraries[library_name]
|
||||
library_other = None
|
||||
if library_mapping:
|
||||
library_other = search_mapping(library_mapping, library_name)
|
||||
# if library in plex library list
|
||||
library_list = user_plex.library.sections()
|
||||
if library_name.lower() not in [
|
||||
x.title.lower() for x in library_list
|
||||
]:
|
||||
if library_other:
|
||||
if library_other.lower() in [
|
||||
x.title.lower() for x in library_list
|
||||
]:
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} not found, but {library_other} found, using {library_other}",
|
||||
)
|
||||
library_name = library_other
|
||||
else:
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} or {library_other} not found in library list",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
logger.info(
|
||||
f"Plex: Library {library_name} not found in library list",
|
||||
)
|
||||
continue
|
||||
|
||||
update_user_watched(
|
||||
try:
|
||||
self.update_user_watched(
|
||||
user,
|
||||
user_plex,
|
||||
library_data,
|
||||
library_name,
|
||||
dryrun,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Plex: Failed to update watched, Error: {e}")
|
||||
raise Exception(e)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Plex: Failed to update watched for {user.title} in {library_name}, Error: {e}",
|
||||
)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.myplex import MyPlexAccount, MyPlexUser
|
||||
from loguru import logger
|
||||
|
||||
from src.emby import Emby
|
||||
|
|
@ -109,7 +109,10 @@ def setup_users(
|
|||
blacklist_users: list[str],
|
||||
whitelist_users: list[str],
|
||||
user_mapping: dict[str, str] | None = None,
|
||||
) -> tuple[list[MyPlexAccount] | dict[str, str], list[MyPlexAccount] | dict[str, str]]:
|
||||
) -> tuple[
|
||||
list[MyPlexAccount | MyPlexUser] | dict[str, str],
|
||||
list[MyPlexAccount | MyPlexUser] | dict[str, str],
|
||||
]:
|
||||
server_1_users = generate_user_list(server_1)
|
||||
server_2_users = generate_user_list(server_2)
|
||||
logger.debug(f"Server 1 users: {server_1_users}")
|
||||
|
|
|
|||
140
src/watched.py
140
src/watched.py
|
|
@ -1,12 +1,14 @@
|
|||
import copy
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from loguru import logger
|
||||
from typing import Any
|
||||
|
||||
from src.functions import search_mapping
|
||||
|
||||
|
||||
class MediaIdentifiers(BaseModel):
|
||||
title: str
|
||||
title: str | None = None
|
||||
|
||||
# File information, will be folder for series and media file for episode/movie
|
||||
locations: tuple[str, ...] = tuple()
|
||||
|
|
@ -20,6 +22,7 @@ class MediaIdentifiers(BaseModel):
|
|||
class WatchedStatus(BaseModel):
|
||||
completed: bool
|
||||
time: int
|
||||
viewed_date: datetime
|
||||
|
||||
|
||||
class MediaItem(BaseModel):
|
||||
|
|
@ -29,17 +32,122 @@ class MediaItem(BaseModel):
|
|||
|
||||
class Series(BaseModel):
|
||||
identifiers: MediaIdentifiers
|
||||
episodes: list[MediaItem] = []
|
||||
episodes: list[MediaItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LibraryData(BaseModel):
|
||||
title: str
|
||||
movies: list[MediaItem] = []
|
||||
series: list[Series] = []
|
||||
movies: list[MediaItem] = Field(default_factory=list)
|
||||
series: list[Series] = Field(default_factory=list)
|
||||
|
||||
|
||||
class UserData(BaseModel):
|
||||
libraries: dict[str, LibraryData] = {}
|
||||
libraries: dict[str, LibraryData] = Field(default_factory=dict)
|
||||
|
||||
|
||||
def merge_mediaitem_data(ep1: MediaItem, ep2: MediaItem) -> MediaItem:
|
||||
"""
|
||||
Merge two MediaItem episodes by comparing their watched status.
|
||||
If one is completed while the other isn't, choose the completed one.
|
||||
If both are completed or both are not, choose the one with the higher time.
|
||||
"""
|
||||
if ep1.status.completed != ep2.status.completed:
|
||||
return ep1 if ep1.status.completed else ep2
|
||||
return ep1 if ep1.status.time >= ep2.status.time else ep2
|
||||
|
||||
|
||||
def merge_series_data(series1: Series, series2: Series) -> Series:
|
||||
"""
|
||||
Merge two Series objects by combining their episodes.
|
||||
For duplicate episodes (determined by check_same_identifiers), merge their watched status.
|
||||
"""
|
||||
merged_series = copy.deepcopy(series1)
|
||||
for ep in series2.episodes:
|
||||
for idx, merged_ep in enumerate(merged_series.episodes):
|
||||
if check_same_identifiers(ep.identifiers, merged_ep.identifiers):
|
||||
merged_series.episodes[idx] = merge_mediaitem_data(merged_ep, ep)
|
||||
break
|
||||
else:
|
||||
merged_series.episodes.append(copy.deepcopy(ep))
|
||||
return merged_series
|
||||
|
||||
|
||||
def merge_library_data(lib1: LibraryData, lib2: LibraryData) -> LibraryData:
|
||||
"""
|
||||
Merge two LibraryData objects by extending movies and merging series.
|
||||
For series, duplicates are determined using check_same_identifiers.
|
||||
"""
|
||||
merged = copy.deepcopy(lib1)
|
||||
|
||||
# Merge movies.
|
||||
for movie in lib2.movies:
|
||||
for idx, merged_movie in enumerate(merged.movies):
|
||||
if check_same_identifiers(movie.identifiers, merged_movie.identifiers):
|
||||
merged.movies[idx] = merge_mediaitem_data(merged_movie, movie)
|
||||
break
|
||||
else:
|
||||
merged.movies.append(copy.deepcopy(movie))
|
||||
|
||||
# Merge series.
|
||||
for series2 in lib2.series:
|
||||
for idx, series1 in enumerate(merged.series):
|
||||
if check_same_identifiers(series1.identifiers, series2.identifiers):
|
||||
merged.series[idx] = merge_series_data(series1, series2)
|
||||
break
|
||||
else:
|
||||
merged.series.append(copy.deepcopy(series2))
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
def merge_user_data(user1: UserData, user2: UserData) -> UserData:
|
||||
"""
|
||||
Merge two UserData objects by merging their libraries.
|
||||
If a library exists in both, merge its content; otherwise, add the new library.
|
||||
"""
|
||||
merged_libraries = copy.deepcopy(user1.libraries)
|
||||
for lib_key, lib_data in user2.libraries.items():
|
||||
if lib_key in merged_libraries:
|
||||
merged_libraries[lib_key] = merge_library_data(
|
||||
merged_libraries[lib_key], lib_data
|
||||
)
|
||||
else:
|
||||
merged_libraries[lib_key] = copy.deepcopy(lib_data)
|
||||
return UserData(libraries=merged_libraries)
|
||||
|
||||
|
||||
def merge_server_watched(
|
||||
watched_list_1: dict[str, UserData],
|
||||
watched_list_2: dict[str, UserData],
|
||||
user_mapping: dict[str, str] | None = None,
|
||||
library_mapping: dict[str, str] | None = None,
|
||||
) -> dict[str, UserData]:
|
||||
"""
|
||||
Merge two dictionaries of UserData while taking into account possible
|
||||
differences in user and library keys via the provided mappings.
|
||||
"""
|
||||
merged_watched = copy.deepcopy(watched_list_1)
|
||||
|
||||
for user_2, user_data in watched_list_2.items():
|
||||
# Determine matching user key.
|
||||
user_key = user_mapping.get(user_2, user_2) if user_mapping else user_2
|
||||
if user_key not in merged_watched:
|
||||
merged_watched[user_2] = copy.deepcopy(user_data)
|
||||
continue
|
||||
|
||||
for lib_key, lib_data in user_data.libraries.items():
|
||||
mapped_lib_key = (
|
||||
library_mapping.get(lib_key, lib_key) if library_mapping else lib_key
|
||||
)
|
||||
if mapped_lib_key not in merged_watched[user_key].libraries:
|
||||
merged_watched[user_key].libraries[lib_key] = copy.deepcopy(lib_data)
|
||||
else:
|
||||
merged_watched[user_key].libraries[mapped_lib_key] = merge_library_data(
|
||||
merged_watched[user_key].libraries[mapped_lib_key],
|
||||
lib_data,
|
||||
)
|
||||
|
||||
return merged_watched
|
||||
|
||||
|
||||
def check_same_identifiers(item1: MediaIdentifiers, item2: MediaIdentifiers) -> bool:
|
||||
|
|
@ -103,8 +211,8 @@ def check_remove_entry(item1: MediaItem, item2: MediaItem) -> bool:
|
|||
def cleanup_watched(
|
||||
watched_list_1: dict[str, UserData],
|
||||
watched_list_2: dict[str, UserData],
|
||||
user_mapping=None,
|
||||
library_mapping=None,
|
||||
user_mapping: dict[str, str] | None = None,
|
||||
library_mapping: dict[str, str] | None = None,
|
||||
) -> dict[str, UserData]:
|
||||
modified_watched_list_1 = copy.deepcopy(watched_list_1)
|
||||
|
||||
|
|
@ -199,11 +307,17 @@ def cleanup_watched(
|
|||
return modified_watched_list_1
|
||||
|
||||
|
||||
def get_other(watched_list, object_1, object_2):
|
||||
def get_other(
|
||||
watched_list: dict[str, Any], object_1: str, object_2: str | None
|
||||
) -> str | None:
|
||||
if object_1 in watched_list:
|
||||
return object_1
|
||||
elif object_2 in watched_list:
|
||||
|
||||
if object_2 and object_2 in watched_list:
|
||||
return object_2
|
||||
else:
|
||||
logger.info(f"{object_1} and {object_2} not found in watched list 2")
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f"{object_1}{' and ' + object_2 if object_2 else ''} not found in watched list 2"
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
|
@ -23,6 +24,8 @@ from src.watched import (
|
|||
cleanup_watched,
|
||||
)
|
||||
|
||||
viewed_date = datetime.today()
|
||||
|
||||
tv_shows_watched_list_1: list[Series] = [
|
||||
Series(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -41,7 +44,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="968589",
|
||||
tvdb_id="295296",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -51,7 +54,9 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="968590",
|
||||
tvdb_id="295297",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=240000, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -61,7 +66,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="968592",
|
||||
tvdb_id="295298",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -82,7 +87,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="4661246",
|
||||
tvdb_id="10009418",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -92,7 +97,9 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="4712059",
|
||||
tvdb_id="10009419",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=240000, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -102,7 +109,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="4712061",
|
||||
tvdb_id="10009420",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -123,7 +130,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="3070048",
|
||||
tvdb_id="8438181",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -133,7 +140,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="4568681",
|
||||
tvdb_id="9829910",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -143,7 +150,7 @@ tv_shows_watched_list_1: list[Series] = [
|
|||
tmdb_id="4497012",
|
||||
tvdb_id="9870382",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -170,7 +177,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="295294",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -180,7 +187,9 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="295295",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=300670),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=300670, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -190,7 +199,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="295298",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -211,7 +220,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="9959300",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -221,7 +230,9 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="10009417",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=300741),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=300741, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -231,7 +242,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="10009420",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -252,7 +263,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="8438181",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -262,7 +273,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="9829910",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -272,7 +283,7 @@ tv_shows_watched_list_2: list[Series] = [
|
|||
tvdb_id="9870382",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -299,7 +310,7 @@ expected_tv_show_watched_list_1: list[Series] = [
|
|||
tmdb_id="968589",
|
||||
tvdb_id="295296",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -309,7 +320,9 @@ expected_tv_show_watched_list_1: list[Series] = [
|
|||
tmdb_id="968590",
|
||||
tvdb_id="295297",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=240000, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -330,7 +343,7 @@ expected_tv_show_watched_list_1: list[Series] = [
|
|||
tmdb_id="4661246",
|
||||
tvdb_id="10009418",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -340,7 +353,9 @@ expected_tv_show_watched_list_1: list[Series] = [
|
|||
tmdb_id="4712059",
|
||||
tvdb_id="10009419",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=240000, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -367,7 +382,7 @@ expected_tv_show_watched_list_2: list[Series] = [
|
|||
tvdb_id="295294",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -377,7 +392,9 @@ expected_tv_show_watched_list_2: list[Series] = [
|
|||
tvdb_id="295295",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=300670),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=300670, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -398,7 +415,7 @@ expected_tv_show_watched_list_2: list[Series] = [
|
|||
tvdb_id="9959300",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -408,7 +425,9 @@ expected_tv_show_watched_list_2: list[Series] = [
|
|||
tvdb_id="10009417",
|
||||
tmdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=300741),
|
||||
status=WatchedStatus(
|
||||
completed=False, time=300741, viewed_date=viewed_date
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -426,7 +445,7 @@ movies_watched_list_1: list[MediaItem] = [
|
|||
tmdb_id="10378",
|
||||
tvdb_id="12352",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -436,7 +455,7 @@ movies_watched_list_1: list[MediaItem] = [
|
|||
tmdb_id="1029575",
|
||||
tvdb_id="351194",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -446,7 +465,7 @@ movies_watched_list_1: list[MediaItem] = [
|
|||
tmdb_id="466420",
|
||||
tvdb_id="135852",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(completed=False, time=240000, viewed_date=viewed_date),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -462,7 +481,7 @@ movies_watched_list_2: list[MediaItem] = [
|
|||
tmdb_id="1029575",
|
||||
tvdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -472,7 +491,7 @@ movies_watched_list_2: list[MediaItem] = [
|
|||
tmdb_id="507089",
|
||||
tvdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -482,7 +501,7 @@ movies_watched_list_2: list[MediaItem] = [
|
|||
tmdb_id="695721",
|
||||
tvdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=301215),
|
||||
status=WatchedStatus(completed=False, time=301215, viewed_date=viewed_date),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -498,7 +517,7 @@ expected_movie_watched_list_1: list[MediaItem] = [
|
|||
tmdb_id="10378",
|
||||
tvdb_id="12352",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -508,7 +527,7 @@ expected_movie_watched_list_1: list[MediaItem] = [
|
|||
tmdb_id="466420",
|
||||
tvdb_id="135852",
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=240000),
|
||||
status=WatchedStatus(completed=False, time=240000, viewed_date=viewed_date),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -524,7 +543,7 @@ expected_movie_watched_list_2: list[MediaItem] = [
|
|||
tmdb_id="507089",
|
||||
tvdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
),
|
||||
MediaItem(
|
||||
identifiers=MediaIdentifiers(
|
||||
|
|
@ -534,7 +553,7 @@ expected_movie_watched_list_2: list[MediaItem] = [
|
|||
tmdb_id="695721",
|
||||
tvdb_id=None,
|
||||
),
|
||||
status=WatchedStatus(completed=False, time=301215),
|
||||
status=WatchedStatus(completed=False, time=301215, viewed_date=viewed_date),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -562,7 +581,7 @@ tv_shows_2_watched_list_1: list[Series] = [
|
|||
tmdb_id="282843",
|
||||
tvdb_id="176357",
|
||||
),
|
||||
status=WatchedStatus(completed=True, time=0),
|
||||
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
362
uv.lock
362
uv.lock
|
|
@ -1,90 +1,97 @@
|
|||
version = 1
|
||||
revision = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
version = "3.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jellyplex-watched"
|
||||
version = "6.1.2"
|
||||
version = "8.3.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "loguru" },
|
||||
|
|
@ -97,7 +104,9 @@ dependencies = [
|
|||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "types-requests" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "ruff" },
|
||||
|
|
@ -106,16 +115,20 @@ lint = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "packaging", specifier = "==24.2" },
|
||||
{ name = "plexapi", specifier = "==4.16.1" },
|
||||
{ name = "pydantic", specifier = "==2.10.6" },
|
||||
{ name = "python-dotenv", specifier = "==1.0.0" },
|
||||
{ name = "requests", specifier = "==2.32.3" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
{ name = "plexapi", specifier = "==4.17.1" },
|
||||
{ name = "pydantic", specifier = "==2.11.7" },
|
||||
{ name = "python-dotenv", specifier = "==1.1.1" },
|
||||
{ name = "requests", specifier = "==2.32.5" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=8.3.4" }]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||
dev = [
|
||||
{ name = "mypy", specifier = ">=1.16.1" },
|
||||
{ name = "pytest", specifier = ">=8.4.1" },
|
||||
{ name = "types-requests", specifier = ">=2.32.0.20250611" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.12.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
|
|
@ -125,121 +138,185 @@ dependencies = [
|
|||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.18.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082, upload-time = "2025-09-11T23:00:41.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107, upload-time = "2025-09-11T22:58:40.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551, upload-time = "2025-09-11T22:58:37.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554, upload-time = "2025-09-11T22:59:38.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933, upload-time = "2025-09-11T22:59:20.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426, upload-time = "2025-09-11T23:00:21.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plexapi"
|
||||
version = "4.16.1"
|
||||
version = "4.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/1c/beeaf8dd489dad13ca4310a7bd9c601da6c6831e7c8ac61a45aadccb742d/plexapi-4.16.1.tar.gz", hash = "sha256:8e62d727e67b69994770196cd83a57783e9194d735aa347f682b4534ce6f0565", size = 153460 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/02/1bebd67c3cd94a45f6c3520da971791b66457535c9771d8e0068746d7bc2/plexapi-4.17.1.tar.gz", hash = "sha256:1e5bfb486bb150e058a80ff4fb9aff9e3efce644c56d52bb5297272e005d8241", size = 154746, upload-time = "2025-08-26T00:11:02.819Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/07/20b8a50b8b78374a49685dfe12739c27a9cd440b13913c2cbeeb50470d1e/PlexAPI-4.16.1-py3-none-any.whl", hash = "sha256:87432226c4cd682b5780b01e8def313285c52bdd57c8e72f66a5cef73ce64530", size = 165325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1c/9fdaa0e1f797dde3c3cb56d7b222109009f70380e7f49fc0ff42d5705409/plexapi-4.17.1-py3-none-any.whl", hash = "sha256:9d51adb112a2b0b7aa91a928c8b5c0dfffc0d51108cea67d86fea08cee06c998", size = 166861, upload-time = "2025-08-26T00:11:00.89Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.0"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
|
|
@ -247,59 +324,84 @@ dependencies = [
|
|||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.6"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.4.20250809"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue