From 327d818a1f1d9105a2670a7a20620f3ffd7fa5ee Mon Sep 17 00:00:00 2001 From: BuriedInCode <6057651+Buried-In-Code@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:56:12 +1200 Subject: [PATCH 1/7] Rename to Visage Improve sync/media with better caching, to not need `simple-clean` --- .github/workflows/publishing.yaml | 2 +- .github/workflows/rich-codex.yaml | 2 +- README.md | 49 +- ...sters_commands.svg => visage_commands.svg} | 0 ...iux-posters_media.svg => visage_media.svg} | 0 ...{mediux-posters_set.svg => visage_set.svg} | 0 ...sters_settings.svg => visage_settings.svg} | 0 ...ediux-posters_sync.svg => visage_sync.svg} | 0 mediux_posters/__main__.py | 658 ------------------ mediux_posters/mediux/__init__.py | 4 - mediux_posters/services/jellyfin/__init__.py | 3 - mediux_posters/services/plex/__init__.py | 3 - pyproject.toml | 20 +- tests/conftest.py | 6 +- tests/jellyfin_test.py | 2 +- tests/mediux_test.py | 7 +- tests/plex_test.py | 2 +- uv.lock | 283 ++++---- {mediux_posters => visage}/__init__.py | 19 +- visage/__main__.py | 556 +++++++++++++++ visage/cli/__init__.py | 3 + visage/cli/settings.py | 19 + {mediux_posters => visage}/console.py | 0 {mediux_posters => visage}/errors.py | 0 visage/mediux/__init__.py | 4 + {mediux_posters => visage}/mediux/schemas.py | 8 +- {mediux_posters => visage}/mediux/service.py | 29 +- .../services/__init__.py | 6 +- .../services/_base/__init__.py | 4 +- .../services/_base/schemas.py | 2 +- .../services/_base/service.py | 8 +- visage/services/jellyfin/__init__.py | 3 + .../services/jellyfin/schemas.py | 10 +- .../services/jellyfin/service.py | 29 +- visage/services/plex/__init__.py | 3 + .../services/plex/schemas.py | 10 +- .../services/plex/service.py | 22 +- visage/services/service_cache.py | 76 ++ {mediux_posters => visage}/settings.py | 6 +- {mediux_posters => visage}/utils.py | 4 +- 40 files changed, 939 insertions(+), 923 deletions(-) rename docs/img/{mediux-posters_commands.svg => visage_commands.svg} (100%) rename docs/img/{mediux-posters_media.svg => visage_media.svg} (100%) rename docs/img/{mediux-posters_set.svg => visage_set.svg} (100%) rename docs/img/{mediux-posters_settings.svg => visage_settings.svg} (100%) rename docs/img/{mediux-posters_sync.svg => visage_sync.svg} (100%) delete mode 100644 mediux_posters/__main__.py delete mode 100644 mediux_posters/mediux/__init__.py delete mode 100644 mediux_posters/services/jellyfin/__init__.py delete mode 100644 mediux_posters/services/plex/__init__.py rename {mediux_posters => visage}/__init__.py (85%) create mode 100644 visage/__main__.py create mode 100644 visage/cli/__init__.py create mode 100644 visage/cli/settings.py rename {mediux_posters => visage}/console.py (100%) rename {mediux_posters => visage}/errors.py (100%) create mode 100644 visage/mediux/__init__.py rename {mediux_posters => visage}/mediux/schemas.py (94%) rename {mediux_posters => visage}/mediux/service.py (94%) rename {mediux_posters => visage}/services/__init__.py (63%) rename {mediux_posters => visage}/services/_base/__init__.py (61%) rename {mediux_posters => visage}/services/_base/schemas.py (98%) rename {mediux_posters => visage}/services/_base/service.py (91%) create mode 100644 visage/services/jellyfin/__init__.py rename {mediux_posters => visage}/services/jellyfin/schemas.py (95%) rename {mediux_posters => visage}/services/jellyfin/service.py (92%) create mode 100644 visage/services/plex/__init__.py rename {mediux_posters => visage}/services/plex/schemas.py (95%) rename {mediux_posters => visage}/services/plex/service.py (95%) create mode 100644 visage/services/service_cache.py rename {mediux_posters => visage}/settings.py (95%) rename {mediux_posters => visage}/utils.py (96%) diff --git a/.github/workflows/publishing.yaml b/.github/workflows/publishing.yaml index b5e9139..aac818b 100644 --- a/.github/workflows/publishing.yaml +++ b/.github/workflows/publishing.yaml @@ -30,7 +30,7 @@ jobs: environment: name: pypi - url: https://pypi.org/p/Mediux-Posters + url: https://pypi.org/p/Visage name: Publish to PyPI permissions: id-token: write diff --git a/.github/workflows/rich-codex.yaml b/.github/workflows/rich-codex.yaml index 6664101..71da6a9 100644 --- a/.github/workflows/rich-codex.yaml +++ b/.github/workflows/rich-codex.yaml @@ -4,7 +4,7 @@ on: push: paths: - README.md - - mediux_posters/__main__.py + - visage/__main__.py workflow_dispatch: permissions: diff --git a/README.md b/README.md index 1b3605b..2ddf334 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Mediux Posters +# Visage -[![PyPI - Python](https://img.shields.io/pypi/pyversions/Mediux-Posters.svg?logo=PyPI&label=Python&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) -[![PyPI - Status](https://img.shields.io/pypi/status/Mediux-Posters.svg?logo=PyPI&label=Status&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) -[![PyPI - Version](https://img.shields.io/pypi/v/Mediux-Posters.svg?logo=PyPI&label=Version&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) -[![PyPI - License](https://img.shields.io/pypi/l/Mediux-Posters.svg?logo=PyPI&label=License&style=flat-square)](https://opensource.org/licenses/MIT) +[![PyPI - Python](https://img.shields.io/pypi/pyversions/Visage.svg?logo=PyPI&label=Python&style=flat-square)](https://pypi.python.org/pypi/Visage/) +[![PyPI - Status](https://img.shields.io/pypi/status/Visage.svg?logo=PyPI&label=Status&style=flat-square)](https://pypi.python.org/pypi/Visage/) +[![PyPI - Version](https://img.shields.io/pypi/v/Visage.svg?logo=PyPI&label=Version&style=flat-square)](https://pypi.python.org/pypi/Visage/) +[![PyPI - License](https://img.shields.io/pypi/l/Visage.svg?logo=PyPI&label=License&style=flat-square)](https://opensource.org/licenses/MIT) [![Pre-Commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&style=flat-square)](https://github.com/pre-commit/pre-commit) [![Ruff](https://img.shields.io/badge/ruff-enabled-brightgreen?logo=ruff&style=flat-square)](https://github.com/astral-sh/ruff) -[![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Mediux-Posters.svg?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Mediux-Posters/graphs/contributors) -[![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Mediux-Posters/testing.yaml?branch=main&logo=Github&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Mediux-Posters/actions/workflows/testing.yaml) +[![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Visage.svg?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Visage/graphs/contributors) +[![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Visage/testing.yaml?branch=main&logo=Github&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Visage/actions/workflows/testing.yaml) Fetches Show, Movie, and Collection cover art from Mediux and updates Plex/Jellyfin using TMDB IDs. @@ -20,38 +20,45 @@ _Jellyfin Collections are not yet supported._ ### Pipx 1. Ensure you have [Pipx](https://pipx.pypa.io/stable/) installed: `pipx --version` -2. Install the project: `pipx install Mediux-Posters` +2. Install the project: `pipx install visage` ## Usage -
Mediux-Posters Commands +
visage Commands - ![`uv run Mediux-Posters --help`](docs/img/mediux-posters_commands.svg) + ![`uv run visage --help`](docs/img/visage_commands.svg)
-
Mediux-Posters sync +
visage sync - ![`uv run Mediux-Posters sync --help`](docs/img/mediux-posters_sync.svg) + ![`uv run visage sync --help`](docs/img/visage_sync.svg)
-
Mediux-Posters media +
visage media - ![`uv run Mediux-Posters media --help`](docs/img/mediux-posters_media.svg) + ![`uv run visage media --help`](docs/img/visage_media.svg)
-
Mediux-Posters set +
visage set - ![`uv run Mediux-Posters set --help`](docs/img/mediux-posters_set.svg) + ![`uv run visage set --help`](docs/img/visage_set.svg)
-
Mediux-Posters settings +### Visage settings Commands +
visage settings view - ![`uv run Mediux-Posters settings --help`](docs/img/mediux-posters_settings.svg) + ![`uv run visage settings view --help`](docs/img/visage_settings_view.svg) + +
+
visage settings locate + + + ![`uv run visage settings locate --help`](docs/img/visage_settings_locate.svg)
@@ -64,11 +71,11 @@ _Jellyfin Collections are not yet supported._ - **Collection:** Poster and Backdrop - **Movie:** Poster and Backdrop - To enable support for Collections in Plex, each Collection needs to have a label with its tmdb-id, in the format of `tmdb-`\ - If using Kometa, refer to [this comment](https://github.com/Buried-In-Code/Mediux-Posters/issues/12#issuecomment-2622002859) to have Kometa add these labels. + If using Kometa, refer to [this comment](https://github.com/Buried-In-Code/Visage/issues/12#issuecomment-2622002859) to have Kometa add these labels. ## Settings -To set Plex and/or Jellyfin details, update the file: `~/.config/mediux-posters/settings.toml`. +To set Plex and/or Jellyfin details, update the file: `~/.config/visage/settings.toml`. File will be created on first run. ### Example File @@ -100,7 +107,7 @@ token = "" - `kometa_integration` - If enabled, will remove the `Overlay` label from any media Mediux-Posters uploads a poster/backdrop/title-card to. + If enabled, will remove the `Overlay` label from any media Visage uploads a poster/backdrop/title-card to. - `only_priority_usernames` diff --git a/docs/img/mediux-posters_commands.svg b/docs/img/visage_commands.svg similarity index 100% rename from docs/img/mediux-posters_commands.svg rename to docs/img/visage_commands.svg diff --git a/docs/img/mediux-posters_media.svg b/docs/img/visage_media.svg similarity index 100% rename from docs/img/mediux-posters_media.svg rename to docs/img/visage_media.svg diff --git a/docs/img/mediux-posters_set.svg b/docs/img/visage_set.svg similarity index 100% rename from docs/img/mediux-posters_set.svg rename to docs/img/visage_set.svg diff --git a/docs/img/mediux-posters_settings.svg b/docs/img/visage_settings.svg similarity index 100% rename from docs/img/mediux-posters_settings.svg rename to docs/img/visage_settings.svg diff --git a/docs/img/mediux-posters_sync.svg b/docs/img/visage_sync.svg similarity index 100% rename from docs/img/mediux-posters_sync.svg rename to docs/img/visage_sync.svg diff --git a/mediux_posters/__main__.py b/mediux_posters/__main__.py deleted file mode 100644 index 4f1298e..0000000 --- a/mediux_posters/__main__.py +++ /dev/null @@ -1,658 +0,0 @@ -import logging -from collections.abc import Generator -from enum import Enum -from pathlib import Path -from platform import python_version -from typing import Annotated, Protocol, TypeVar - -from typer import Abort, Context, Exit, Option, Typer - -from mediux_posters import __version__, get_cache_root, setup_logging -from mediux_posters.errors import ServiceError -from mediux_posters.mediux import CollectionSet, File, FileType, Mediux, MovieSet, ShowSet -from mediux_posters.services import ( - BaseService, - Collection, - Episode, - Jellyfin, - Movie, - Plex, - Season, - Show, -) -from mediux_posters.settings import Settings -from mediux_posters.utils import CONSOLE, MediaType, delete_folder, get_cached_image, slugify - -app = Typer() -LOGGER = logging.getLogger("mediux-posters") - - -@app.callback(invoke_without_command=True) -def common( - ctx: Context, - version: Annotated[ - bool | None, Option("--version", is_eager=True, help="Show the version and exit.") - ] = None, -) -> None: - if ctx.invoked_subcommand: - return - if version: - CONSOLE.print(f"Mediux Posters v{__version__}") - raise Exit - - -@app.command(name="settings", help="Display the current and default settings.") -def view_settings() -> None: - Settings.load().display() - - -def setup( - full_clean: bool = False, debug: bool = False -) -> tuple[Settings, Mediux, list[BaseService]]: - setup_logging(debug=debug) - LOGGER.info("Python v%s", python_version()) - LOGGER.info("Mediux Posters v%s", __version__) - - if full_clean: - LOGGER.info("Cleaning Cache: %s", get_cache_root()) - delete_folder(folder=get_cache_root()) - settings = Settings.load().save() - if not settings.mediux.token: - LOGGER.fatal("Mediux Posters requires a Mediux Token to be set.") - raise Abort - mediux = Mediux(base_url=settings.mediux.base_url, token=settings.mediux.token) - service_list = [] - if settings.plex.token: - service_list.append(Plex(base_url=settings.plex.base_url, token=settings.plex.token)) - if settings.jellyfin.token: - service_list.append( - Jellyfin(base_url=settings.jellyfin.base_url, token=settings.jellyfin.token) - ) - return settings, mediux, service_list - - -T = TypeVar("T", bound="MediuxSet") - - -class MediuxSet(Protocol): - username: str - - -def filter_sets( - set_list: list[T], priority_usernames: list[str], only_priority_usernames: bool -) -> Generator[T]: - if not set_list: - return - - # Yield priority usernames first - for username in priority_usernames: - yield from [x for x in set_list if x.username == username] - - # If allowed, yield remaining sets - if not only_priority_usernames: - yield from [x for x in set_list if x.username not in priority_usernames] - - -def find_file(files: list[File], file_type: FileType, id_value: int) -> File | None: - id_fields = ["show_id", "season_id", "episode_id", "collection_id", "movie_id"] - for x in files: - if x.file_type != file_type: - continue - for field in id_fields: - if getattr(x, field, None) == id_value: - return x - return None - - -def process_image( - entry: Show | Season | Episode | Collection | Movie, - file_type: FileType, - set_data: ShowSet | CollectionSet | MovieSet, - id_value: int, - parent: str, - filename: str, - should_log: bool, - mediux: Mediux, - service: BaseService, - kometa_integration: bool = False, -) -> bool: - attribute = f"{file_type.name.lower()}_uploaded" - if not getattr(entry, attribute) and ( - file_info := find_file(files=set_data.files, file_type=file_type, id_value=id_value) - ): - image_file = get_cached_image(parent, filename) - if image_file.exists(): - setattr(entry, attribute, True) - else: - if should_log: - LOGGER.info( - "[Mediux] Downloading '%s' by '%s'", set_data.set_title, set_data.username - ) - should_log = False - mediux.download_image(file_id=file_info.id, output=image_file) - setattr( - entry, - attribute, - service.upload_image( - object_id=entry.id, image_file=image_file, kometa_integration=kometa_integration - ), - ) - return should_log - - -def process_set_data( - entry: Show | Season | Episode | Collection | Movie, - set_data: ShowSet | CollectionSet | MovieSet, - mediux: Mediux, - service: BaseService, - media_type: MediaType, - kometa_integration: bool = False, -) -> bool: - should_log = True - if entry.all_posters_uploaded: - LOGGER.info( - "[%s] All posters have been uploaded, skipping remaining sets", type(service).__name__ - ) - return False - - should_log = process_image( - entry=entry, - file_type=FileType.POSTER, - set_data=set_data, - id_value=entry.tmdb_id, - parent=slugify(value=entry.display_name), - filename=f"{FileType.POSTER}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - should_log = process_image( - entry=entry, - file_type=FileType.BACKDROP, - set_data=set_data, - id_value=entry.tmdb_id, - parent=slugify(value=entry.display_name), - filename=f"{FileType.BACKDROP}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - if media_type is MediaType.SHOW: - for season in service.list_seasons(show_id=entry.id): - entry.seasons.append(season) - mediux_season = next( - (x for x in set_data.show.seasons if x.number == season.number), None - ) - if not mediux_season: - continue - should_log = process_image( - entry=season, - file_type=FileType.POSTER, - set_data=set_data, - id_value=mediux_season.id, - parent=slugify(value=entry.display_name), - filename=f"s{season.number:02}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - for episode in service.list_episodes(show_id=entry.id, season_id=season.id): - season.episodes.append(episode) - mediux_episode = next( - (x for x in mediux_season.episodes if x.number == episode.number), None - ) - if not mediux_episode: - continue - should_log = process_image( - entry=episode, - file_type=FileType.TITLE_CARD, - set_data=set_data, - id_value=mediux_episode.id, - parent=slugify(value=entry.display_name), - filename=f"s{season.number:02}e{episode.number:02}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - elif media_type is MediaType.COLLECTION: - for movie in service.list_collection_movies(collection_id=entry.id): - entry.movies.append(movie) - mediux_movie = next( - (x for x in set_data.collection.movies if x.tmdb_id == movie.tmdb_id), None - ) - if not mediux_movie: - continue - should_log = process_image( - entry=movie, - file_type=FileType.POSTER, - set_data=set_data, - id_value=movie.tmdb_id, - parent=slugify(value=movie.display_name), - filename=f"{FileType.POSTER}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - should_log = process_image( - entry=movie, - file_type=FileType.BACKDROP, - set_data=set_data, - id_value=movie.tmdb_id, - parent=slugify(value=movie.display_name), - filename=f"{FileType.BACKDROP}.jpg", - should_log=should_log, - mediux=mediux, - service=service, - kometa_integration=kometa_integration, - ) - return True - - -class ServiceChoice(str, Enum): - PLEX = Plex.__name__ - JELLYFIN = Jellyfin.__name__ - - -@app.command( - name="sync", help="Synchronize posters by fetching data from Mediux and updating your services." -) -def sync_posters( - skip_services: Annotated[ - list[ServiceChoice], - Option( - "--skip-service", - "-S", - show_default=False, - case_sensitive=False, - default_factory=list, - help="List of Services to skip. " - "Specify this option multiple times for skipping multiple services.", - ), - ], - skip_media_types: Annotated[ - list[MediaType], - Option( - "--skip-type", - "-T", - show_default=False, - case_sensitive=False, - default_factory=list, - help="List of MediaTypes to skip. " - "Specify this option multiple times for skipping multiple types.", - ), - ], - skip_libraries: Annotated[ - list[str], - Option( - "--skip-library", - "-L", - show_default=False, - default_factory=list, - help="List of libraries to skip. " - "Specify this option multiple times for skipping multiple libraries. ", - ), - ], - start: Annotated[ - int, Option("--start", "-s", help="The starting index for processing media.") - ] = 0, - end: Annotated[ - int, Option("--end", "-e", help="The ending index for processing media.") - ] = 100_000, - full_clean: Annotated[ - bool, - Option( - "--full-clean", "-C", show_default=False, help="Delete the whole cache before starting." - ), - ] = False, - debug: Annotated[ - bool, - Option( - "--debug", - help="Enable debug mode to show extra logging information for troubleshooting.", - ), - ] = False, -) -> None: - settings, mediux, service_list = setup(full_clean=full_clean, debug=debug) - skip_services = [x.value for x in skip_services] - - for idx, service in enumerate(service_list): - if type(service).__name__ in skip_services: - continue - CONSOLE.rule( - f"[{idx + 1}/{len(service_list)}] {type(service).__name__} Service", - align="left", - style="title", - ) - - for media_type in MediaType: - if media_type in skip_media_types: - continue - - try: - with CONSOLE.status( - f"[{type(service).__name__}] Fetching {media_type.value} media" - ): - entries = service.list(media_type=media_type, skip_libraries=skip_libraries)[ - start:end - ] - except ServiceError as err: - LOGGER.warning("[%s] %s", type(service).__name__, err) - break - for index, entry in enumerate(entries): - CONSOLE.rule( - f"[{index + 1}/{len(entries)}] {entry.display_name} [tmdb-{entry.tmdb_id}]", - align="left", - style="subtitle", - ) - - LOGGER.info( - "[%s] Searching Mediux for '%s' sets", - type(service).__name__, - entry.display_name, - ) - try: - set_list = mediux.list_sets( - media_type=media_type, - tmdb_id=entry.tmdb_id, - exclude_usernames=settings.exclude_usernames, - ) - except ServiceError as err: - LOGGER.error(err) - set_list = [] - - filtered_sets = filter_sets( - set_list=set_list, - priority_usernames=settings.priority_usernames, - only_priority_usernames=settings.only_priority_usernames, - ) - for set_data in filtered_sets: - if not process_set_data( - entry=entry, - set_data=set_data, - mediux=mediux, - service=service, - media_type=media_type, - kometa_integration=settings.kometa_integration, - ): - break - - -@app.command( - name="media", help="Manually set posters for specific Mediux media using a file or URLs." -) -def media_posters( - skip_services: Annotated[ - list[ServiceChoice], - Option( - "--skip-service", - "-S", - show_default=False, - case_sensitive=False, - default_factory=list, - help="List of Services to skip. " - "Specify this option multiple times for skipping multiple services.", - ), - ], - urls: Annotated[ - list[str], - Option( - "--url", - "-u", - default_factory=list, - show_default=False, - help="List of URLs from Mediux to process. " - "Specify this option multiple times for multiple URLs.", - ), - ], - file: Annotated[ - Path | None, - Option( - dir_okay=False, - exists=True, - show_default=False, - help="Path to a file containing URLs from Mediux, one per line. " - "If set, the file must exist and cannot be a directory.", - ), - ] = None, - full_clean: Annotated[ - bool, - Option( - "--full-clean", "-C", show_default=False, help="Delete the whole cache before starting." - ), - ] = False, - simple_clean: Annotated[ - bool, - Option( - "--simple-clean", - "-c", - show_default=False, - help="Delete the cache of each media instead of the whole cache.", - ), - ] = False, - debug: Annotated[ - bool, - Option( - "--debug", - help="Enable debug mode to show extra logging information for troubleshooting.", - ), - ] = False, -) -> None: - settings, mediux, service_list = setup(full_clean=full_clean, debug=debug) - skip_services = [x.value for x in skip_services] - url_list = [x.strip() for x in file.read_text().splitlines()] if file else urls - - for idx, service in enumerate(service_list): - if type(service).__name__ in skip_services: - continue - CONSOLE.rule( - f"[{idx + 1}/{len(service_list)}] {type(service).__name__} Service", - align="left", - style="title", - ) - - for index, url in enumerate(url_list): - media_type = None - for type_key in MediaType: - if url.startswith(f"{Mediux.WEB_URL}/{type_key}s"): - media_type = type_key - break - if not media_type: - LOGGER.warning("Unknown Mediux url: '%s'", url) - continue - - tmdb_id = int(url.split("/")[-1]) - try: - with CONSOLE.status(f"Searching {type(service).__name__} for TMDB id: '{tmdb_id}'"): - entry = service.get(media_type=media_type, tmdb_id=tmdb_id) - if not entry: - LOGGER.warning( - "[%s] Unable to find a %s with a Tmdb Id of '%d'", - type(service).__name__, - media_type.value.capitalize(), - tmdb_id, - ) - continue - except ServiceError as err: - LOGGER.warning("[%s] %s", type(service).__name__, err) - break - CONSOLE.rule( - f"[{index + 1}/{len(url_list)}] {entry.display_name} [tmdb-{entry.tmdb_id}]", - align="left", - style="subtitle", - ) - if simple_clean: - LOGGER.info("Cleaning %s from cache", entry.display_name) - delete_folder(folder=get_cached_image(slugify(value=entry.display_name))) - if media_type is MediaType.COLLECTION: - for movie in entry.movies: - delete_folder(folder=get_cached_image(slugify(value=movie.display_name))) - - try: - set_list = mediux.list_sets( - media_type=media_type, - tmdb_id=tmdb_id, - exclude_usernames=settings.exclude_usernames, - ) - except ServiceError as err: - if debug: - LOGGER.error(err) - set_list = [] - - filtered_sets = filter_sets( - set_list=set_list, - priority_usernames=settings.priority_usernames, - only_priority_usernames=settings.only_priority_usernames, - ) - for set_data in filtered_sets: - if not process_set_data( - entry=entry, - set_data=set_data, - mediux=mediux, - service=service, - media_type=media_type, - kometa_integration=settings.kometa_integration, - ): - break - - -@app.command(name="set", help="Manually set posters for specific Mediux sets using a file or URLs.") -def set_posters( - skip_services: Annotated[ - list[ServiceChoice], - Option( - "--skip-service", - "-S", - show_default=False, - case_sensitive=False, - default_factory=list, - help="List of Services to skip. " - "Specify this option multiple times for skipping multiple services.", - ), - ], - urls: Annotated[ - list[str], - Option( - "--url", - "-u", - default_factory=list, - show_default=False, - help="List of URLs from Mediux to process. " - "Specify this option multiple times for multiple URLs.", - ), - ], - file: Annotated[ - Path | None, - Option( - dir_okay=False, - exists=True, - show_default=False, - help="Path to a file containing URLs from Mediux, one per line. " - "If set, the file must exist and cannot be a directory.", - ), - ] = None, - full_clean: Annotated[ - bool, - Option( - "--full-clean", "-C", show_default=False, help="Delete the whole cache before starting." - ), - ] = False, - simple_clean: Annotated[ - bool, - Option( - "--simple-clean", - "-c", - show_default=False, - help="Delete the cache of each media instead of the whole cache.", - ), - ] = False, - debug: Annotated[ - bool, - Option( - "--debug", - help="Enable debug mode to show extra logging information for troubleshooting.", - ), - ] = False, -) -> None: - settings, mediux, service_list = setup(full_clean=full_clean, debug=debug) - skip_services = [x.value for x in skip_services] - url_list = [x.strip() for x in file.read_text().splitlines()] if file else urls - - for idx, service in enumerate(service_list): - if type(service).__name__ in skip_services: - continue - CONSOLE.rule( - f"[{idx + 1}/{len(service_list)}] {type(service).__name__} Service", - align="left", - style="title", - ) - - for index, url in enumerate(url_list): - if not url.startswith(f"{Mediux.WEB_URL}/sets"): - continue - set_id = int(url.split("/")[-1]) - set_data = ( - mediux.get_show_set(set_id=set_id) - or mediux.get_collection_set(set_id=set_id) - or mediux.get_movie_set(set_id=set_id) - ) - if not set_data: - LOGGER.warning("[Mediux] Unable to find a Set with an Id of '%d'", set_id) - continue - - media_type, tmdb_id = ( - (MediaType.SHOW, set_data.show.tmdb_id) - if isinstance(set_data, ShowSet) - else (MediaType.COLLECTION, set_data.collection.tmdb_id) - if isinstance(set_data, CollectionSet) - else (MediaType.MOVIE, set_data.movie.tmdb_id) - if isinstance(set_data, MovieSet) - else (None, None) - ) - if not tmdb_id: - LOGGER.error("Set does not contain a TmdbId, this should not be possible") - continue - - CONSOLE.rule( - f"[{index + 1}/{len(url_list)}] {set_data.set_title} [tmdb-{tmdb_id}]", - align="left", - style="subtitle", - ) - try: - with CONSOLE.status( - f"Searching {type(service).__name__} for '{set_data.set_title} [{tmdb_id}]'" - ): - entry = service.get(media_type=media_type, tmdb_id=tmdb_id) - if not entry: - LOGGER.warning( - "[%s] Unable to find any media with a Tmdb Id of '%d'", - type(service).__name__, - tmdb_id, - ) - continue - except ServiceError as err: - LOGGER.warning("[%s] %s", type(service).__name__, err) - break - if simple_clean: - LOGGER.info("Cleaning %s from cache", entry.display_name) - delete_folder(folder=get_cached_image(slugify(value=entry.display_name))) - if media_type is MediaType.COLLECTION: - for movie in entry.movies: - delete_folder(folder=get_cached_image(slugify(value=movie.display_name))) - - process_set_data( - entry=entry, - set_data=set_data, - mediux=mediux, - service=service, - media_type=media_type, - kometa_integration=settings.kometa_integration, - ) - - -if __name__ == "__main__": - app(prog_name="Mediux-Posters") diff --git a/mediux_posters/mediux/__init__.py b/mediux_posters/mediux/__init__.py deleted file mode 100644 index 149df1b..0000000 --- a/mediux_posters/mediux/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ["CollectionSet", "File", "FileType", "Mediux", "MovieSet", "ShowSet"] - -from mediux_posters.mediux.schemas import CollectionSet, File, FileType, MovieSet, ShowSet -from mediux_posters.mediux.service import Mediux diff --git a/mediux_posters/services/jellyfin/__init__.py b/mediux_posters/services/jellyfin/__init__.py deleted file mode 100644 index 601ab5b..0000000 --- a/mediux_posters/services/jellyfin/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["Jellyfin"] - -from mediux_posters.services.jellyfin.service import Jellyfin diff --git a/mediux_posters/services/plex/__init__.py b/mediux_posters/services/plex/__init__.py deleted file mode 100644 index 6e652e6..0000000 --- a/mediux_posters/services/plex/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["Plex"] - -from mediux_posters.services.plex.service import Plex diff --git a/pyproject.toml b/pyproject.toml index ea023ea..e5761b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dev = [ ] tests = [ "pytest >= 8.4.0", - "pytest-cov >= 6.1.1", + "pytest-cov >= 6.2.1", "pytest-httpx >= 0.35.0", "tox >= 4.26.0", "tox-uv >= 1.26.0" @@ -32,7 +32,7 @@ classifiers = [ dependencies = [ "graphql-query >= 1.4.0", "httpx[http2] >= 0.28.1", - "pydantic >= 2.11.5", + "pydantic >= 2.11.7", "ratelimit >= 2.2.1", "rich >= 14.0.0", "tomli >= 2.2.1 ; python_version < '3.11'", @@ -43,29 +43,30 @@ description = "Fetches Show, Movie, and Collection cover art from Mediux and upd dynamic = ["version"] keywords = [] license = {text = "MIT"} -name = "mediux-posters" +name = "visage" readme = "README.md" requires-python = ">= 3.10" [project.scripts] -Mediux-Posters = "mediux_posters.__main__:app" +Mediux-Posters = "visage.__main__:app" +visage = "visage.__main__:app" [project.urls] -Homepage = "https://pypi.org/project/Mediux-Posters" -Issues = "https://github.com/Buried-In-Code/Mediux-Posters/issues" -Source = "https://github.com/Buried-In-Code/Mediux-Posters" +Homepage = "https://pypi.org/project/Visage" +Issues = "https://github.com/Buried-In-Code/Visage/issues" +Source = "https://github.com/Buried-In-Code/Visage" [tool.coverage.report] show_missing = true [tool.coverage.run] -source = ["mediux_posters"] +source = ["visage"] [tool.hatch.build.targets.sdist] exclude = [".github/"] [tool.hatch.version] -path = "mediux_posters/__init__.py" +path = "visage/__init__.py" [tool.pytest.ini_options] addopts = ["--cov"] @@ -92,6 +93,7 @@ ignore = [ "FIX", "PLR0912", "PLR0913", + "PLR0915", "PLW2901", "TCH", "TD", diff --git a/tests/conftest.py b/tests/conftest.py index 4e4f553..c907570 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,9 @@ import pytest -from mediux_posters.mediux import Mediux -from mediux_posters.services.jellyfin import Jellyfin -from mediux_posters.services.plex import Plex +from visage.mediux import Mediux +from visage.services.jellyfin import Jellyfin +from visage.services.plex import Plex @pytest.fixture(scope="session") diff --git a/tests/jellyfin_test.py b/tests/jellyfin_test.py index 45398e4..b9ba4bc 100644 --- a/tests/jellyfin_test.py +++ b/tests/jellyfin_test.py @@ -6,7 +6,7 @@ import pytest from pytest_httpx import HTTPXMock -from mediux_posters.services.jellyfin import Jellyfin +from visage.services.jellyfin import Jellyfin @pytest.mark.httpx_mock( diff --git a/tests/mediux_test.py b/tests/mediux_test.py index ab511e4..24a1285 100644 --- a/tests/mediux_test.py +++ b/tests/mediux_test.py @@ -3,9 +3,9 @@ from pathlib import Path from tempfile import NamedTemporaryFile -from mediux_posters.mediux import Mediux -from mediux_posters.mediux.schemas import FileType -from mediux_posters.utils import MediaType +from visage.mediux import Mediux +from visage.mediux.schemas import FileType +from visage.utils import MediaType def test_list_show_sets(mediux_session: Mediux) -> None: @@ -171,6 +171,7 @@ def test_download_image(mediux_session: Mediux) -> None: "Downloaded image does not match expected image" ) + def test_season_name_none(mediux_session: Mediux) -> None: results = mediux_session.list_sets(media_type=MediaType.SHOW, tmdb_id=95479) assert len(results) != 0 diff --git a/tests/plex_test.py b/tests/plex_test.py index a264cec..f1362b6 100644 --- a/tests/plex_test.py +++ b/tests/plex_test.py @@ -6,7 +6,7 @@ import pytest from pytest_httpx import HTTPXMock -from mediux_posters.services.plex import Plex +from visage.services.plex import Plex @pytest.mark.httpx_mock( diff --git a/uv.lock b/uv.lock index 1e8b9cc..39e7ee5 100644 --- a/uv.lock +++ b/uv.lock @@ -28,20 +28,20 @@ wheels = [ [[package]] name = "cachetools" -version = "6.0.0" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/b0/f539a1ddff36644c28a61490056e5bae43bd7386d9f9c69beae2d7e7d6d1/cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf", size = 30160, upload-time = "2025-05-23T20:01:13.076Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c3/8bb087c903c95a570015ce84e0c23ae1d79f528c349cbc141b5c4e250293/cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e", size = 10964, upload-time = "2025-05-23T20:01:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" }, ] [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] @@ -85,66 +85,66 @@ wheels = [ [[package]] name = "coverage" -version = "7.8.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, - { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, - { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, - { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, - { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, - { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, - { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, - { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, - { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, - { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, - { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, - { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, - { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, - { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, - { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, - { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, - { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, - { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, - { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, - { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, - { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, - { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, - { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, - { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, - { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, - { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, - { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, - { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, - { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, - { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, - { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, - { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, - { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, - { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, - { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, - { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, +version = "7.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, + { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, + { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, + { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, + { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, + { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, + { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, + { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, + { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, + { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, + { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, ] [package.optional-dependencies] @@ -386,54 +386,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mediux-posters" -source = { editable = "." } -dependencies = [ - { name = "graphql-query" }, - { name = "httpx", extra = ["http2"] }, - { name = "pydantic" }, - { name = "ratelimit" }, - { name = "rich" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomli-w" }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pre-commit" }, -] -tests = [ - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-httpx" }, - { name = "tox" }, - { name = "tox-uv" }, -] - -[package.metadata] -requires-dist = [ - { name = "graphql-query", specifier = ">=1.4.0" }, - { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.11.5" }, - { name = "ratelimit", specifier = ">=2.2.1" }, - { name = "rich", specifier = ">=14.0.0" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.2.1" }, - { name = "tomli-w", specifier = ">=1.2.0" }, - { name = "typer", specifier = ">=0.16.0" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "pre-commit", specifier = ">=4.2.0" }] -tests = [ - { name = "pytest", specifier = ">=8.4.0" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "pytest-httpx", specifier = ">=0.35.0" }, - { name = "tox", specifier = ">=4.26.0" }, - { name = "tox-uv", specifier = ">=1.26.0" }, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -488,7 +440,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.5" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -496,9 +448,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +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/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, + { 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]] @@ -630,15 +582,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -858,27 +811,27 @@ wheels = [ [[package]] name = "uv" -version = "0.7.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/6d/bc877dfd6bb2cfc10be6214062cd6f3a680b48aada457b57e4bc05deb4c5/uv-0.7.10.tar.gz", hash = "sha256:b37b8afd1429268f82690063652173ad65b541dcacf49c11e27ec823da4a55db", size = 3280661, upload-time = "2025-06-03T16:29:43.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/42/f706108425cb4774885a0d854722cdabf176e2492771a59e52807e5e9db3/uv-0.7.10-py3-none-linux_armv6l.whl", hash = "sha256:831c318ad75b5bc0122d59ad394914f6e637a3b822b0de9d64e5af4bdfd5e694", size = 17010267, upload-time = "2025-06-03T16:29:01.387Z" }, - { url = "https://files.pythonhosted.org/packages/9a/40/cafd080caffe0a262c5d44e39f4094e173cfc634e8c0ae8cceef07ad9fc6/uv-0.7.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:14b0d0c743045552f189556034602d686114fa5810a1858bb001eea5d979ce72", size = 17105484, upload-time = "2025-06-03T16:29:04.935Z" }, - { url = "https://files.pythonhosted.org/packages/a8/d2/471ffaccbd6fdd11419d172862cf4f10a50374013b37444a62c3d0257076/uv-0.7.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c37556c238cd71604ce3e4d36d1f14efe33eb7db82e44b77e0471ed3207b419a", size = 15825286, upload-time = "2025-06-03T16:29:07.485Z" }, - { url = "https://files.pythonhosted.org/packages/c7/fd/657a08f892844cc5a11cdaf587ba8c74643099e152f29472e6737505d893/uv-0.7.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8a3f6da7da5d3cff722276fcc6dd74b2a26919b31a7be85fac7346749a7781de", size = 16361493, upload-time = "2025-06-03T16:29:09.622Z" }, - { url = "https://files.pythonhosted.org/packages/08/8e/85c2253c9d62e0700440b6761231ef573fb2f07ec51cc04a5297554ab099/uv-0.7.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e0c6d02737e8f596609d783f0861413e264e0a39bf4a0004744564f33597a44", size = 16791192, upload-time = "2025-06-03T16:29:11.706Z" }, - { url = "https://files.pythonhosted.org/packages/1b/3e/be09e408c100d97b03079e575d9a50665e425feb972c14067963d79e7f15/uv-0.7.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b6f5aef962fb30dc1c95e3a53e630c0fbc809c8eba00ea0a57719a3eff5111b", size = 17551515, upload-time = "2025-06-03T16:29:14.017Z" }, - { url = "https://files.pythonhosted.org/packages/bf/83/9eb5df77b5968aa75ce1e3dddda6a900bb8c5d3d25fd358a60d88437e70b/uv-0.7.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9ec23930c1bb380ba3a43106b8914c0a64b991d3fc9d1abc9909e15bf0dd9fa6", size = 18393368, upload-time = "2025-06-03T16:29:16.351Z" }, - { url = "https://files.pythonhosted.org/packages/41/ee/7f9a7f5635a1eaa4d76fb21087af69f92cebf8fc47618ecb7953af3f8fc3/uv-0.7.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c14f75e079949b46477a2d4bbab92597323f6ec88e03b65ca481ea78843649b1", size = 18083341, upload-time = "2025-06-03T16:29:18.562Z" }, - { url = "https://files.pythonhosted.org/packages/95/05/0e823400786fe08645498d9009929d7f2303549ab4bd47956c9f745ca2ed/uv-0.7.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29bdf4bafa805a7501dee917c112145a29ba391894b176ab8cd644442511be3", size = 18245972, upload-time = "2025-06-03T16:29:21.596Z" }, - { url = "https://files.pythonhosted.org/packages/9a/2c/ccd90aa61cd3e31ce842da92c0a73e0b1820dfaba4c8b543ce434af4824e/uv-0.7.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d0d97e279a2f1ed3e62275278d67c56cdfb05e9319cc780a18e1d73191648d2", size = 17746353, upload-time = "2025-06-03T16:29:24.037Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/4ab5e27b90004752a52684871ae5dd40a9f75e1286eb02913260965a81c4/uv-0.7.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:13bebf00b3a04734d8023ac761b2dd4f542115d14bdaae6736ec42a9d8e68920", size = 16620646, upload-time = "2025-06-03T16:29:26.706Z" }, - { url = "https://files.pythonhosted.org/packages/89/07/a648cf428853e2fce2f9e1948b81271ac948dd2f2ae17f6601b5b4f3eba2/uv-0.7.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1ab3acbaa82110edcb073e6bf621cfe7f72b9c49d189a1022f8aeb757ee2766d", size = 16709036, upload-time = "2025-06-03T16:29:28.984Z" }, - { url = "https://files.pythonhosted.org/packages/4f/98/d5e01b6bbc765a96ea59624200acedb3858dc9dd52f314f713f1e42239ca/uv-0.7.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:2207f6659663b8a64cd05766759129c0528b8c6f179c28d62b4a65ba43835874", size = 17117728, upload-time = "2025-06-03T16:29:31.247Z" }, - { url = "https://files.pythonhosted.org/packages/61/d5/2158d1aca21afeb20180c8e8f82ff5fc11534579fb5f911d2ad9c19abd8b/uv-0.7.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0e7d46405a1018c371f5193c24b25af8d2b00ac76c7e26243ca1c9f0c900c5a7", size = 17885281, upload-time = "2025-06-03T16:29:33.526Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9b/ad6ba66926e7258030d8889df35f821172e9e1ddf4703c404d50724b85f7/uv-0.7.10-py3-none-win32.whl", hash = "sha256:afd0b26d3d45d7544a87d530c47cfb3cea67aea5c0860203bc64e2576cfee0ea", size = 17325882, upload-time = "2025-06-03T16:29:36.474Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/0ec999ed01c25be26248feaa689a9ac9b2ad44d7a9d365466b4825a72fc3/uv-0.7.10-py3-none-win_amd64.whl", hash = "sha256:e1bd83509ebe5a065a39d2725b535adea6f4b9aaa74443315e0ef76ff94537e8", size = 18792420, upload-time = "2025-06-03T16:29:38.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/8b/e01feb7ebeefd418a9226e786ea15fa6a6747fb789f1a81b2f3e2a6f1fe3/uv-0.7.10-py3-none-win_arm64.whl", hash = "sha256:f0c22a94e583ef573a7c1cb19543b263e934ffdf6f6d22dc090246714a92fdc0", size = 17472097, upload-time = "2025-06-03T16:29:41.344Z" }, +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/08/1bcafa9077965de397d927f291827a77a915d75567b42c3ad6bb6a2e0bcd/uv-0.7.13.tar.gz", hash = "sha256:05f3c03c4ea55d294f3da725b6c2c2ff544754c18552da7594def4ec3889dcfb", size = 3308772, upload-time = "2025-06-12T22:23:10.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/4e/cdf97c831be960e13c7db28b6ba226e5bdbfee9a38f6099687c7a395ec52/uv-0.7.13-py3-none-linux_armv6l.whl", hash = "sha256:59915aec9fd2b845708a76ddc6c0639cfc99b6e2811854ea2425ee7552aff0e9", size = 17073615, upload-time = "2025-06-12T20:58:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/27217e8a7a457bc9c068d99f2d860706649130755fa377306d75a326ce0b/uv-0.7.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9c457a84cfbe2019ba301e14edd3e1c950472abd0b87fc77622ab3fc475ba012", size = 17099887, upload-time = "2025-06-12T20:58:50.272Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/1d7ec2211732512ae43d7176242fea3eea1915c83565953014bbafcb6be2/uv-0.7.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f828174e15a557d3bc0f809de76135c3b66bcbf524657f8ced9d22fc978b89c", size = 15800953, upload-time = "2025-06-12T20:58:52.897Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5b/81ea6ec50890a064b37d8f8dc097901768f73c747d965ffd96f1ebdfacea/uv-0.7.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:88fcf2bfbb53309531a850af50d2ea75874099b19d4159625d0b4f88c53494b9", size = 16355391, upload-time = "2025-06-12T20:58:55.146Z" }, + { url = "https://files.pythonhosted.org/packages/64/24/92a30049a74bf17c9c4ffbf36462c5ff593617c2d0b78efb3c9d55293746/uv-0.7.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:721b058064150fc1c6d88e277af093d1b4f8bb7a59546fe9969d9ff7dbe3f6fd", size = 16819352, upload-time = "2025-06-12T20:58:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/74/fe/8b4de3addc375ba00bd1a515a79aaccbb3a600bc66c03e5fd159d6928066/uv-0.7.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f28e70baadfebe71dcc2d9505059b988d75e903fc62258b102eb87dc4b6994a3", size = 17518852, upload-time = "2025-06-12T20:58:59.538Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/e9c14c6aba0316da7fe30b0dac4f8f6d1155d0422dcff1138b85f4eb4c08/uv-0.7.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9d2952a1e74c7027347c74cee1cb2be09121a5290db38498b8b17ff585f73748", size = 18405034, upload-time = "2025-06-12T20:59:01.747Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/a2f4147fa2714ce765104e2984abcdaa0605725b10ca70bee7de4a1ba88c/uv-0.7.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a51006c7574e819308d92a3452b22d5bd45ef8593a4983b5856aa7cb8220885f", size = 18120055, upload-time = "2025-06-12T20:59:03.997Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/f4381c1aa4d3d13ff36359e4176cd34d1da1548ba2a6c763a953b282ede0/uv-0.7.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33837aca7bdf02d47554d5d44f9e71756ee17c97073b07b4afead25309855bc7", size = 18283737, upload-time = "2025-06-12T20:59:06.437Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/f2e96cec5e4cf65d7fde89b5dcf9540ddacf42e8e39de2fa0332614e55a8/uv-0.7.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5786a29e286f2cc3cbda13a357fd9a4dd5bf1d7448a9d3d842b26b4f784a3a86", size = 17755308, upload-time = "2025-06-12T20:59:08.837Z" }, + { url = "https://files.pythonhosted.org/packages/34/6d/d7a1af8ece6d5cac5287d00e15b9650eb9d3203606add4cd035009d52de6/uv-0.7.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1afdbfcabc3425b383141ba42d413841c0a48b9ee0f4da65459313275e3cea84", size = 16611463, upload-time = "2025-06-12T20:59:10.971Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e8/27294e3067295db8f54dbe8a1f64b6e3000adc1cba29f953c440bc184a5d/uv-0.7.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:866cad0d04a7de1aaa3c5cbef203f9d3feef9655972dcccc3283d60122db743b", size = 16759459, upload-time = "2025-06-12T22:22:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/36f055eb1b9a44d60eed9a5aa93cf0f23660a19ab07a5ef085331dd9fc0a/uv-0.7.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:527a12d0c2f4d15f72b275b6f4561ae92af76dd59b4624796fddd45867f13c33", size = 17108780, upload-time = "2025-06-12T22:22:48.412Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/0f09c0de0896d04b4bb81bdd7833643f055e8a5c2c04f8a2ddf3a74453d8/uv-0.7.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4efa555b217e15767f0691a51d435f7bb2b0bf473fdfd59f173aeda8a93b8d17", size = 17900498, upload-time = "2025-06-12T22:22:50.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6f/ee435b4ec3903617b5f592c0077ef0c1e22c41e2ab872be2134b223aabb2/uv-0.7.13-py3-none-win32.whl", hash = "sha256:b1af81e57d098b21b28f42ec756f0e26dce2341d59ba4e4f11759bc3ca2c0a99", size = 17329841, upload-time = "2025-06-12T22:22:57.517Z" }, + { url = "https://files.pythonhosted.org/packages/af/05/c16e2b9369d440e3c85439257bd679c3a92bdd248015238a8848941828f6/uv-0.7.13-py3-none-win_amd64.whl", hash = "sha256:8c0c29a2089ff9011d6c3abccd272f3ee6d0e166dae9e5232099fd83d26104d9", size = 18820166, upload-time = "2025-06-12T22:23:05.224Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/68fd18d5190515f9ddb31cc2f14e21d1b38bee721ece2d43c42e13646fa3/uv-0.7.13-py3-none-win_arm64.whl", hash = "sha256:e077dcac19e564cae8b4223b7807c2f617a59938f8142ca77fc6348ae9c6d0aa", size = 17456260, upload-time = "2025-06-12T22:23:08.227Z" }, ] [[package]] @@ -894,3 +847,51 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] + +[[package]] +name = "visage" +source = { editable = "." } +dependencies = [ + { name = "graphql-query" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "ratelimit" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, +] +tests = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-httpx" }, + { name = "tox" }, + { name = "tox-uv" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphql-query", specifier = ">=1.4.0" }, + { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "ratelimit", specifier = ">=2.2.1" }, + { name = "rich", specifier = ">=14.0.0" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.2.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "typer", specifier = ">=0.16.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pre-commit", specifier = ">=4.2.0" }] +tests = [ + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "tox", specifier = ">=4.26.0" }, + { name = "tox-uv", specifier = ">=1.26.0" }, +] diff --git a/mediux_posters/__init__.py b/visage/__init__.py similarity index 85% rename from mediux_posters/__init__.py rename to visage/__init__.py index b3d86c3..bfb2f33 100644 --- a/mediux_posters/__init__.py +++ b/visage/__init__.py @@ -1,4 +1,5 @@ __all__ = [ + "__project__", "__version__", "get_cache_root", "get_config_root", @@ -6,47 +7,43 @@ "get_state_root", "setup_logging", ] +__project__ = "visage" __version__ = "0.5.0" import logging import os -from functools import cache from pathlib import Path from rich.logging import RichHandler from rich.traceback import install -from mediux_posters.console import CONSOLE +from visage.console import CONSOLE -@cache def get_cache_root() -> Path: cache_home = os.getenv("XDG_CACHE_HOME", default=str(Path.home() / ".cache")) - folder = Path(cache_home).resolve() / "mediux-posters" + folder = Path(cache_home).resolve() / __project__ folder.mkdir(exist_ok=True, parents=True) return folder -@cache def get_config_root() -> Path: config_home = os.getenv("XDG_CONFIG_HOME", default=str(Path.home() / ".config")) - folder = Path(config_home).resolve() / "mediux-posters" + folder = Path(config_home).resolve() / __project__ folder.mkdir(exist_ok=True, parents=True) return folder -@cache def get_data_root() -> Path: data_home = os.getenv("XDG_DATA_HOME", default=str(Path.home() / ".local" / "share")) - folder = Path(data_home).resolve() / "mediux-posters" + folder = Path(data_home).resolve() / __project__ folder.mkdir(exist_ok=True, parents=True) return folder -@cache def get_state_root() -> Path: data_home = os.getenv("XDG_STATE_HOME", default=str(Path.home() / ".local" / "state")) - folder = Path(data_home).resolve() / "mediux-posters" + folder = Path(data_home).resolve() / __project__ folder.mkdir(exist_ok=True, parents=True) return folder @@ -65,7 +62,7 @@ def setup_logging(debug: bool = False) -> None: ) console_handler.setLevel(logging.DEBUG if debug else logging.INFO) console_handler.setFormatter(logging.Formatter("%(message)s")) - file_handler = logging.FileHandler(filename=get_state_root() / "mediux-posters.log") + file_handler = logging.FileHandler(filename=get_state_root() / f"{__project__}.log") file_handler.setLevel(logging.DEBUG if debug else logging.INFO) logging.basicConfig( format="[%(asctime)s] [%(levelname)-8s] {%(name)s} | %(message)s", diff --git a/visage/__main__.py b/visage/__main__.py new file mode 100644 index 0000000..8218feb --- /dev/null +++ b/visage/__main__.py @@ -0,0 +1,556 @@ +import logging +from collections.abc import Generator +from enum import Enum +from platform import python_version +from typing import Annotated, Protocol, TypeVar + +from typer import Abort, Argument, Context, Exit, Option, Typer + +from visage import __project__, __version__, get_cache_root, setup_logging +from visage.cli import settings_app +from visage.console import CONSOLE +from visage.errors import ServiceError +from visage.mediux import CollectionSet, File, FileType, Mediux, MovieSet, ShowSet +from visage.services import BaseService, Collection, Episode, Jellyfin, Movie, Plex, Season, Show +from visage.settings import Settings +from visage.utils import MediaType, delete_folder, get_cached_image, slugify + +LOGGER = logging.getLogger(__project__) +app = Typer(no_args_is_help=True) +app.add_typer(settings_app, name="settings") +HIGH_VALUE = 1_000_000 + + +class ServiceOption(str, Enum): + PLEX = Plex.__name__ + JELLYFIN = Jellyfin.__name__ + + +def setup( + skip_services: list[ServiceOption], clean: bool, debug: bool = False +) -> tuple[Settings, Mediux, list[BaseService]]: + setup_logging(debug=debug) + LOGGER.info("%s v%s", __project__, __version__) + LOGGER.info("Python v%s", python_version()) + + if clean: + LOGGER.info("Cleaning cache directory: '%s'", get_cache_root()) + delete_folder(folder=get_cache_root()) + + settings = Settings.load().save() + + if not settings.mediux.token: + LOGGER.error("Missing Mediux token, check your settings") + raise Abort + mediux = Mediux(base_url=settings.mediux.base_url, token=settings.mediux.token) + if not mediux.validate(): + raise Abort + + services = [] + skip_services = [x.value for x in skip_services] + if Plex.__name__ not in skip_services and settings.plex.token: + plex = Plex(base_url=settings.plex.base_url, token=settings.plex.token) + if plex.validate(): + services.append(plex) + if Jellyfin.__name__ not in skip_services and settings.jellyfin.token: + jellyfin = Jellyfin(base_url=settings.jellyfin.base_url, token=settings.jellyfin.token) + if jellyfin.validate(): + services.append(jellyfin) + if not services: + LOGGER.error("No services configured, check your settings") + raise Abort + + return settings, mediux, services + + +T = TypeVar("T", bound="MediuxSet") + + +class MediuxSet(Protocol): + username: str + + +def filter_sets( + set_list: list[T], priority_usernames: list[str], only_priority_usernames: bool +) -> Generator[T]: + if not set_list: + return + # Priority usernames first + for username in priority_usernames: + yield from [x for x in set_list if x.username == username] + if not only_priority_usernames: + # Remaining sets + yield from [x for x in set_list if x.username not in priority_usernames] + + +def process_set_data( + entry: Show | Collection | Movie, + set_data: ShowSet | CollectionSet | MovieSet, + mediux: Mediux, + service: BaseService, + priority_usernames: list[str], + force: bool = False, + kometa_integration: bool = False, +) -> bool: + should_log = True + if entry.all_posters_uploaded: + return False + + def get_creator_rank(creator: str | None) -> int: + if creator and creator in priority_usernames: + return priority_usernames.index(creator) + return HIGH_VALUE + + def find_matching_file(file_type: FileType, id_value: int) -> File | None: + for f in set_data.files: + if f.file_type == file_type and any( + getattr(f, field, None) == id_value + for field in ["show_id", "season_id", "episode_id", "collection_id", "movie_id"] + ): + return f + return None + + def process_image( + obj: Show | Season | Episode | Collection | Movie, + file_type: FileType, + id_value: int, + parent: str, + filename: str, + ) -> None: + nonlocal should_log + uploaded_attr = f"{file_type.name.lower()}_uploaded" + if getattr(obj, uploaded_attr): + return + + file = find_matching_file(file_type=file_type, id_value=id_value) + if not file: + return + + image_file = get_cached_image(parent, filename) + existing = service.cache.select(object_id=obj.id, file_type=file_type) + if force: + existing = None + + if existing: + existing_rank = get_creator_rank(creator=existing.creator) + new_rank = get_creator_rank(creator=set_data.username) + if new_rank > existing_rank or ( + new_rank == existing_rank and set_data.id != existing.set_id + ): + setattr(obj, uploaded_attr, True) + return + if set_data.id == existing.set_id and file.last_updated <= existing.last_updated: + setattr(obj, uploaded_attr, True) + return + + image_file.unlink(missing_ok=True) + if should_log: + LOGGER.info( + "[Mediux] Downloading '%s' by '%s'", + set_data.set_title.replace("'", "'"), + set_data.username, + ) + should_log = False + mediux.download_image(file_id=file.id, output=image_file) + + success = service.upload_image( + object_id=obj.id, image_file=image_file, kometa_integration=kometa_integration + ) + setattr(obj, uploaded_attr, success) + if success: + service.cache.insert( + object_id=obj.id, + file_type=file_type, + creator=set_data.username, + set_id=set_data.id, + last_updated=file.last_updated, + ) + + process_image( + obj=entry, + file_type=FileType.POSTER, + id_value=entry.tmdb_id, + parent=slugify(value=entry.display_name), + filename="poster.jpg", + ) + process_image( + obj=entry, + file_type=FileType.BACKDROP, + id_value=entry.tmdb_id, + parent=slugify(value=entry.display_name), + filename="backdrop.jpg", + ) + if isinstance(entry, Show) and isinstance(set_data, ShowSet): + for season in entry.seasons or service.list_seasons(show_id=entry.id): + entry.seasons.append(season) + mediux_season = next( + (x for x in set_data.show.seasons if x.number == season.number), None + ) + if not mediux_season: + continue + process_image( + obj=season, + file_type=FileType.POSTER, + id_value=mediux_season.id, + parent=slugify(value=entry.display_name), + filename=f"s{season.number:02}.jpg", + ) + for episode in season.episodes or service.list_episodes( + show_id=entry.id, season_id=season.id + ): + season.episodes.append(episode) + mediux_episode = next( + (x for x in mediux_season.episodes if x.number == episode.number), None + ) + if not mediux_episode: + continue + process_image( + obj=episode, + file_type=FileType.TITLE_CARD, + id_value=mediux_episode.id, + parent=slugify(value=entry.display_name), + filename=f"s{season.number:02}e{episode.number:02}.jpg", + ) + elif isinstance(entry, Collection) and isinstance(set_data, CollectionSet): + for movie in service.list_collection_movies(collection_id=entry.id): + entry.movies.append(movie) + mediux_movie = next( + (x for x in set_data.collection.movies if x.id == movie.tmdb_id), None + ) + if not mediux_movie: + continue + process_image( + obj=movie, + file_type=FileType.POSTER, + id_value=mediux_movie.id, + parent=slugify(value=movie.display_name), + filename="poster.jpg", + ) + process_image( + obj=movie, + file_type=FileType.BACKDROP, + id_value=mediux_movie.id, + parent=slugify(value=movie.display_name), + filename="backdrop.jpg", + ) + return True + + +@app.callback(invoke_without_command=True) +def common( + ctx: Context, + version: Annotated[ + bool | None, Option("--version", is_eager=True, help="Show the version.") + ] = None, +) -> None: + if ctx.invoked_subcommand: + return + if version: + CONSOLE.print(f"{__project__.title()} v{__version__}") + raise Exit + + +@app.command( + name="sync", help="Synchronize posters by fetching data from Mediux and updating your services." +) +def sync_posters( + skip_services: Annotated[ + list[ServiceOption], + Option( + "--skip-service", + "-S", + show_default=False, + case_sensitive=False, + default_factory=list, + help="List of Services to skip. " + "Specify this option multiple times for skipping multiple services.", + ), + ], + skip_media_types: Annotated[ + list[MediaType], + Option( + "--skip-type", + "-T", + show_default=False, + case_sensitive=False, + default_factory=list, + help="List of MediaTypes to skip. " + "Specify this option multiple times for skipping multiple types.", + ), + ], + skip_libraries: Annotated[ + list[str], + Option( + "--skip-library", + "-L", + show_default=False, + default_factory=list, + help="List of libraries to skip. " + "Specify this option multiple times for skipping multiple libraries. ", + ), + ], + start: Annotated[ + int, Option("--start", "-s", help="The starting index for processing media.") + ] = 0, + end: Annotated[ + int, Option("--end", "-e", help="The ending index for processing media.") + ] = 1_000, + clean: Annotated[ + bool, + Option("--clean", "-c", show_default=False, help="Delete the whole cache before starting."), + ] = False, + debug: Annotated[ + bool, + Option( + "--debug", + help="Enable debug mode to show extra logging information for troubleshooting.", + ), + ] = False, +) -> None: + settings, mediux, services = setup(skip_services=skip_services, clean=clean, debug=debug) + + for service_idx, service in enumerate(services): + CONSOLE.rule( + f"[{service_idx + 1}/{len(services)}] {type(service).__name__} Service", + align="left", + style="title", + ) + for media_type in MediaType: + if media_type in skip_media_types: + continue + with CONSOLE.status(rf"\[{type(service).__name__}] Fetching {media_type.value} media"): + try: + entries = service.list(media_type=media_type, skip_libraries=skip_libraries)[ + start:end + ] + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + continue + for idx, entry in enumerate(entries): + CONSOLE.rule( + rf"[{idx + 1}/{len(entries)}] {entry.display_name} \[tmdb-{entry.tmdb_id}]", + align="left", + style="subtitle", + ) + with CONSOLE.status(r"\[Mediux] Searching for new Sets"): + try: + set_list = mediux.list_sets( + media_type=media_type, + tmdb_id=entry.tmdb_id, + exclude_usernames=settings.exclude_usernames, + ) + except ServiceError as err: + LOGGER.error("[Mediux] %s", err) + set_list = [] + filtered_sets = filter_sets( + set_list=set_list, + priority_usernames=settings.priority_usernames, + only_priority_usernames=settings.only_priority_usernames, + ) + for set_data in filtered_sets: + if not process_set_data( + entry=entry, + set_data=set_data, + mediux=mediux, + service=service, + priority_usernames=settings.priority_usernames, + kometa_integration=settings.kometa_integration, + ): + break + + +@app.command(name="media", help="Manually set posters for specific Mediux media using URLs.") +def media_posters( + urls: Annotated[ + list[str], Argument(show_default=False, help="List of URLs from Mediux to process.") + ], + skip_services: Annotated[ + list[ServiceOption], + Option( + "--skip-service", + "-S", + show_default=False, + case_sensitive=False, + default_factory=list, + help="List of Services to skip. " + "Specify this option multiple times for skipping multiple services.", + ), + ], + clean: Annotated[ + bool, + Option("--clean", "-c", show_default=False, help="Delete the whole cache before starting."), + ] = False, + debug: Annotated[ + bool, + Option( + "--debug", + help="Enable debug mode to show extra logging information for troubleshooting.", + ), + ] = False, +) -> None: + settings, mediux, services = setup(skip_services=skip_services, clean=clean, debug=debug) + + for service_idx, service in enumerate(services): + CONSOLE.rule( + f"[{service_idx + 1}/{len(services)}] {type(service).__name__} Service", + align="left", + style="title", + ) + for idx, url in enumerate(urls): + media_type = next( + (x for x in MediaType if url.startswith(f"{Mediux.WEB_URL}/{x}s/")), None + ) + if not media_type: + LOGGER.warning("Unknown Mediux url: '%s'", url) + continue + try: + tmdb_id = int(url.split("/")[-1]) + except ValueError: + LOGGER.warning("Unable to parse %s as an int", url.split("/")[-1]) + continue + with CONSOLE.status(rf"\[{type(service).__name__}] Searching for TMDB id: {tmdb_id}"): + try: + entry = service.get(media_type=media_type, tmdb_id=tmdb_id) + if not entry: + LOGGER.warning( + "[%s] Unable to find media with tmdb id: %d", + type(service).__name__, + tmdb_id, + ) + continue + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + continue + CONSOLE.rule( + rf"[{idx + 1}/{len(urls)}] {entry.display_name} \[tmdb-{tmdb_id}]", + align="left", + style="subtitle", + ) + with CONSOLE.status(r"\[Mediux] Searching for new Sets"): + try: + set_list = mediux.list_sets( + media_type=media_type, + tmdb_id=tmdb_id, + exclude_usernames=settings.exclude_usernames, + ) + except ServiceError as err: + LOGGER.error("[Mediux] %s", err) + set_list = [] + filtered_sets = filter_sets( + set_list=set_list, + priority_usernames=settings.priority_usernames, + only_priority_usernames=settings.only_priority_usernames, + ) + for set_data in filtered_sets: + if not process_set_data( + entry=entry, + set_data=set_data, + mediux=mediux, + service=service, + priority_usernames=settings.priority_usernames, + kometa_integration=settings.kometa_integration, + ): + break + + +@app.command(name="set", help="Manually set posters for specific Mediux sets using URLs.") +def set_posters( + urls: Annotated[ + list[str], Argument(show_default=False, help="List of URLs from Mediux to process.") + ], + skip_services: Annotated[ + list[ServiceOption], + Option( + "--skip-service", + "-S", + show_default=False, + case_sensitive=False, + default_factory=list, + help="List of Services to skip. " + "Specify this option multiple times for skipping multiple services.", + ), + ], + clean: Annotated[ + bool, + Option("--clean", "-c", show_default=False, help="Delete the whole cache before starting."), + ] = False, + debug: Annotated[ + bool, + Option( + "--debug", + help="Enable debug mode to show extra logging information for troubleshooting.", + ), + ] = False, +) -> None: + settings, mediux, services = setup(skip_services=skip_services, clean=clean, debug=debug) + + for service_idx, service in enumerate(services): + CONSOLE.rule( + f"[{service_idx + 1}/{len(services)}] {type(service).__name__} Service", + align="left", + style="title", + ) + for idx, url in enumerate(urls): + if not url.startswith(f"{Mediux.WEB_URL}/sets/"): + continue + try: + set_id = int(url.split("/")[-1]) + except ValueError: + LOGGER.warning("Unable to parse %s as an int", url.split("/")[-1]) + continue + with CONSOLE.status(rf"\[Mediux] Searching for Set id: {set_id}"): + try: + set_data = ( + mediux.get_show_set(set_id=set_id) + or mediux.get_collection_set(set_id=set_id) + or mediux.get_movie_set(set_id=set_id) + ) + if not set_data: + LOGGER.warning("[Mediux] Unable to find set with id: %d", set_id) + continue + except ServiceError as err: + LOGGER.error("[Mediux] %s", err) + continue + media_type, tmdb_id = ( + (MediaType.SHOW, set_data.show.id) + if isinstance(set_data, ShowSet) + else (MediaType.COLLECTION, set_data.collection.id) + if isinstance(set_data, CollectionSet) + else (MediaType.MOVIE, set_data.movie.id) + if isinstance(set_data, MovieSet) + else (None, None) + ) + if not media_type or not tmdb_id: + LOGGER.warning("[Mediux] Unable to determine media type for set id: %d", set_id) + continue + CONSOLE.rule( + rf"[{idx + 1}/{len(urls)}] {set_data.set_title} \[tmdb-{tmdb_id}]", + align="left", + style="subtitle", + ) + with CONSOLE.status(rf"\[{type(service).__name__}] Searching for TMDB id: {tmdb_id}"): + try: + entry = service.get(media_type=media_type, tmdb_id=tmdb_id) + if not entry: + LOGGER.warning( + "[%s] Unable to find media with tmdb id: %d", + type(service).__name__, + tmdb_id, + ) + continue + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + continue + process_set_data( + entry=entry, + set_data=set_data, + mediux=mediux, + service=service, + force=True, + priority_usernames=settings.priority_usernames, + kometa_integration=settings.kometa_integration, + ) + + +if __name__ == "__main__": + app(prog_name=__project__) diff --git a/visage/cli/__init__.py b/visage/cli/__init__.py new file mode 100644 index 0000000..7d3ccbe --- /dev/null +++ b/visage/cli/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["settings_app"] + +from visage.cli.settings import app as settings_app diff --git a/visage/cli/settings.py b/visage/cli/settings.py new file mode 100644 index 0000000..568460e --- /dev/null +++ b/visage/cli/settings.py @@ -0,0 +1,19 @@ +__all__ = ["app"] + +from typer import Typer + +from visage.console import CONSOLE +from visage.settings import Settings + +app = Typer(help="Commands for application settings.") + + +@app.command(name="view", help="Display the current and default settings.") +def view_settings() -> None: + settings = Settings.load() + settings.display() + + +@app.command(name="locate", help="Display the path to the settings file.") +def locate_settings() -> None: + CONSOLE.print(Settings._file) # noqa: SLF001 diff --git a/mediux_posters/console.py b/visage/console.py similarity index 100% rename from mediux_posters/console.py rename to visage/console.py diff --git a/mediux_posters/errors.py b/visage/errors.py similarity index 100% rename from mediux_posters/errors.py rename to visage/errors.py diff --git a/visage/mediux/__init__.py b/visage/mediux/__init__.py new file mode 100644 index 0000000..636a9c0 --- /dev/null +++ b/visage/mediux/__init__.py @@ -0,0 +1,4 @@ +__all__ = ["CollectionSet", "File", "FileType", "Mediux", "MovieSet", "ShowSet"] + +from visage.mediux.schemas import CollectionSet, File, FileType, MovieSet, ShowSet +from visage.mediux.service import Mediux diff --git a/mediux_posters/mediux/schemas.py b/visage/mediux/schemas.py similarity index 94% rename from mediux_posters/mediux/schemas.py rename to visage/mediux/schemas.py index eb38b71..5632d35 100644 --- a/mediux_posters/mediux/schemas.py +++ b/visage/mediux/schemas.py @@ -15,7 +15,7 @@ from pydantic import AliasPath, Field -from mediux_posters.utils import BaseModel +from visage.utils import BaseModel class MediuxModel(BaseModel, extra="ignore"): ... @@ -58,10 +58,10 @@ class Season(MediuxModel): class Show(MediuxModel): + id: int release_date: date | None = Field(alias="first_air_date") seasons: list[Season] title: str - tmdb_id: int = Field(alias="id") class ShowSet(MediuxModel): @@ -74,9 +74,9 @@ class ShowSet(MediuxModel): class Movie(MediuxModel): + id: int release_date: date | None title: str - tmdb_id: int = Field(alias="id") class MovieSet(MediuxModel): @@ -89,9 +89,9 @@ class MovieSet(MediuxModel): class Collection(MediuxModel): + id: int movies: list[Movie] title: str = Field(alias="collection_name") - tmdb_id: int = Field(alias="id") class CollectionSet(MediuxModel): diff --git a/mediux_posters/mediux/service.py b/visage/mediux/service.py similarity index 94% rename from mediux_posters/mediux/service.py rename to visage/mediux/service.py index c04c7ee..d852ccc 100644 --- a/mediux_posters/mediux/service.py +++ b/visage/mediux/service.py @@ -12,11 +12,11 @@ from ratelimit import limits, sleep_and_retry from rich.progress import Progress -from mediux_posters import __version__ -from mediux_posters.console import CONSOLE -from mediux_posters.errors import AuthenticationError, ServiceError -from mediux_posters.mediux.schemas import CollectionSet, MovieSet, ShowSet -from mediux_posters.utils import MediaType +from visage import __project__, __version__ +from visage.console import CONSOLE +from visage.errors import AuthenticationError, ServiceError +from visage.mediux.schemas import CollectionSet, MovieSet, ShowSet +from visage.utils import MediaType LOGGER = logging.getLogger(__name__) # 60 Calls per Minute @@ -102,7 +102,7 @@ def __init__(self, base_url: str, token: str): headers={ "Accept": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": f"Mediux-Posters/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", }, ) @@ -117,10 +117,10 @@ def _perform_graphql_request(self, query: str) -> dict[str, Any]: raise ServiceError(f"Unable to connect to '{err.request.url.path}'") from err except HTTPStatusError as err: try: - error_msg = next((x["message"] for x in err.response.json()["errors"]), None) + errors = err.response.json()["errors"] if err.response.status_code in (401, 403): - raise AuthenticationError(f"{err.response.status_code}: {error_msg}") - raise ServiceError(f"{err.response.status_code}: {error_msg}") + raise AuthenticationError(f"{err.response.status_code}: {errors}") + raise ServiceError(f"{err.response.status_code}: {errors}") except JSONDecodeError as err: raise ServiceError("Unable to parse response as Json") from err except JSONDecodeError as err: @@ -128,6 +128,14 @@ def _perform_graphql_request(self, query: str) -> dict[str, Any]: except TimeoutException as err: raise ServiceError("Service took too long to respond") from err + def validate(self) -> bool: + try: + # TODO: Do single set call to validate credentials + return True + except ServiceError as err: + LOGGER.error("[Mediux] %s", err) + return False + def list_show_sets( self, tmdb_id: int, exclude_usernames: list[str] | None = None ) -> list[ShowSet]: @@ -296,7 +304,6 @@ def get_movie_set(self, set_id: int) -> MovieSet | None: def list_sets( self, media_type: MediaType, tmdb_id: int, exclude_usernames: list[str] | None = None ) -> list[ShowSet] | list[CollectionSet] | list[MovieSet]: - exclude_usernames = exclude_usernames or [] return ( self.list_show_sets(tmdb_id=tmdb_id, exclude_usernames=exclude_usernames) if media_type is MediaType.SHOW @@ -328,7 +335,7 @@ def download_image(self, file_id: str, output: Path) -> None: with self.client.stream("GET", f"/assets/{file_id}") as response: total = int(response.headers["Content-Length"]) - with Progress(console=CONSOLE) as progress: + with Progress(console=CONSOLE, expand=True) as progress: download_task = progress.add_task( f"Downloading {output.parent.name}/{output.name}", total=total ) diff --git a/mediux_posters/services/__init__.py b/visage/services/__init__.py similarity index 63% rename from mediux_posters/services/__init__.py rename to visage/services/__init__.py index ab2e7e3..b97e78f 100644 --- a/mediux_posters/services/__init__.py +++ b/visage/services/__init__.py @@ -1,6 +1,6 @@ __all__ = ["BaseService", "Collection", "Episode", "Jellyfin", "Movie", "Plex", "Season", "Show"] -from mediux_posters.services._base import ( +from visage.services._base import ( BaseCollection as Collection, BaseEpisode as Episode, BaseMovie as Movie, @@ -8,5 +8,5 @@ BaseService, BaseShow as Show, ) -from mediux_posters.services.jellyfin import Jellyfin -from mediux_posters.services.plex import Plex +from visage.services.jellyfin import Jellyfin +from visage.services.plex import Plex diff --git a/mediux_posters/services/_base/__init__.py b/visage/services/_base/__init__.py similarity index 61% rename from mediux_posters/services/_base/__init__.py rename to visage/services/_base/__init__.py index 78b385a..ad28bb7 100644 --- a/mediux_posters/services/_base/__init__.py +++ b/visage/services/_base/__init__.py @@ -1,10 +1,10 @@ __all__ = ["BaseCollection", "BaseEpisode", "BaseMovie", "BaseSeason", "BaseService", "BaseShow"] -from mediux_posters.services._base.schemas import ( +from visage.services._base.schemas import ( BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow, ) -from mediux_posters.services._base.service import BaseService +from visage.services._base.service import BaseService diff --git a/mediux_posters/services/_base/schemas.py b/visage/services/_base/schemas.py similarity index 98% rename from mediux_posters/services/_base/schemas.py rename to visage/services/_base/schemas.py index f7e53ef..367c5d0 100644 --- a/mediux_posters/services/_base/schemas.py +++ b/visage/services/_base/schemas.py @@ -4,7 +4,7 @@ from pydantic import Field -from mediux_posters.utils import BaseModel +from visage.utils import BaseModel class BaseEpisode(BaseModel): diff --git a/mediux_posters/services/_base/service.py b/visage/services/_base/service.py similarity index 91% rename from mediux_posters/services/_base/service.py rename to visage/services/_base/service.py index 21fcbfa..bbe02e3 100644 --- a/mediux_posters/services/_base/service.py +++ b/visage/services/_base/service.py @@ -4,14 +4,15 @@ from pathlib import Path from typing import Generic, TypeVar -from mediux_posters.services._base.schemas import ( +from visage.services._base.schemas import ( BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow, ) -from mediux_posters.utils import MediaType +from visage.services.service_cache import ServiceCache +from visage.utils import MediaType T = TypeVar("T", bound=BaseShow) S = TypeVar("S", bound=BaseSeason) @@ -21,6 +22,9 @@ class BaseService(ABC, Generic[T, S, E, C, M]): + def __init__(self, cache: ServiceCache) -> None: + self.cache = cache + @abstractmethod def list_episodes(self, show_id: int | str, season_id: int | str) -> list[E]: ... diff --git a/visage/services/jellyfin/__init__.py b/visage/services/jellyfin/__init__.py new file mode 100644 index 0000000..0c99a27 --- /dev/null +++ b/visage/services/jellyfin/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["Jellyfin"] + +from visage.services.jellyfin.service import Jellyfin diff --git a/mediux_posters/services/jellyfin/schemas.py b/visage/services/jellyfin/schemas.py similarity index 95% rename from mediux_posters/services/jellyfin/schemas.py rename to visage/services/jellyfin/schemas.py index 9059037..f974138 100644 --- a/mediux_posters/services/jellyfin/schemas.py +++ b/visage/services/jellyfin/schemas.py @@ -6,14 +6,8 @@ from pydantic import AliasPath, Field, field_validator from pydantic.alias_generators import to_pascal -from mediux_posters.services._base import ( - BaseCollection, - BaseEpisode, - BaseMovie, - BaseSeason, - BaseShow, -) -from mediux_posters.utils import BaseModel +from visage.services._base import BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow +from visage.utils import BaseModel class JellyfinModel(BaseModel, alias_generator=to_pascal, extra="ignore"): ... diff --git a/mediux_posters/services/jellyfin/service.py b/visage/services/jellyfin/service.py similarity index 92% rename from mediux_posters/services/jellyfin/service.py rename to visage/services/jellyfin/service.py index 63939b5..c7e3484 100644 --- a/mediux_posters/services/jellyfin/service.py +++ b/visage/services/jellyfin/service.py @@ -11,30 +11,25 @@ from httpx import Client, HTTPStatusError, RequestError, TimeoutException from pydantic import TypeAdapter, ValidationError -from mediux_posters import __version__ -from mediux_posters.console import CONSOLE -from mediux_posters.errors import AuthenticationError, ServiceError -from mediux_posters.services._base import BaseService -from mediux_posters.services.jellyfin.schemas import ( - Collection, - Episode, - Library, - Movie, - Season, - Show, -) +from visage import __project__, __version__ +from visage.console import CONSOLE +from visage.errors import AuthenticationError, ServiceError +from visage.services._base import BaseService +from visage.services.jellyfin.schemas import Collection, Episode, Library, Movie, Season, Show +from visage.services.service_cache import ServiceCache LOGGER = logging.getLogger(__name__) class Jellyfin(BaseService[Show, Season, Episode, Collection, Movie]): def __init__(self, base_url: str, token: str): + super().__init__(cache=ServiceCache(service="jellyfin")) self.client = Client( base_url=base_url, headers={ "Accept": "application/json", "X-Emby-Token": token, - "User-Agent": f"Mediux-Posters/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", }, ) @@ -108,6 +103,14 @@ def _list_libraries( except ValidationError as err: raise ServiceError(err) from err + def validate(self) -> bool: + try: + self._list_libraries(media_type="movies") + return True + except ServiceError as err: + LOGGER.warning("[Jellyfin] %s", err) + return False + def list_episodes(self, show_id: str, season_id: str) -> list[Episode]: results = self._perform_get_request( endpoint=f"/Shows/{show_id}/Episodes", diff --git a/visage/services/plex/__init__.py b/visage/services/plex/__init__.py new file mode 100644 index 0000000..a1fe9d7 --- /dev/null +++ b/visage/services/plex/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["Plex"] + +from visage.services.plex.service import Plex diff --git a/mediux_posters/services/plex/schemas.py b/visage/services/plex/schemas.py similarity index 95% rename from mediux_posters/services/plex/schemas.py rename to visage/services/plex/schemas.py index 260ae58..cdca6f0 100644 --- a/mediux_posters/services/plex/schemas.py +++ b/visage/services/plex/schemas.py @@ -6,14 +6,8 @@ from pydantic import Field, model_validator from pydantic.alias_generators import to_camel -from mediux_posters.services._base import ( - BaseCollection, - BaseEpisode, - BaseMovie, - BaseSeason, - BaseShow, -) -from mediux_posters.utils import BaseModel +from visage.services._base import BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow +from visage.utils import BaseModel class PlexModel(BaseModel, alias_generator=to_camel, extra="ignore"): ... diff --git a/mediux_posters/services/plex/service.py b/visage/services/plex/service.py similarity index 95% rename from mediux_posters/services/plex/service.py rename to visage/services/plex/service.py index 2c3b7e7..859ddcd 100644 --- a/mediux_posters/services/plex/service.py +++ b/visage/services/plex/service.py @@ -9,11 +9,11 @@ from httpx import Client, HTTPStatusError, RequestError, TimeoutException from pydantic import TypeAdapter, ValidationError -from mediux_posters import __version__ -from mediux_posters.console import CONSOLE -from mediux_posters.errors import AuthenticationError, ServiceError -from mediux_posters.services._base import BaseService -from mediux_posters.services.plex.schemas import ( +from visage import __project__, __version__ +from visage.console import CONSOLE +from visage.errors import AuthenticationError, ServiceError +from visage.services._base import BaseService +from visage.services.plex.schemas import ( Collection, Episode, Library, @@ -22,18 +22,20 @@ Season, Show, ) +from visage.services.service_cache import ServiceCache LOGGER = logging.getLogger(__name__) class Plex(BaseService[Show, Season, Episode, Collection, Movie]): def __init__(self, base_url: str, token: str): + super().__init__(cache=ServiceCache(service="plex")) self.client = Client( base_url=base_url, headers={ "Accept": "application/json", "X-Plex-Token": token, - "User-Agent": f"Mediux-Posters/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", }, ) @@ -147,6 +149,14 @@ def _list_libraries( except ValidationError as err: raise ServiceError(err) from err + def validate(self) -> bool: + try: + self._list_libraries(media_type=MediaType.MOVIE) + return True + except ServiceError as err: + LOGGER.warning("[Plex] %s", err) + return False + def list_episodes(self, show_id: int, season_id: int) -> list[Episode]: # noqa: ARG002 results = ( self._perform_get_request( diff --git a/visage/services/service_cache.py b/visage/services/service_cache.py new file mode 100644 index 0000000..92d564b --- /dev/null +++ b/visage/services/service_cache.py @@ -0,0 +1,76 @@ +__all__ = ["ServiceCache"] + +import sqlite3 +from dataclasses import dataclass +from datetime import datetime + +from visage import get_cache_root +from visage.mediux import FileType + + +@dataclass(kw_only=True) +class CacheData: + creator: str + set_id: int + last_updated: datetime + + +class ServiceCache: + def __init__(self, service: str) -> None: + self._db_path = get_cache_root() / f"{service}.sqlite" + self.initialize() + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self._db_path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + def initialize(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS cache ( + id TEXT NOT NULL, + type TEXT NOT NULL, + creator TEXT NOT NULL, + set_id INTEGER NOT NULL, + last_updated TIMESTAMP NOT NULL, + PRIMARY KEY (id, type) + ); + """ + ) + + def select(self, object_id: int | str, file_type: FileType) -> CacheData | None: + with self._connect() as conn: + row = conn.execute( + "SELECT creator, set_id, last_updated FROM cache WHERE id = ? AND type = ?;", + (str(object_id), str(file_type)), + ).fetchone() + return ( + CacheData( + creator=row["creator"], + set_id=row["set_id"], + last_updated=datetime.fromisoformat(row["last_updated"]), + ) + if row + else None + ) + + def insert( + self, + object_id: int | str, + file_type: FileType, + creator: str, + set_id: int, + last_updated: datetime, + ) -> None: + with self._connect() as conn: + conn.execute( + """ + INSERT OR REPLACE INTO cache ( + id, type, creator, set_id, last_updated + ) VALUES (?, ?, ?, ?, ?); + """, + (str(object_id), str(file_type), creator, set_id, last_updated.isoformat()), + ) diff --git a/mediux_posters/settings.py b/visage/settings.py similarity index 95% rename from mediux_posters/settings.py rename to visage/settings.py index b4dffaf..58b98df 100644 --- a/mediux_posters/settings.py +++ b/visage/settings.py @@ -7,9 +7,9 @@ from pydantic import BeforeValidator, Field from rich.panel import Panel -from mediux_posters import get_config_root -from mediux_posters.console import CONSOLE -from mediux_posters.utils import BaseModel, blank_is_none, flatten_dict +from visage import get_config_root +from visage.console import CONSOLE +from visage.utils import BaseModel, blank_is_none, flatten_dict try: from typing import Self # Python >= 3.11 diff --git a/mediux_posters/utils.py b/visage/utils.py similarity index 96% rename from mediux_posters/utils.py rename to visage/utils.py index 570e41a..a6e3556 100644 --- a/mediux_posters/utils.py +++ b/visage/utils.py @@ -18,8 +18,8 @@ from pydantic import BaseModel as PydanticModel from rich.panel import Panel -from mediux_posters import get_cache_root -from mediux_posters.console import CONSOLE +from visage import get_cache_root +from visage.console import CONSOLE LOGGER = logging.getLogger(__name__) From 22f705da4617ca3af2b835c691105048695317f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Jun 2025 10:56:59 +0000 Subject: [PATCH 2/7] Generate new screengrabs with rich-codex --- docs/img/visage_commands.svg | 104 +++++++++---------- docs/img/visage_media.svg | 138 +++++++++++-------------- docs/img/visage_set.svg | 138 +++++++++++-------------- docs/img/visage_settings_locate.svg | 98 ++++++++++++++++++ docs/img/visage_settings_view.svg | 98 ++++++++++++++++++ docs/img/visage_sync.svg | 154 ++++++++++++++-------------- 6 files changed, 437 insertions(+), 293 deletions(-) create mode 100644 docs/img/visage_settings_locate.svg create mode 100644 docs/img/visage_settings_view.svg diff --git a/docs/img/visage_commands.svg b/docs/img/visage_commands.svg index d395ca0..476a6d9 100644 --- a/docs/img/visage_commands.svg +++ b/docs/img/visage_commands.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - + - + - - -Usage: Mediux-Posters [OPTIONS] COMMAND [ARGS]... - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---version                     Show the version and exit.                      ---install-completion          Install completion for the current shell.       ---show-completion             Show completion for the current shell, to copy  -                               it or customize the installation.               ---help                        Show this message and exit.                     -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -settings  Display the current and default settings.                          -sync      Synchronize posters by fetching data from Mediux and updating      - your services.                                                     -media     Manually set posters for specific Mediux media using a file or     - URLs.                                                              -set       Manually set posters for specific Mediux sets using a file or      - URLs.                                                              -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: visage [OPTIONS] COMMAND [ARGS]... + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--version                     Show the version.                               +--install-completion          Install completion for the current shell.       +--show-completion             Show completion for the current shell, to copy  +                               it or customize the installation.               +--help                        Show this message and exit.                     +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────╮ +sync      Synchronize posters by fetching data from Mediux and updating      + your services.                                                     +media     Manually set posters for specific Mediux media using URLs.         +set       Manually set posters for specific Mediux sets using URLs.          +settings  Commands for application settings.                                 +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/visage_media.svg b/docs/img/visage_media.svg index 525c1a0..4450cd8 100644 --- a/docs/img/visage_media.svg +++ b/docs/img/visage_media.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - + - + - - -Usage: Mediux-Posters media [OPTIONS] - - Manually set posters for specific Mediux media using a file or URLs.            - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[Plex|Jellyfin]  List of Services to skip. Specify    -                                          this option multiple times for       -                                          skipping multiple services.          ---url-uTEXT             List of URLs from Mediux to          -                                          process. Specify this option         -                                          multiple times for multiple URLs.    ---fileFILE             Path to a file containing URLs from  -                                          Mediux, one per line. If set, the    -                                          file must exist and cannot be a      -                                          directory.                           ---full-clean-C  Delete the whole cache before        -                                          starting.                            ---simple-clean-c  Delete the cache of each media       -                                          instead of the whole cache.          ---debug  Enable debug mode to show extra      -                                          logging information for              -                                          troubleshooting.                     ---help  Show this message and exit.          -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: visage media [OPTIONS] URLS... + + Manually set posters for specific Mediux media using URLs.                      + + +╭─ Arguments ──────────────────────────────────────────────────────────────────╮ +*    urls      URLS...  List of URLs from Mediux to process. [required] +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip. Specify    +                                          this option multiple times for       +                                          skipping multiple services.          +--clean-c  Delete the whole cache before        +                                          starting.                            +--debug  Enable debug mode to show extra      +                                          logging information for              +                                          troubleshooting.                     +--help  Show this message and exit.          +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/visage_set.svg b/docs/img/visage_set.svg index 1d4e711..09b17c2 100644 --- a/docs/img/visage_set.svg +++ b/docs/img/visage_set.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - + - + - - -Usage: Mediux-Posters set [OPTIONS] - - Manually set posters for specific Mediux sets using a file or URLs.             - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[Plex|Jellyfin]  List of Services to skip. Specify    -                                          this option multiple times for       -                                          skipping multiple services.          ---url-uTEXT             List of URLs from Mediux to          -                                          process. Specify this option         -                                          multiple times for multiple URLs.    ---fileFILE             Path to a file containing URLs from  -                                          Mediux, one per line. If set, the    -                                          file must exist and cannot be a      -                                          directory.                           ---full-clean-C  Delete the whole cache before        -                                          starting.                            ---simple-clean-c  Delete the cache of each media       -                                          instead of the whole cache.          ---debug  Enable debug mode to show extra      -                                          logging information for              -                                          troubleshooting.                     ---help  Show this message and exit.          -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: visage set [OPTIONS] URLS... + + Manually set posters for specific Mediux sets using URLs.                       + + +╭─ Arguments ──────────────────────────────────────────────────────────────────╮ +*    urls      URLS...  List of URLs from Mediux to process. [required] +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip. Specify    +                                          this option multiple times for       +                                          skipping multiple services.          +--clean-c  Delete the whole cache before        +                                          starting.                            +--debug  Enable debug mode to show extra      +                                          logging information for              +                                          troubleshooting.                     +--help  Show this message and exit.          +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/visage_settings_locate.svg b/docs/img/visage_settings_locate.svg new file mode 100644 index 0000000..bdd675d --- /dev/null +++ b/docs/img/visage_settings_locate.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Usage: visage settings locate [OPTIONS] + + Display the path to the settings file.                                          + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--help          Show this message and exit.                                   +╰──────────────────────────────────────────────────────────────────────────────╯ + + + + + diff --git a/docs/img/visage_settings_view.svg b/docs/img/visage_settings_view.svg new file mode 100644 index 0000000..abae9da --- /dev/null +++ b/docs/img/visage_settings_view.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Usage: visage settings view [OPTIONS] + + Display the current and default settings.                                       + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--help          Show this message and exit.                                   +╰──────────────────────────────────────────────────────────────────────────────╯ + + + + + diff --git a/docs/img/visage_sync.svg b/docs/img/visage_sync.svg index d36fe6a..458db25 100644 --- a/docs/img/visage_sync.svg +++ b/docs/img/visage_sync.svg @@ -19,126 +19,126 @@ font-weight: 700; } - .terminal-798121580-matrix { + .terminal-1398292516-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-798121580-title { + .terminal-1398292516-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-798121580-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-798121580-r2 { fill: #c5c8c6 } -.terminal-798121580-r3 { fill: #d0b344;font-weight: bold } -.terminal-798121580-r4 { fill: #868887 } -.terminal-798121580-r5 { fill: #68a0b3;font-weight: bold } -.terminal-798121580-r6 { fill: #98a84b;font-weight: bold } -.terminal-798121580-r7 { fill: #8d7b39;font-weight: bold } + .terminal-1398292516-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-1398292516-r2 { fill: #c5c8c6 } +.terminal-1398292516-r3 { fill: #d0b344;font-weight: bold } +.terminal-1398292516-r4 { fill: #868887 } +.terminal-1398292516-r5 { fill: #68a0b3;font-weight: bold } +.terminal-1398292516-r6 { fill: #98a84b;font-weight: bold } +.terminal-1398292516-r7 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -150,42 +150,42 @@ - + - - -Usage: Mediux-Posters sync [OPTIONS] - - Synchronize posters by fetching data from Mediux and updating your services.    - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[Plex|Jellyfin]  List of Services to skip.    -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple services.           ---skip-type-T[show|collection|movie]  List of MediaTypes to skip.  -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple types.              ---skip-library-LTEXT                     List of libraries to skip.   -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple libraries.          ---start-sINTEGER                  The starting index for       -                                                  processing media.            -[default: 0]                ---end-eINTEGER                  The ending index for         -                                                  processing media.            -[default: 100000]           ---full-clean-C  Delete the whole cache       -                                                  before starting.             ---debug  Enable debug mode to show    -                                                  extra logging information    -                                                  for troubleshooting.         ---help  Show this message and exit.  -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: visage sync [OPTIONS] + + Synchronize posters by fetching data from Mediux and updating your services.    + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip.    +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple services.           +--skip-type-T[show|collection|movie]  List of MediaTypes to skip.  +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple types.              +--skip-library-LTEXT                     List of libraries to skip.   +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple libraries.          +--start-sINTEGER                  The starting index for       +                                                  processing media.            +[default: 0]                +--end-eINTEGER                  The ending index for         +                                                  processing media.            +[default: 1000]             +--clean-c  Delete the whole cache       +                                                  before starting.             +--debug  Enable debug mode to show    +                                                  extra logging information    +                                                  for troubleshooting.         +--help  Show this message and exit.  +╰──────────────────────────────────────────────────────────────────────────────╯ + From b67de6c310ef2caebc41d4bbff2302fb80072d83 Mon Sep 17 00:00:00 2001 From: BuriedInCode <6057651+Buried-In-Code@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:44:46 +1200 Subject: [PATCH 3/7] Rollback renaming --- .editorconfig | 30 ------ .github/workflows/publishing.yaml | 2 +- .github/workflows/rich-codex.yaml | 2 +- README.md | 56 ++++++----- ...visage_commands.svg => mediux-posters.svg} | 0 ...age_media.svg => mediux-posters_media.svg} | 0 ...{visage_set.svg => mediux-posters_set.svg} | 0 ...ttings.svg => mediux-posters_settings.svg} | 0 ...svg => mediux-posters_settings_locate.svg} | 0 ...w.svg => mediux-posters_settings_view.svg} | 0 ...isage_sync.svg => mediux-posters_sync.svg} | 0 {visage => mediux_posters}/__init__.py | 4 +- {visage => mediux_posters}/__main__.py | 27 ++++-- mediux_posters/cli/__init__.py | 3 + {visage => mediux_posters}/cli/settings.py | 4 +- {visage => mediux_posters}/console.py | 0 {visage => mediux_posters}/errors.py | 0 mediux_posters/mediux/__init__.py | 4 + {visage => mediux_posters}/mediux/schemas.py | 2 +- {visage => mediux_posters}/mediux/service.py | 10 +- .../services/__init__.py | 6 +- .../services/_base/__init__.py | 4 +- .../services/_base/schemas.py | 2 +- .../services/_base/service.py | 6 +- mediux_posters/services/jellyfin/__init__.py | 3 + .../services/jellyfin/schemas.py | 10 +- .../services/jellyfin/service.py | 19 ++-- mediux_posters/services/plex/__init__.py | 3 + .../services/plex/schemas.py | 10 +- .../services/plex/service.py | 12 +-- .../services/service_cache.py | 4 +- {visage => mediux_posters}/settings.py | 6 +- {visage => mediux_posters}/utils.py | 4 +- pyproject.toml | 16 ++-- tests/conftest.py | 1 - tests/jellyfin_test.py | 1 - tests/plex_test.py | 1 - uv.lock | 96 +++++++++---------- visage/cli/__init__.py | 3 - visage/mediux/__init__.py | 4 - visage/services/jellyfin/__init__.py | 3 - visage/services/plex/__init__.py | 3 - 42 files changed, 182 insertions(+), 179 deletions(-) delete mode 100644 .editorconfig rename docs/img/{visage_commands.svg => mediux-posters.svg} (100%) rename docs/img/{visage_media.svg => mediux-posters_media.svg} (100%) rename docs/img/{visage_set.svg => mediux-posters_set.svg} (100%) rename docs/img/{visage_settings.svg => mediux-posters_settings.svg} (100%) rename docs/img/{visage_settings_locate.svg => mediux-posters_settings_locate.svg} (100%) rename docs/img/{visage_settings_view.svg => mediux-posters_settings_view.svg} (100%) rename docs/img/{visage_sync.svg => mediux-posters_sync.svg} (100%) rename {visage => mediux_posters}/__init__.py (96%) rename {visage => mediux_posters}/__main__.py (96%) create mode 100644 mediux_posters/cli/__init__.py rename {visage => mediux_posters}/cli/settings.py (82%) rename {visage => mediux_posters}/console.py (100%) rename {visage => mediux_posters}/errors.py (100%) create mode 100644 mediux_posters/mediux/__init__.py rename {visage => mediux_posters}/mediux/schemas.py (98%) rename {visage => mediux_posters}/mediux/service.py (97%) rename {visage => mediux_posters}/services/__init__.py (63%) rename {visage => mediux_posters}/services/_base/__init__.py (61%) rename {visage => mediux_posters}/services/_base/schemas.py (98%) rename {visage => mediux_posters}/services/_base/service.py (93%) create mode 100644 mediux_posters/services/jellyfin/__init__.py rename {visage => mediux_posters}/services/jellyfin/schemas.py (95%) rename {visage => mediux_posters}/services/jellyfin/service.py (95%) create mode 100644 mediux_posters/services/plex/__init__.py rename {visage => mediux_posters}/services/plex/schemas.py (95%) rename {visage => mediux_posters}/services/plex/service.py (97%) rename {visage => mediux_posters}/services/service_cache.py (96%) rename {visage => mediux_posters}/settings.py (95%) rename {visage => mediux_posters}/utils.py (96%) delete mode 100644 visage/cli/__init__.py delete mode 100644 visage/mediux/__init__.py delete mode 100644 visage/services/jellyfin/__init__.py delete mode 100644 visage/services/plex/__init__.py diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 390b247..0000000 --- a/.editorconfig +++ /dev/null @@ -1,30 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -max_line_length = 100 - -[{*.html,*.jinja}] -indent_size = 4 -insert_final_newline = false -max_line_length = off - -[*.json] -insert_final_newline = false - -[*.md] -trim_trailing_whitespace = false -max_line_length = off - -[*.py] -indent_size = 4 - -[{*.xml,*.xsd}] -indent_size = 4 -insert_final_newline = false -max_line_length = off diff --git a/.github/workflows/publishing.yaml b/.github/workflows/publishing.yaml index aac818b..b5e9139 100644 --- a/.github/workflows/publishing.yaml +++ b/.github/workflows/publishing.yaml @@ -30,7 +30,7 @@ jobs: environment: name: pypi - url: https://pypi.org/p/Visage + url: https://pypi.org/p/Mediux-Posters name: Publish to PyPI permissions: id-token: write diff --git a/.github/workflows/rich-codex.yaml b/.github/workflows/rich-codex.yaml index 71da6a9..6664101 100644 --- a/.github/workflows/rich-codex.yaml +++ b/.github/workflows/rich-codex.yaml @@ -4,7 +4,7 @@ on: push: paths: - README.md - - visage/__main__.py + - mediux_posters/__main__.py workflow_dispatch: permissions: diff --git a/README.md b/README.md index 2ddf334..1520b54 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Visage +# Mediux Posters -[![PyPI - Python](https://img.shields.io/pypi/pyversions/Visage.svg?logo=PyPI&label=Python&style=flat-square)](https://pypi.python.org/pypi/Visage/) -[![PyPI - Status](https://img.shields.io/pypi/status/Visage.svg?logo=PyPI&label=Status&style=flat-square)](https://pypi.python.org/pypi/Visage/) -[![PyPI - Version](https://img.shields.io/pypi/v/Visage.svg?logo=PyPI&label=Version&style=flat-square)](https://pypi.python.org/pypi/Visage/) -[![PyPI - License](https://img.shields.io/pypi/l/Visage.svg?logo=PyPI&label=License&style=flat-square)](https://opensource.org/licenses/MIT) +[![PyPI - Python](https://img.shields.io/pypi/pyversions/Mediux-Posters.svg?logo=PyPI&label=Python&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) +[![PyPI - Status](https://img.shields.io/pypi/status/Mediux-Posters.svg?logo=PyPI&label=Status&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) +[![PyPI - Version](https://img.shields.io/pypi/v/Mediux-Posters.svg?logo=PyPI&label=Version&style=flat-square)](https://pypi.python.org/pypi/Mediux-Posters/) +[![PyPI - License](https://img.shields.io/pypi/l/Mediux-Posters.svg?logo=PyPI&label=License&style=flat-square)](https://opensource.org/licenses/MIT) [![Pre-Commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&style=flat-square)](https://github.com/pre-commit/pre-commit) [![Ruff](https://img.shields.io/badge/ruff-enabled-brightgreen?logo=ruff&style=flat-square)](https://github.com/astral-sh/ruff) -[![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Visage.svg?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Visage/graphs/contributors) -[![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Visage/testing.yaml?branch=main&logo=Github&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Visage/actions/workflows/testing.yaml) +[![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Mediux-Posters.svg?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Mediux-Posters/graphs/contributors) +[![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Mediux-Posters/testing.yaml?branch=main&logo=Github&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Mediux-Posters/actions/workflows/testing.yaml) Fetches Show, Movie, and Collection cover art from Mediux and updates Plex/Jellyfin using TMDB IDs. @@ -20,45 +20,53 @@ _Jellyfin Collections are not yet supported._ ### Pipx 1. Ensure you have [Pipx](https://pipx.pypa.io/stable/) installed: `pipx --version` -2. Install the project: `pipx install visage` +2. Install the project: `pipx install Mediux-Posters` ## Usage -
visage Commands +
mediux-posters Commands - ![`uv run visage --help`](docs/img/visage_commands.svg) + ![`uv run mediux-posters --help`](docs/img/mediux-posters.svg)
-
visage sync +
mediux-posters sync - ![`uv run visage sync --help`](docs/img/visage_sync.svg) + ![`uv run mediux-posters sync --help`](docs/img/mediux-posters_sync.svg)
-
visage media +
mediux-posters media - ![`uv run visage media --help`](docs/img/visage_media.svg) + ![`uv run mediux-posters media --help`](docs/img/mediux-posters_media.svg)
-
visage set +
mediux-posters set - ![`uv run visage set --help`](docs/img/visage_set.svg) + ![`uv run mediux-posters set --help`](docs/img/mediux-posters_set.svg)
-### Visage settings Commands -
visage settings view + +### mediux-posters settings Commands + +
mediux-posters settings Commands + + + ![`uv run mediux-posters settings --help`](docs/img/mediux-posters_settings.svg) + +
+
mediux-posters settings view - ![`uv run visage settings view --help`](docs/img/visage_settings_view.svg) + ![`uv run mediux-posters settings view --help`](docs/img/mediux-posters_settings_view.svg)
-
visage settings locate +
mediux-posters settings locate - ![`uv run visage settings locate --help`](docs/img/visage_settings_locate.svg) + ![`uv run mediux-posters settings locate --help`](docs/img/mediux-posters_settings_locate.svg)
@@ -71,11 +79,11 @@ _Jellyfin Collections are not yet supported._ - **Collection:** Poster and Backdrop - **Movie:** Poster and Backdrop - To enable support for Collections in Plex, each Collection needs to have a label with its tmdb-id, in the format of `tmdb-`\ - If using Kometa, refer to [this comment](https://github.com/Buried-In-Code/Visage/issues/12#issuecomment-2622002859) to have Kometa add these labels. + If using Kometa, refer to [this comment](https://github.com/Buried-In-Code/Mediux-Posters/issues/12#issuecomment-2622002859) to have Kometa add these labels. ## Settings -To set Plex and/or Jellyfin details, update the file: `~/.config/visage/settings.toml`. +To set Plex and/or Jellyfin details, update the file: `~/.config/mediux-posters/settings.toml`. File will be created on first run. ### Example File @@ -107,7 +115,7 @@ token = "" - `kometa_integration` - If enabled, will remove the `Overlay` label from any media Visage uploads a poster/backdrop/title-card to. + If enabled, will remove the `Overlay` label from any media Mediux-Posters uploads a poster/backdrop/title-card to. - `only_priority_usernames` diff --git a/docs/img/visage_commands.svg b/docs/img/mediux-posters.svg similarity index 100% rename from docs/img/visage_commands.svg rename to docs/img/mediux-posters.svg diff --git a/docs/img/visage_media.svg b/docs/img/mediux-posters_media.svg similarity index 100% rename from docs/img/visage_media.svg rename to docs/img/mediux-posters_media.svg diff --git a/docs/img/visage_set.svg b/docs/img/mediux-posters_set.svg similarity index 100% rename from docs/img/visage_set.svg rename to docs/img/mediux-posters_set.svg diff --git a/docs/img/visage_settings.svg b/docs/img/mediux-posters_settings.svg similarity index 100% rename from docs/img/visage_settings.svg rename to docs/img/mediux-posters_settings.svg diff --git a/docs/img/visage_settings_locate.svg b/docs/img/mediux-posters_settings_locate.svg similarity index 100% rename from docs/img/visage_settings_locate.svg rename to docs/img/mediux-posters_settings_locate.svg diff --git a/docs/img/visage_settings_view.svg b/docs/img/mediux-posters_settings_view.svg similarity index 100% rename from docs/img/visage_settings_view.svg rename to docs/img/mediux-posters_settings_view.svg diff --git a/docs/img/visage_sync.svg b/docs/img/mediux-posters_sync.svg similarity index 100% rename from docs/img/visage_sync.svg rename to docs/img/mediux-posters_sync.svg diff --git a/visage/__init__.py b/mediux_posters/__init__.py similarity index 96% rename from visage/__init__.py rename to mediux_posters/__init__.py index bfb2f33..42a6b49 100644 --- a/visage/__init__.py +++ b/mediux_posters/__init__.py @@ -7,7 +7,7 @@ "get_state_root", "setup_logging", ] -__project__ = "visage" +__project__ = "mediux-posters" __version__ = "0.5.0" import logging @@ -17,7 +17,7 @@ from rich.logging import RichHandler from rich.traceback import install -from visage.console import CONSOLE +from mediux_posters.console import CONSOLE def get_cache_root() -> Path: diff --git a/visage/__main__.py b/mediux_posters/__main__.py similarity index 96% rename from visage/__main__.py rename to mediux_posters/__main__.py index 8218feb..74a06ae 100644 --- a/visage/__main__.py +++ b/mediux_posters/__main__.py @@ -6,14 +6,23 @@ from typer import Abort, Argument, Context, Exit, Option, Typer -from visage import __project__, __version__, get_cache_root, setup_logging -from visage.cli import settings_app -from visage.console import CONSOLE -from visage.errors import ServiceError -from visage.mediux import CollectionSet, File, FileType, Mediux, MovieSet, ShowSet -from visage.services import BaseService, Collection, Episode, Jellyfin, Movie, Plex, Season, Show -from visage.settings import Settings -from visage.utils import MediaType, delete_folder, get_cached_image, slugify +from mediux_posters import __project__, __version__, get_cache_root, setup_logging +from mediux_posters.cli import settings_app +from mediux_posters.console import CONSOLE +from mediux_posters.errors import ServiceError +from mediux_posters.mediux import CollectionSet, File, FileType, Mediux, MovieSet, ShowSet +from mediux_posters.services import ( + BaseService, + Collection, + Episode, + Jellyfin, + Movie, + Plex, + Season, + Show, +) +from mediux_posters.settings import Settings +from mediux_posters.utils import MediaType, delete_folder, get_cached_image, slugify LOGGER = logging.getLogger(__project__) app = Typer(no_args_is_help=True) @@ -30,7 +39,7 @@ def setup( skip_services: list[ServiceOption], clean: bool, debug: bool = False ) -> tuple[Settings, Mediux, list[BaseService]]: setup_logging(debug=debug) - LOGGER.info("%s v%s", __project__, __version__) + LOGGER.info("%s v%s", __project__.title(), __version__) LOGGER.info("Python v%s", python_version()) if clean: diff --git a/mediux_posters/cli/__init__.py b/mediux_posters/cli/__init__.py new file mode 100644 index 0000000..8228fbe --- /dev/null +++ b/mediux_posters/cli/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["settings_app"] + +from mediux_posters.cli.settings import app as settings_app diff --git a/visage/cli/settings.py b/mediux_posters/cli/settings.py similarity index 82% rename from visage/cli/settings.py rename to mediux_posters/cli/settings.py index 568460e..407438b 100644 --- a/visage/cli/settings.py +++ b/mediux_posters/cli/settings.py @@ -2,8 +2,8 @@ from typer import Typer -from visage.console import CONSOLE -from visage.settings import Settings +from mediux_posters.console import CONSOLE +from mediux_posters.settings import Settings app = Typer(help="Commands for application settings.") diff --git a/visage/console.py b/mediux_posters/console.py similarity index 100% rename from visage/console.py rename to mediux_posters/console.py diff --git a/visage/errors.py b/mediux_posters/errors.py similarity index 100% rename from visage/errors.py rename to mediux_posters/errors.py diff --git a/mediux_posters/mediux/__init__.py b/mediux_posters/mediux/__init__.py new file mode 100644 index 0000000..149df1b --- /dev/null +++ b/mediux_posters/mediux/__init__.py @@ -0,0 +1,4 @@ +__all__ = ["CollectionSet", "File", "FileType", "Mediux", "MovieSet", "ShowSet"] + +from mediux_posters.mediux.schemas import CollectionSet, File, FileType, MovieSet, ShowSet +from mediux_posters.mediux.service import Mediux diff --git a/visage/mediux/schemas.py b/mediux_posters/mediux/schemas.py similarity index 98% rename from visage/mediux/schemas.py rename to mediux_posters/mediux/schemas.py index 5632d35..c11fef6 100644 --- a/visage/mediux/schemas.py +++ b/mediux_posters/mediux/schemas.py @@ -15,7 +15,7 @@ from pydantic import AliasPath, Field -from visage.utils import BaseModel +from mediux_posters.utils import BaseModel class MediuxModel(BaseModel, extra="ignore"): ... diff --git a/visage/mediux/service.py b/mediux_posters/mediux/service.py similarity index 97% rename from visage/mediux/service.py rename to mediux_posters/mediux/service.py index d852ccc..a567764 100644 --- a/visage/mediux/service.py +++ b/mediux_posters/mediux/service.py @@ -12,11 +12,11 @@ from ratelimit import limits, sleep_and_retry from rich.progress import Progress -from visage import __project__, __version__ -from visage.console import CONSOLE -from visage.errors import AuthenticationError, ServiceError -from visage.mediux.schemas import CollectionSet, MovieSet, ShowSet -from visage.utils import MediaType +from mediux_posters import __project__, __version__ +from mediux_posters.console import CONSOLE +from mediux_posters.errors import AuthenticationError, ServiceError +from mediux_posters.mediux.schemas import CollectionSet, MovieSet, ShowSet +from mediux_posters.utils import MediaType LOGGER = logging.getLogger(__name__) # 60 Calls per Minute diff --git a/visage/services/__init__.py b/mediux_posters/services/__init__.py similarity index 63% rename from visage/services/__init__.py rename to mediux_posters/services/__init__.py index b97e78f..ab2e7e3 100644 --- a/visage/services/__init__.py +++ b/mediux_posters/services/__init__.py @@ -1,6 +1,6 @@ __all__ = ["BaseService", "Collection", "Episode", "Jellyfin", "Movie", "Plex", "Season", "Show"] -from visage.services._base import ( +from mediux_posters.services._base import ( BaseCollection as Collection, BaseEpisode as Episode, BaseMovie as Movie, @@ -8,5 +8,5 @@ BaseService, BaseShow as Show, ) -from visage.services.jellyfin import Jellyfin -from visage.services.plex import Plex +from mediux_posters.services.jellyfin import Jellyfin +from mediux_posters.services.plex import Plex diff --git a/visage/services/_base/__init__.py b/mediux_posters/services/_base/__init__.py similarity index 61% rename from visage/services/_base/__init__.py rename to mediux_posters/services/_base/__init__.py index ad28bb7..78b385a 100644 --- a/visage/services/_base/__init__.py +++ b/mediux_posters/services/_base/__init__.py @@ -1,10 +1,10 @@ __all__ = ["BaseCollection", "BaseEpisode", "BaseMovie", "BaseSeason", "BaseService", "BaseShow"] -from visage.services._base.schemas import ( +from mediux_posters.services._base.schemas import ( BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow, ) -from visage.services._base.service import BaseService +from mediux_posters.services._base.service import BaseService diff --git a/visage/services/_base/schemas.py b/mediux_posters/services/_base/schemas.py similarity index 98% rename from visage/services/_base/schemas.py rename to mediux_posters/services/_base/schemas.py index 367c5d0..f7e53ef 100644 --- a/visage/services/_base/schemas.py +++ b/mediux_posters/services/_base/schemas.py @@ -4,7 +4,7 @@ from pydantic import Field -from visage.utils import BaseModel +from mediux_posters.utils import BaseModel class BaseEpisode(BaseModel): diff --git a/visage/services/_base/service.py b/mediux_posters/services/_base/service.py similarity index 93% rename from visage/services/_base/service.py rename to mediux_posters/services/_base/service.py index bbe02e3..3a51bac 100644 --- a/visage/services/_base/service.py +++ b/mediux_posters/services/_base/service.py @@ -4,15 +4,15 @@ from pathlib import Path from typing import Generic, TypeVar -from visage.services._base.schemas import ( +from mediux_posters.services._base.schemas import ( BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow, ) -from visage.services.service_cache import ServiceCache -from visage.utils import MediaType +from mediux_posters.services.service_cache import ServiceCache +from mediux_posters.utils import MediaType T = TypeVar("T", bound=BaseShow) S = TypeVar("S", bound=BaseSeason) diff --git a/mediux_posters/services/jellyfin/__init__.py b/mediux_posters/services/jellyfin/__init__.py new file mode 100644 index 0000000..601ab5b --- /dev/null +++ b/mediux_posters/services/jellyfin/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["Jellyfin"] + +from mediux_posters.services.jellyfin.service import Jellyfin diff --git a/visage/services/jellyfin/schemas.py b/mediux_posters/services/jellyfin/schemas.py similarity index 95% rename from visage/services/jellyfin/schemas.py rename to mediux_posters/services/jellyfin/schemas.py index f974138..9059037 100644 --- a/visage/services/jellyfin/schemas.py +++ b/mediux_posters/services/jellyfin/schemas.py @@ -6,8 +6,14 @@ from pydantic import AliasPath, Field, field_validator from pydantic.alias_generators import to_pascal -from visage.services._base import BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow -from visage.utils import BaseModel +from mediux_posters.services._base import ( + BaseCollection, + BaseEpisode, + BaseMovie, + BaseSeason, + BaseShow, +) +from mediux_posters.utils import BaseModel class JellyfinModel(BaseModel, alias_generator=to_pascal, extra="ignore"): ... diff --git a/visage/services/jellyfin/service.py b/mediux_posters/services/jellyfin/service.py similarity index 95% rename from visage/services/jellyfin/service.py rename to mediux_posters/services/jellyfin/service.py index c7e3484..50b4336 100644 --- a/visage/services/jellyfin/service.py +++ b/mediux_posters/services/jellyfin/service.py @@ -11,12 +11,19 @@ from httpx import Client, HTTPStatusError, RequestError, TimeoutException from pydantic import TypeAdapter, ValidationError -from visage import __project__, __version__ -from visage.console import CONSOLE -from visage.errors import AuthenticationError, ServiceError -from visage.services._base import BaseService -from visage.services.jellyfin.schemas import Collection, Episode, Library, Movie, Season, Show -from visage.services.service_cache import ServiceCache +from mediux_posters import __project__, __version__ +from mediux_posters.console import CONSOLE +from mediux_posters.errors import AuthenticationError, ServiceError +from mediux_posters.services._base import BaseService +from mediux_posters.services.jellyfin.schemas import ( + Collection, + Episode, + Library, + Movie, + Season, + Show, +) +from mediux_posters.services.service_cache import ServiceCache LOGGER = logging.getLogger(__name__) diff --git a/mediux_posters/services/plex/__init__.py b/mediux_posters/services/plex/__init__.py new file mode 100644 index 0000000..6e652e6 --- /dev/null +++ b/mediux_posters/services/plex/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["Plex"] + +from mediux_posters.services.plex.service import Plex diff --git a/visage/services/plex/schemas.py b/mediux_posters/services/plex/schemas.py similarity index 95% rename from visage/services/plex/schemas.py rename to mediux_posters/services/plex/schemas.py index cdca6f0..260ae58 100644 --- a/visage/services/plex/schemas.py +++ b/mediux_posters/services/plex/schemas.py @@ -6,8 +6,14 @@ from pydantic import Field, model_validator from pydantic.alias_generators import to_camel -from visage.services._base import BaseCollection, BaseEpisode, BaseMovie, BaseSeason, BaseShow -from visage.utils import BaseModel +from mediux_posters.services._base import ( + BaseCollection, + BaseEpisode, + BaseMovie, + BaseSeason, + BaseShow, +) +from mediux_posters.utils import BaseModel class PlexModel(BaseModel, alias_generator=to_camel, extra="ignore"): ... diff --git a/visage/services/plex/service.py b/mediux_posters/services/plex/service.py similarity index 97% rename from visage/services/plex/service.py rename to mediux_posters/services/plex/service.py index 859ddcd..df8690c 100644 --- a/visage/services/plex/service.py +++ b/mediux_posters/services/plex/service.py @@ -9,11 +9,11 @@ from httpx import Client, HTTPStatusError, RequestError, TimeoutException from pydantic import TypeAdapter, ValidationError -from visage import __project__, __version__ -from visage.console import CONSOLE -from visage.errors import AuthenticationError, ServiceError -from visage.services._base import BaseService -from visage.services.plex.schemas import ( +from mediux_posters import __project__, __version__ +from mediux_posters.console import CONSOLE +from mediux_posters.errors import AuthenticationError, ServiceError +from mediux_posters.services._base import BaseService +from mediux_posters.services.plex.schemas import ( Collection, Episode, Library, @@ -22,7 +22,7 @@ Season, Show, ) -from visage.services.service_cache import ServiceCache +from mediux_posters.services.service_cache import ServiceCache LOGGER = logging.getLogger(__name__) diff --git a/visage/services/service_cache.py b/mediux_posters/services/service_cache.py similarity index 96% rename from visage/services/service_cache.py rename to mediux_posters/services/service_cache.py index 92d564b..80a2af1 100644 --- a/visage/services/service_cache.py +++ b/mediux_posters/services/service_cache.py @@ -4,8 +4,8 @@ from dataclasses import dataclass from datetime import datetime -from visage import get_cache_root -from visage.mediux import FileType +from mediux_posters import get_cache_root +from mediux_posters.mediux import FileType @dataclass(kw_only=True) diff --git a/visage/settings.py b/mediux_posters/settings.py similarity index 95% rename from visage/settings.py rename to mediux_posters/settings.py index 58b98df..b4dffaf 100644 --- a/visage/settings.py +++ b/mediux_posters/settings.py @@ -7,9 +7,9 @@ from pydantic import BeforeValidator, Field from rich.panel import Panel -from visage import get_config_root -from visage.console import CONSOLE -from visage.utils import BaseModel, blank_is_none, flatten_dict +from mediux_posters import get_config_root +from mediux_posters.console import CONSOLE +from mediux_posters.utils import BaseModel, blank_is_none, flatten_dict try: from typing import Self # Python >= 3.11 diff --git a/visage/utils.py b/mediux_posters/utils.py similarity index 96% rename from visage/utils.py rename to mediux_posters/utils.py index a6e3556..570e41a 100644 --- a/visage/utils.py +++ b/mediux_posters/utils.py @@ -18,8 +18,8 @@ from pydantic import BaseModel as PydanticModel from rich.panel import Panel -from visage import get_cache_root -from visage.console import CONSOLE +from mediux_posters import get_cache_root +from mediux_posters.console import CONSOLE LOGGER = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index e5761b7..181a4da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,30 +43,30 @@ description = "Fetches Show, Movie, and Collection cover art from Mediux and upd dynamic = ["version"] keywords = [] license = {text = "MIT"} -name = "visage" +name = "mediux-posters" readme = "README.md" requires-python = ">= 3.10" [project.scripts] -Mediux-Posters = "visage.__main__:app" -visage = "visage.__main__:app" +Mediux-Posters = "mediux_posters.__main__:app" +mediux-posters = "mediux_posters.__main__:app" [project.urls] -Homepage = "https://pypi.org/project/Visage" -Issues = "https://github.com/Buried-In-Code/Visage/issues" -Source = "https://github.com/Buried-In-Code/Visage" +Homepage = "https://pypi.org/project/Mediux-Posters" +Issues = "https://github.com/Buried-In-Code/Mediux-Posters/issues" +Source = "https://github.com/Buried-In-Code/Mediux-Posters" [tool.coverage.report] show_missing = true [tool.coverage.run] -source = ["visage"] +source = ["mediux_posters"] [tool.hatch.build.targets.sdist] exclude = [".github/"] [tool.hatch.version] -path = "visage/__init__.py" +path = "mediux_posters/__init__.py" [tool.pytest.ini_options] addopts = ["--cov"] diff --git a/tests/conftest.py b/tests/conftest.py index c907570..f71ddf8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ import os import pytest - from visage.mediux import Mediux from visage.services.jellyfin import Jellyfin from visage.services.plex import Plex diff --git a/tests/jellyfin_test.py b/tests/jellyfin_test.py index b9ba4bc..b33d248 100644 --- a/tests/jellyfin_test.py +++ b/tests/jellyfin_test.py @@ -5,7 +5,6 @@ import pytest from pytest_httpx import HTTPXMock - from visage.services.jellyfin import Jellyfin diff --git a/tests/plex_test.py b/tests/plex_test.py index f1362b6..69907aa 100644 --- a/tests/plex_test.py +++ b/tests/plex_test.py @@ -5,7 +5,6 @@ import pytest from pytest_httpx import HTTPXMock - from visage.services.plex import Plex diff --git a/uv.lock b/uv.lock index 39e7ee5..a01b11e 100644 --- a/uv.lock +++ b/uv.lock @@ -386,6 +386,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mediux-posters" +source = { editable = "." } +dependencies = [ + { name = "graphql-query" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "ratelimit" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, +] +tests = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-httpx" }, + { name = "tox" }, + { name = "tox-uv" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphql-query", specifier = ">=1.4.0" }, + { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "ratelimit", specifier = ">=2.2.1" }, + { name = "rich", specifier = ">=14.0.0" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.2.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "typer", specifier = ">=0.16.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pre-commit", specifier = ">=4.2.0" }] +tests = [ + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "tox", specifier = ">=4.26.0" }, + { name = "tox-uv", specifier = ">=1.26.0" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -847,51 +895,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] - -[[package]] -name = "visage" -source = { editable = "." } -dependencies = [ - { name = "graphql-query" }, - { name = "httpx", extra = ["http2"] }, - { name = "pydantic" }, - { name = "ratelimit" }, - { name = "rich" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomli-w" }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pre-commit" }, -] -tests = [ - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-httpx" }, - { name = "tox" }, - { name = "tox-uv" }, -] - -[package.metadata] -requires-dist = [ - { name = "graphql-query", specifier = ">=1.4.0" }, - { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.11.7" }, - { name = "ratelimit", specifier = ">=2.2.1" }, - { name = "rich", specifier = ">=14.0.0" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.2.1" }, - { name = "tomli-w", specifier = ">=1.2.0" }, - { name = "typer", specifier = ">=0.16.0" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "pre-commit", specifier = ">=4.2.0" }] -tests = [ - { name = "pytest", specifier = ">=8.4.0" }, - { name = "pytest-cov", specifier = ">=6.2.1" }, - { name = "pytest-httpx", specifier = ">=0.35.0" }, - { name = "tox", specifier = ">=4.26.0" }, - { name = "tox-uv", specifier = ">=1.26.0" }, -] diff --git a/visage/cli/__init__.py b/visage/cli/__init__.py deleted file mode 100644 index 7d3ccbe..0000000 --- a/visage/cli/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["settings_app"] - -from visage.cli.settings import app as settings_app diff --git a/visage/mediux/__init__.py b/visage/mediux/__init__.py deleted file mode 100644 index 636a9c0..0000000 --- a/visage/mediux/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ["CollectionSet", "File", "FileType", "Mediux", "MovieSet", "ShowSet"] - -from visage.mediux.schemas import CollectionSet, File, FileType, MovieSet, ShowSet -from visage.mediux.service import Mediux diff --git a/visage/services/jellyfin/__init__.py b/visage/services/jellyfin/__init__.py deleted file mode 100644 index 0c99a27..0000000 --- a/visage/services/jellyfin/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["Jellyfin"] - -from visage.services.jellyfin.service import Jellyfin diff --git a/visage/services/plex/__init__.py b/visage/services/plex/__init__.py deleted file mode 100644 index a1fe9d7..0000000 --- a/visage/services/plex/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["Plex"] - -from visage.services.plex.service import Plex From cb414eae1d082def051e2a2f4e57338ee10ae28c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Jun 2025 13:45:35 +0000 Subject: [PATCH 4/7] Generate new screengrabs with rich-codex --- docs/img/mediux-posters.svg | 90 ++++++------ docs/img/mediux-posters_media.svg | 110 +++++++------- docs/img/mediux-posters_set.svg | 110 +++++++------- docs/img/mediux-posters_settings.svg | 80 ++++++---- docs/img/mediux-posters_settings_locate.svg | 58 ++++---- docs/img/mediux-posters_settings_view.svg | 58 ++++---- docs/img/mediux-posters_sync.svg | 154 ++++++++++---------- 7 files changed, 338 insertions(+), 322 deletions(-) diff --git a/docs/img/mediux-posters.svg b/docs/img/mediux-posters.svg index 476a6d9..4206798 100644 --- a/docs/img/mediux-posters.svg +++ b/docs/img/mediux-posters.svg @@ -19,79 +19,79 @@ font-weight: 700; } - .terminal-2235280414-matrix { + .terminal-1927589480-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2235280414-title { + .terminal-1927589480-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2235280414-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-2235280414-r2 { fill: #c5c8c6 } -.terminal-2235280414-r3 { fill: #d0b344;font-weight: bold } -.terminal-2235280414-r4 { fill: #868887 } -.terminal-2235280414-r5 { fill: #68a0b3;font-weight: bold } + .terminal-1927589480-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-1927589480-r2 { fill: #c5c8c6 } +.terminal-1927589480-r3 { fill: #d0b344;font-weight: bold } +.terminal-1927589480-r4 { fill: #868887 } +.terminal-1927589480-r5 { fill: #68a0b3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -103,27 +103,27 @@
- + - - -Usage: visage [OPTIONS] COMMAND [ARGS]... - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---version                     Show the version.                               ---install-completion          Install completion for the current shell.       ---show-completion             Show completion for the current shell, to copy  -                               it or customize the installation.               ---help                        Show this message and exit.                     -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -sync      Synchronize posters by fetching data from Mediux and updating      - your services.                                                     -media     Manually set posters for specific Mediux media using URLs.         -set       Manually set posters for specific Mediux sets using URLs.          -settings  Commands for application settings.                                 -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters [OPTIONS] COMMAND [ARGS]... + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--version                     Show the version.                               +--install-completion          Install completion for the current shell.       +--show-completion             Show completion for the current shell, to copy  +                               it or customize the installation.               +--help                        Show this message and exit.                     +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────╮ +sync      Synchronize posters by fetching data from Mediux and updating      + your services.                                                     +media     Manually set posters for specific Mediux media using URLs.         +set       Manually set posters for specific Mediux sets using URLs.          +settings  Commands for application settings.                                 +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_media.svg b/docs/img/mediux-posters_media.svg index 4450cd8..c40ce67 100644 --- a/docs/img/mediux-posters_media.svg +++ b/docs/img/mediux-posters_media.svg @@ -19,92 +19,92 @@ font-weight: 700; } - .terminal-3043114156-matrix { + .terminal-2333949686-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3043114156-title { + .terminal-2333949686-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3043114156-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-3043114156-r2 { fill: #c5c8c6 } -.terminal-3043114156-r3 { fill: #d0b344;font-weight: bold } -.terminal-3043114156-r4 { fill: #868887 } -.terminal-3043114156-r5 { fill: #cc555a } -.terminal-3043114156-r6 { fill: #8a4346 } -.terminal-3043114156-r7 { fill: #68a0b3;font-weight: bold } -.terminal-3043114156-r8 { fill: #98a84b;font-weight: bold } -.terminal-3043114156-r9 { fill: #8d7b39;font-weight: bold } + .terminal-2333949686-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-2333949686-r2 { fill: #c5c8c6 } +.terminal-2333949686-r3 { fill: #d0b344;font-weight: bold } +.terminal-2333949686-r4 { fill: #868887 } +.terminal-2333949686-r5 { fill: #cc555a } +.terminal-2333949686-r6 { fill: #8a4346 } +.terminal-2333949686-r7 { fill: #68a0b3;font-weight: bold } +.terminal-2333949686-r8 { fill: #98a84b;font-weight: bold } +.terminal-2333949686-r9 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -116,30 +116,30 @@ - + - - -Usage: visage media [OPTIONS] URLS... - - Manually set posters for specific Mediux media using URLs.                      - - -╭─ Arguments ──────────────────────────────────────────────────────────────────╮ -*    urls      URLS...  List of URLs from Mediux to process. [required] -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[plex|jellyfin]  List of Services to skip. Specify    -                                          this option multiple times for       -                                          skipping multiple services.          ---clean-c  Delete the whole cache before        -                                          starting.                            ---debug  Enable debug mode to show extra      -                                          logging information for              -                                          troubleshooting.                     ---help  Show this message and exit.          -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters media [OPTIONS] URLS... + + Manually set posters for specific Mediux media using URLs.                      + + +╭─ Arguments ──────────────────────────────────────────────────────────────────╮ +*    urls      URLS...  List of URLs from Mediux to process. [required] +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip. Specify    +                                          this option multiple times for       +                                          skipping multiple services.          +--clean-c  Delete the whole cache before        +                                          starting.                            +--debug  Enable debug mode to show extra      +                                          logging information for              +                                          troubleshooting.                     +--help  Show this message and exit.          +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_set.svg b/docs/img/mediux-posters_set.svg index 09b17c2..f4c6df5 100644 --- a/docs/img/mediux-posters_set.svg +++ b/docs/img/mediux-posters_set.svg @@ -19,92 +19,92 @@ font-weight: 700; } - .terminal-4077468695-matrix { + .terminal-3429121633-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4077468695-title { + .terminal-3429121633-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4077468695-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-4077468695-r2 { fill: #c5c8c6 } -.terminal-4077468695-r3 { fill: #d0b344;font-weight: bold } -.terminal-4077468695-r4 { fill: #868887 } -.terminal-4077468695-r5 { fill: #cc555a } -.terminal-4077468695-r6 { fill: #8a4346 } -.terminal-4077468695-r7 { fill: #68a0b3;font-weight: bold } -.terminal-4077468695-r8 { fill: #98a84b;font-weight: bold } -.terminal-4077468695-r9 { fill: #8d7b39;font-weight: bold } + .terminal-3429121633-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-3429121633-r2 { fill: #c5c8c6 } +.terminal-3429121633-r3 { fill: #d0b344;font-weight: bold } +.terminal-3429121633-r4 { fill: #868887 } +.terminal-3429121633-r5 { fill: #cc555a } +.terminal-3429121633-r6 { fill: #8a4346 } +.terminal-3429121633-r7 { fill: #68a0b3;font-weight: bold } +.terminal-3429121633-r8 { fill: #98a84b;font-weight: bold } +.terminal-3429121633-r9 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -116,30 +116,30 @@ - + - - -Usage: visage set [OPTIONS] URLS... - - Manually set posters for specific Mediux sets using URLs.                       - - -╭─ Arguments ──────────────────────────────────────────────────────────────────╮ -*    urls      URLS...  List of URLs from Mediux to process. [required] -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[plex|jellyfin]  List of Services to skip. Specify    -                                          this option multiple times for       -                                          skipping multiple services.          ---clean-c  Delete the whole cache before        -                                          starting.                            ---debug  Enable debug mode to show extra      -                                          logging information for              -                                          troubleshooting.                     ---help  Show this message and exit.          -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters set [OPTIONS] URLS... + + Manually set posters for specific Mediux sets using URLs.                       + + +╭─ Arguments ──────────────────────────────────────────────────────────────────╮ +*    urls      URLS...  List of URLs from Mediux to process. [required] +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip. Specify    +                                          this option multiple times for       +                                          skipping multiple services.          +--clean-c  Delete the whole cache before        +                                          starting.                            +--debug  Enable debug mode to show extra      +                                          logging information for              +                                          troubleshooting.                     +--help  Show this message and exit.          +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_settings.svg b/docs/img/mediux-posters_settings.svg index 93fa5f8..b539abd 100644 --- a/docs/img/mediux-posters_settings.svg +++ b/docs/img/mediux-posters_settings.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + - + - + - - -Usage: Mediux-Posters settings [OPTIONS] - - Display the current and default settings.                                       - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---help          Show this message and exit.                                   -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters settings [OPTIONS] COMMAND [ARGS]... + + Commands for application settings.                                              + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--help          Show this message and exit.                                   +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────╮ +view    Display the current and default settings.                            +locate  Display the path to the settings file.                               +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_settings_locate.svg b/docs/img/mediux-posters_settings_locate.svg index bdd675d..8499783 100644 --- a/docs/img/mediux-posters_settings_locate.svg +++ b/docs/img/mediux-posters_settings_locate.svg @@ -19,55 +19,55 @@ font-weight: 700; } - .terminal-4006584346-matrix { + .terminal-3740377700-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4006584346-title { + .terminal-3740377700-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4006584346-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-4006584346-r2 { fill: #c5c8c6 } -.terminal-4006584346-r3 { fill: #d0b344;font-weight: bold } -.terminal-4006584346-r4 { fill: #868887 } -.terminal-4006584346-r5 { fill: #68a0b3;font-weight: bold } + .terminal-3740377700-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-3740377700-r2 { fill: #c5c8c6 } +.terminal-3740377700-r3 { fill: #d0b344;font-weight: bold } +.terminal-3740377700-r4 { fill: #868887 } +.terminal-3740377700-r5 { fill: #68a0b3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + @@ -79,19 +79,19 @@ - + - - -Usage: visage settings locate [OPTIONS] - - Display the path to the settings file.                                          - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---help          Show this message and exit.                                   -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters settings locate [OPTIONS] + + Display the path to the settings file.                                          + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--help          Show this message and exit.                                   +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_settings_view.svg b/docs/img/mediux-posters_settings_view.svg index abae9da..bf73f91 100644 --- a/docs/img/mediux-posters_settings_view.svg +++ b/docs/img/mediux-posters_settings_view.svg @@ -19,55 +19,55 @@ font-weight: 700; } - .terminal-3381502151-matrix { + .terminal-3180831505-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3381502151-title { + .terminal-3180831505-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3381502151-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-3381502151-r2 { fill: #c5c8c6 } -.terminal-3381502151-r3 { fill: #d0b344;font-weight: bold } -.terminal-3381502151-r4 { fill: #868887 } -.terminal-3381502151-r5 { fill: #68a0b3;font-weight: bold } + .terminal-3180831505-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-3180831505-r2 { fill: #c5c8c6 } +.terminal-3180831505-r3 { fill: #d0b344;font-weight: bold } +.terminal-3180831505-r4 { fill: #868887 } +.terminal-3180831505-r5 { fill: #68a0b3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + @@ -79,19 +79,19 @@ - + - - -Usage: visage settings view [OPTIONS] - - Display the current and default settings.                                       - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---help          Show this message and exit.                                   -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters settings view [OPTIONS] + + Display the current and default settings.                                       + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--help          Show this message and exit.                                   +╰──────────────────────────────────────────────────────────────────────────────╯ + diff --git a/docs/img/mediux-posters_sync.svg b/docs/img/mediux-posters_sync.svg index 458db25..fd0c3e5 100644 --- a/docs/img/mediux-posters_sync.svg +++ b/docs/img/mediux-posters_sync.svg @@ -19,126 +19,126 @@ font-weight: 700; } - .terminal-1398292516-matrix { + .terminal-3161735790-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1398292516-title { + .terminal-3161735790-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1398292516-r1 { fill: #c5c8c6;font-weight: bold } -.terminal-1398292516-r2 { fill: #c5c8c6 } -.terminal-1398292516-r3 { fill: #d0b344;font-weight: bold } -.terminal-1398292516-r4 { fill: #868887 } -.terminal-1398292516-r5 { fill: #68a0b3;font-weight: bold } -.terminal-1398292516-r6 { fill: #98a84b;font-weight: bold } -.terminal-1398292516-r7 { fill: #8d7b39;font-weight: bold } + .terminal-3161735790-r1 { fill: #c5c8c6;font-weight: bold } +.terminal-3161735790-r2 { fill: #c5c8c6 } +.terminal-3161735790-r3 { fill: #d0b344;font-weight: bold } +.terminal-3161735790-r4 { fill: #868887 } +.terminal-3161735790-r5 { fill: #68a0b3;font-weight: bold } +.terminal-3161735790-r6 { fill: #98a84b;font-weight: bold } +.terminal-3161735790-r7 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -150,42 +150,42 @@ - + - - -Usage: visage sync [OPTIONS] - - Synchronize posters by fetching data from Mediux and updating your services.    - - -╭─ Options ────────────────────────────────────────────────────────────────────╮ ---skip-service-S[plex|jellyfin]  List of Services to skip.    -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple services.           ---skip-type-T[show|collection|movie]  List of MediaTypes to skip.  -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple types.              ---skip-library-LTEXT                     List of libraries to skip.   -                                                  Specify this option          -                                                  multiple times for skipping  -                                                  multiple libraries.          ---start-sINTEGER                  The starting index for       -                                                  processing media.            -[default: 0]                ---end-eINTEGER                  The ending index for         -                                                  processing media.            -[default: 1000]             ---clean-c  Delete the whole cache       -                                                  before starting.             ---debug  Enable debug mode to show    -                                                  extra logging information    -                                                  for troubleshooting.         ---help  Show this message and exit.  -╰──────────────────────────────────────────────────────────────────────────────╯ - + + +Usage: mediux-posters sync [OPTIONS] + + Synchronize posters by fetching data from Mediux and updating your services.    + + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +--skip-service-S[plex|jellyfin]  List of Services to skip.    +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple services.           +--skip-type-T[show|collection|movie]  List of MediaTypes to skip.  +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple types.              +--skip-library-LTEXT                     List of libraries to skip.   +                                                  Specify this option          +                                                  multiple times for skipping  +                                                  multiple libraries.          +--start-sINTEGER                  The starting index for       +                                                  processing media.            +[default: 0]                +--end-eINTEGER                  The ending index for         +                                                  processing media.            +[default: 1000]             +--clean-c  Delete the whole cache       +                                                  before starting.             +--debug  Enable debug mode to show    +                                                  extra logging information    +                                                  for troubleshooting.         +--help  Show this message and exit.  +╰──────────────────────────────────────────────────────────────────────────────╯ + From ced645e132f0726719fb0ba9e3e2571f40a2ae47 Mon Sep 17 00:00:00 2001 From: BuriedInCode <6057651+Buried-In-Code@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:32:02 +1200 Subject: [PATCH 5/7] Revert test naming --- tests/conftest.py | 7 ++++--- tests/jellyfin_test.py | 3 ++- tests/mediux_test.py | 6 +++--- tests/plex_test.py | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f71ddf8..4e4f553 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ import os import pytest -from visage.mediux import Mediux -from visage.services.jellyfin import Jellyfin -from visage.services.plex import Plex + +from mediux_posters.mediux import Mediux +from mediux_posters.services.jellyfin import Jellyfin +from mediux_posters.services.plex import Plex @pytest.fixture(scope="session") diff --git a/tests/jellyfin_test.py b/tests/jellyfin_test.py index b33d248..45398e4 100644 --- a/tests/jellyfin_test.py +++ b/tests/jellyfin_test.py @@ -5,7 +5,8 @@ import pytest from pytest_httpx import HTTPXMock -from visage.services.jellyfin import Jellyfin + +from mediux_posters.services.jellyfin import Jellyfin @pytest.mark.httpx_mock( diff --git a/tests/mediux_test.py b/tests/mediux_test.py index 24a1285..d7ef81c 100644 --- a/tests/mediux_test.py +++ b/tests/mediux_test.py @@ -3,9 +3,9 @@ from pathlib import Path from tempfile import NamedTemporaryFile -from visage.mediux import Mediux -from visage.mediux.schemas import FileType -from visage.utils import MediaType +from mediux_posters.mediux import Mediux +from mediux_posters.mediux.schemas import FileType +from mediux_posters.utils import MediaType def test_list_show_sets(mediux_session: Mediux) -> None: diff --git a/tests/plex_test.py b/tests/plex_test.py index 69907aa..a264cec 100644 --- a/tests/plex_test.py +++ b/tests/plex_test.py @@ -5,7 +5,8 @@ import pytest from pytest_httpx import HTTPXMock -from visage.services.plex import Plex + +from mediux_posters.services.plex import Plex @pytest.mark.httpx_mock( From e9bef7456e069263aee1aa0f19810001f29d4f5f Mon Sep 17 00:00:00 2001 From: BuriedInCode <6057651+Buried-In-Code@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:54:32 +1200 Subject: [PATCH 6/7] Fix tests Update dependencies --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- tests/mediux_test.py | 8 ++++---- uv.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa639de..68a1694 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.12.0 hooks: - id: ruff-format - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 181a4da..b33ca10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ tests = [ "pytest >= 8.4.0", "pytest-cov >= 6.2.1", "pytest-httpx >= 0.35.0", - "tox >= 4.26.0", + "tox >= 4.27.0", "tox-uv >= 1.26.0" ] diff --git a/tests/mediux_test.py b/tests/mediux_test.py index d7ef81c..47b9fa7 100644 --- a/tests/mediux_test.py +++ b/tests/mediux_test.py @@ -24,6 +24,7 @@ def test_list_show_sets(mediux_session: Mediux) -> None: assert result.files[0].collection_id is None assert result.id == 28831 assert result.set_title == "Downton Abbey (2010) Set" + assert result.show.id == 33907 assert result.show.release_date == date(2010, 9, 26) assert len(result.show.seasons) != 0 assert len(result.show.seasons[0].episodes) != 0 @@ -34,7 +35,6 @@ def test_list_show_sets(mediux_session: Mediux) -> None: assert result.show.seasons[0].title == "Specials" assert result.show.seasons[0].number == 0 assert result.show.title == "Downton Abbey" - assert result.show.tmdb_id == 33907 assert result.username == "JackTaylor803" @@ -66,12 +66,12 @@ def test_list_collection_sets(mediux_session: Mediux) -> None: result = next(iter(x for x in results if x.id == 24404), None) assert result is not None + assert result.collection.id == 573436 assert len(result.collection.movies) != 0 + assert result.collection.movies[0].id == 324857 assert result.collection.movies[0].release_date == date(2018, 12, 6) assert result.collection.movies[0].title == "Spider-Man: Into the Spider-Verse" - assert result.collection.movies[0].tmdb_id == 324857 assert result.collection.title == "Spider-Man: Spider-Verse Collection" - assert result.collection.tmdb_id == 573436 assert len(result.files) != 0 assert result.files[0].id == "3ae60cf9-ad99-449c-971f-5d7c6eaba02f" assert result.files[0].file_type == FileType.POSTER @@ -122,9 +122,9 @@ def test_list_movie_sets(mediux_session: Mediux) -> None: assert result.files[0].movie_id == 535544 assert result.files[0].collection_id is None assert result.id == 11023 + assert result.movie.id == 535544 assert result.movie.release_date == date(2019, 9, 12) assert result.movie.title == "Downton Abbey" - assert result.movie.tmdb_id == 535544 assert result.set_title == "Downton Abbey (2019) Set" assert result.username == "fwlolx" diff --git a/uv.lock b/uv.lock index a01b11e..ec19986 100644 --- a/uv.lock +++ b/uv.lock @@ -430,7 +430,7 @@ tests = [ { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-httpx", specifier = ">=0.35.0" }, - { name = "tox", specifier = ">=4.26.0" }, + { name = "tox", specifier = ">=4.27.0" }, { name = "tox-uv", specifier = ">=1.26.0" }, ] @@ -787,7 +787,7 @@ wheels = [ [[package]] name = "tox" -version = "4.26.0" +version = "4.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -802,9 +802,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/dcec0c00321a107f7f697fd00754c5112572ea6dcacb40b16d8c3eea7c37/tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca", size = 197260, upload-time = "2025-05-13T15:04:28.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/b7/19c01717747076f63c54d871ada081cd711a7c9a7572f2225675c3858b94/tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57", size = 198351, upload-time = "2025-06-17T15:17:50.585Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761, upload-time = "2025-05-13T15:04:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3a/30889167f41ecaffb957ec4409e1cbc1d5d558a5bbbdfb734a5b9911930f/tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20", size = 173441, upload-time = "2025-06-17T15:17:48.689Z" }, ] [[package]] From e238b8348c5ab4e8f732a1ef1270c2ff013b289b Mon Sep 17 00:00:00 2001 From: BuriedInCode <6057651+Buried-In-Code@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:49:07 +1200 Subject: [PATCH 7/7] Better handle exceptions Add validate to tests --- mediux_posters/__main__.py | 39 ++++++++++++++++----- mediux_posters/mediux/service.py | 6 ++-- mediux_posters/services/jellyfin/service.py | 2 +- mediux_posters/services/plex/service.py | 2 +- tests/conftest.py | 12 +++++-- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/mediux_posters/__main__.py b/mediux_posters/__main__.py index 74a06ae..8c84a46 100644 --- a/mediux_posters/__main__.py +++ b/mediux_posters/__main__.py @@ -160,11 +160,19 @@ def process_image( set_data.username, ) should_log = False - mediux.download_image(file_id=file.id, output=image_file) + try: + mediux.download_image(file_id=file.id, output=image_file) + except ServiceError as err: + LOGGER.error("[Mediux] %s", err) + return - success = service.upload_image( - object_id=obj.id, image_file=image_file, kometa_integration=kometa_integration - ) + try: + success = service.upload_image( + object_id=obj.id, image_file=image_file, kometa_integration=kometa_integration + ) + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + success = False setattr(obj, uploaded_attr, success) if success: service.cache.insert( @@ -190,7 +198,12 @@ def process_image( filename="backdrop.jpg", ) if isinstance(entry, Show) and isinstance(set_data, ShowSet): - for season in entry.seasons or service.list_seasons(show_id=entry.id): + try: + seasons = service.list_seasons(show_id=entry.id) + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + seasons = [] + for season in seasons: entry.seasons.append(season) mediux_season = next( (x for x in set_data.show.seasons if x.number == season.number), None @@ -204,9 +217,12 @@ def process_image( parent=slugify(value=entry.display_name), filename=f"s{season.number:02}.jpg", ) - for episode in season.episodes or service.list_episodes( - show_id=entry.id, season_id=season.id - ): + try: + episodes = service.list_episodes(show_id=entry.id, season_id=season.id) + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + episodes = [] + for episode in episodes: season.episodes.append(episode) mediux_episode = next( (x for x in mediux_season.episodes if x.number == episode.number), None @@ -221,7 +237,12 @@ def process_image( filename=f"s{season.number:02}e{episode.number:02}.jpg", ) elif isinstance(entry, Collection) and isinstance(set_data, CollectionSet): - for movie in service.list_collection_movies(collection_id=entry.id): + try: + movies = service.list_collection_movies(collection_id=entry.id) + except ServiceError as err: + LOGGER.error("[%s] %s", type(service).__name__, err) + movies = [] + for movie in movies: entry.movies.append(movie) mediux_movie = next( (x for x in set_data.collection.movies if x.id == movie.tmdb_id), None diff --git a/mediux_posters/mediux/service.py b/mediux_posters/mediux/service.py index a567764..241b071 100644 --- a/mediux_posters/mediux/service.py +++ b/mediux_posters/mediux/service.py @@ -102,7 +102,7 @@ def __init__(self, base_url: str, token: str): headers={ "Accept": "application/json", "Authorization": f"Bearer {token}", - "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__.title()}/{__version__}/{system()}: {release()}", }, ) @@ -130,8 +130,8 @@ def _perform_graphql_request(self, query: str) -> dict[str, Any]: def validate(self) -> bool: try: - # TODO: Do single set call to validate credentials - return True + results = self.list_movie_sets(tmdb_id=324857) + return results != [] except ServiceError as err: LOGGER.error("[Mediux] %s", err) return False diff --git a/mediux_posters/services/jellyfin/service.py b/mediux_posters/services/jellyfin/service.py index 50b4336..1416ee3 100644 --- a/mediux_posters/services/jellyfin/service.py +++ b/mediux_posters/services/jellyfin/service.py @@ -36,7 +36,7 @@ def __init__(self, base_url: str, token: str): headers={ "Accept": "application/json", "X-Emby-Token": token, - "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__.title()}/{__version__}/{system()}: {release()}", }, ) diff --git a/mediux_posters/services/plex/service.py b/mediux_posters/services/plex/service.py index df8690c..051b66c 100644 --- a/mediux_posters/services/plex/service.py +++ b/mediux_posters/services/plex/service.py @@ -35,7 +35,7 @@ def __init__(self, base_url: str, token: str): headers={ "Accept": "application/json", "X-Plex-Token": token, - "User-Agent": f"{__project__}/{__version__}/{system()}: {release()}", + "User-Agent": f"{__project__.title()}/{__version__}/{system()}: {release()}", }, ) diff --git a/tests/conftest.py b/tests/conftest.py index 4e4f553..a6bddda 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,9 @@ def mediux_token() -> str: @pytest.fixture(scope="session") def mediux_session(mediux_base_url: str, mediux_token: str) -> Mediux: - return Mediux(base_url=mediux_base_url, token=mediux_token) + mediux = Mediux(base_url=mediux_base_url, token=mediux_token) + assert mediux.validate() is True + return mediux @pytest.fixture(scope="session") @@ -35,7 +37,9 @@ def jellyfin_token() -> str | None: @pytest.fixture(scope="session") def jellyfin_session(jellyfin_base_url: str | None, jellyfin_token: str | None) -> Jellyfin | None: if jellyfin_base_url and jellyfin_token: - return Jellyfin(base_url=jellyfin_base_url, token=jellyfin_token) + jellyfin = Jellyfin(base_url=jellyfin_base_url, token=jellyfin_token) + assert jellyfin.validate() is True + return jellyfin return None @@ -52,5 +56,7 @@ def plex_token() -> str | None: @pytest.fixture(scope="session") def plex_session(plex_base_url: str | None, plex_token: str | None) -> Plex | None: if plex_base_url and plex_token: - return Plex(base_url=plex_base_url, token=plex_token) + plex = Plex(base_url=plex_base_url, token=plex_token) + assert plex.validate() is True + return plex return None