Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/linting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PY_VERSION }}
- name: Install project
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/publishing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PY_VERSION }}
- name: Install project
run: uv sync --locked --no-dev
- name: Run build
run: uv build
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v7.0.1
with:
name: python-package-distributions
path: dist/
Expand All @@ -42,11 +42,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v8.0.1
with:
name: python-package-distributions
path: dist/
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PY_VERSION }}
- name: Validate dist
Expand All @@ -65,8 +65,8 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v8.0.1
with:
name: python-package-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@v1
- uses: pypa/gh-action-pypi-publish@v1.14.0
7 changes: 3 additions & 4 deletions .github/workflows/rich-codex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,13 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PY_VERSION }}
- name: Install project
run: uv sync --locked --dev --all-extras --managed-python
- name: Generate terminal images with rich-codex
uses: ewels/rich-codex@v1.2.11
- uses: ewels/rich-codex@v1.2.11
with:
commit_changes: true
clean_img_paths: docs/img/*.svg
6 changes: 3 additions & 3 deletions .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ jobs:
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install project
run: uv sync --locked --no-dev --all-extras --group tests --managed-python
- name: Run tests
run: uv run pytest
run: uv run --no-dev pytest
env:
MEDIUX__BASE_URL: ${{ secrets.MEDIUX__BASE_URL }}
MEDIUX__TOKEN: ${{ secrets.MEDIUX__TOKEN }}
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ kometa_integration = false
only_priority_usernames = false
priority_usernames = []

[covers]
path = "~/.cache/mediux-posters/covers"
store = true

[jellyfin]
base_url = "http://127.0.0.1:8096"
token = "<Token>"
Expand All @@ -94,6 +98,14 @@ token = "<Token>"

### Details

- `covers.path`

Folder location as to where to store downloaded covers.

- `covers.store`

Wether to store the images in the cache between runs, useful when running multiple services to not have to redownload images.

- `exclude_usernames`

A list of usernames whose sets should be ignored when running a sync.
Expand Down
162 changes: 99 additions & 63 deletions mediux_posters/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from platform import python_version
from typing import Final, Protocol, TypeVar

Expand All @@ -35,7 +36,7 @@
)
from mediux_posters.services.service_cache import CacheData, CacheKey, ServiceCache
from mediux_posters.settings import Settings
from mediux_posters.utils import delete_folder, get_cached_image, slugify
from mediux_posters.utils import delete_folder, slugify

LOGGER = logging.getLogger(__name__)
DEFAULT_CREATOR_RANK: Final[int] = 1_000_000
Expand Down Expand Up @@ -69,10 +70,12 @@ def __str__(self) -> str:
class ProcessContext:
mediux: Mediux
service: BaseService
covers_cache: Path
priority_usernames: list[str]
excluded_usernames: list[str]
force: bool = False
kometa_integration: bool = False
store_cover: bool = True


class Action(str, Enum):
Expand All @@ -91,11 +94,14 @@ def setup_services(
LOGGER.info("Mediux Posters v%s", __version__)
LOGGER.info("Python v%s", python_version())

settings = Settings.load().save()

if clean:
LOGGER.info("Cleaning covers directory: '%s'", settings.covers.path)
delete_folder(folder=settings.covers.path)
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
Expand Down Expand Up @@ -230,7 +236,79 @@ def determine_action( # noqa: PLR0911
return Action.UPLOAD


def process_image( # noqa: PLR0911
def download_image(
image_file: Path,
ctx: ProcessContext,
file: File,
parent: str,
cache_key: CacheKey,
existing: CacheData | None,
set_data: ShowSet | CollectionSet | MovieSet,
) -> bool:
image_file.unlink(missing_ok=True)
try:
ctx.mediux.download_image(file_id=file.id, output=image_file, parent_str=parent)
except ServiceError as err:
LOGGER.error("[Mediux] %s", err)
ctx.service.cache.delete(key=cache_key)
return False
if not existing:
ctx.service.cache.insert(
key=cache_key,
creator=set_data.username,
set_id=set_data.id,
last_updated=file.last_updated,
)
else:
ctx.service.cache.update(
key=cache_key,
creator=set_data.username,
set_id=set_data.id,
last_updated=file.last_updated,
)
return True


def upload_image(
image_file: Path,
ctx: ProcessContext,
obj: Show | Season | Episode | Collection | Movie,
cache_key: CacheKey,
uploaded_attr: str,
) -> None:
if image_file.stat().st_size >= MAX_IMAGE_SIZE:
LOGGER.warning(
"[%s] Image file '%s' is larger than %d MB, skipping upload",
type(ctx.service).__name__,
image_file,
MAX_IMAGE_SIZE / 1000 / 1000,
)
return
upload_success = ctx.service.upload_image(
object_id=obj.id,
image_file=image_file,
file_type=cache_key.type,
kometa_integration=ctx.kometa_integration,
)
if not ctx.store_cover:
image_file.unlink(missing_ok=True)
if not upload_success:
ctx.service.cache.update_service(
key=cache_key,
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
timestamp=None,
)
setattr(obj, uploaded_attr, False)
return
ctx.service.cache.update_service(
key=cache_key,
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
timestamp=datetime.now(tz=timezone.utc),
)
setattr(obj, uploaded_attr, True)


def process_image(
obj: Show | Season | Episode | Collection | Movie,
cache_key: CacheKey,
id_value: int | str,
Expand All @@ -247,7 +325,7 @@ def process_image( # noqa: PLR0911
if not file:
return should_log

image_file = get_cached_image(parent, filename)
image_file = ctx.covers_cache.joinpath(parent, filename)
existing = ctx.service.cache.select(key=cache_key)
service_timestamp = ctx.service.cache.get_timestamp(
key=cache_key,
Expand All @@ -265,6 +343,8 @@ def process_image( # noqa: PLR0911
)
if action is Action.SKIP:
return should_log
if not ctx.store_cover:
action = Action.DOWNLOAD
if should_log:
LOGGER.info(
"[Mediux] %sing '%s' by '%s'",
Expand All @@ -274,56 +354,21 @@ def process_image( # noqa: PLR0911
)
should_log = False
if action is Action.DOWNLOAD or not image_file.exists():
image_file.unlink(missing_ok=True)
try:
ctx.mediux.download_image(file_id=file.id, output=image_file, parent_str=parent)
except ServiceError as err:
LOGGER.error("[Mediux] %s", err)
ctx.service.cache.delete(key=cache_key)
download_success = download_image(
image_file=image_file,
ctx=ctx,
file=file,
parent=parent,
cache_key=cache_key,
existing=existing,
set_data=set_data,
)
if not download_success:
setattr(obj, uploaded_attr, False)
return should_log
if not existing:
ctx.service.cache.insert(
key=cache_key,
creator=set_data.username,
set_id=set_data.id,
last_updated=file.last_updated,
)
else:
ctx.service.cache.update(
key=cache_key,
creator=set_data.username,
set_id=set_data.id,
last_updated=file.last_updated,
)

if image_file.stat().st_size >= MAX_IMAGE_SIZE:
LOGGER.warning(
"[%s] Image file '%s' is larger than %d MB, skipping upload",
type(ctx.service).__name__,
image_file,
MAX_IMAGE_SIZE / 1000 / 1000,
)
return should_log
if not ctx.service.upload_image(
object_id=obj.id,
image_file=image_file,
file_type=cache_key.type,
kometa_integration=ctx.kometa_integration,
):
ctx.service.cache.update_service(
key=cache_key,
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
timestamp=None,
)
setattr(obj, uploaded_attr, False)
return should_log
ctx.service.cache.update_service(
key=cache_key,
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
timestamp=datetime.now(tz=timezone.utc),
upload_image(
image_file=image_file, ctx=ctx, obj=obj, cache_key=cache_key, uploaded_attr=uploaded_attr
)
setattr(obj, uploaded_attr, True)
return should_log


Expand All @@ -349,10 +394,7 @@ def process_entry_images(


def process_show_data(entry: Show, set_data: ShowSet, ctx: ProcessContext) -> None:
should_log = True
should_log = process_entry_images(
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
)
should_log = process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)
show_parent = slugify(value=entry.display_name)
try:
seasons = ctx.service.list_seasons(show_id=entry.id)
Expand Down Expand Up @@ -410,10 +452,7 @@ def process_show_data(entry: Show, set_data: ShowSet, ctx: ProcessContext) -> No
def process_collection_data(
entry: Collection, set_data: CollectionSet, ctx: ProcessContext
) -> None:
should_log = True
should_log = process_entry_images(
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
)
should_log = process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)
try:
movies = ctx.service.list_collection_movies(collection_id=entry.id)
except ServiceError as err:
Expand All @@ -439,7 +478,4 @@ def process_collection_data(


def process_movie_data(entry: Movie, set_data: MovieSet, ctx: ProcessContext) -> None:
should_log = True
should_log = process_entry_images(
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
)
process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)
2 changes: 2 additions & 0 deletions mediux_posters/cli/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,11 @@ def media_posters( # noqa: C901
ctx = ProcessContext(
mediux=mediux,
service=service,
covers_cache=settings.covers.path,
priority_usernames=settings.priority_usernames,
excluded_usernames=settings.exclude_usernames,
kometa_integration=settings.kometa_integration,
store_cover=settings.covers.store,
)
for set_data in filtered_sets:
if media_type is MediaType.SHOW:
Expand Down
2 changes: 2 additions & 0 deletions mediux_posters/cli/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ def set_posters( # noqa: C901
mediux=mediux,
service=service,
force=True,
covers_cache=settings.covers.path,
priority_usernames=settings.priority_usernames,
excluded_usernames=settings.exclude_usernames,
kometa_integration=settings.kometa_integration,
store_cover=settings.covers.store,
)
if media_type is MediaType.SHOW:
assert isinstance(set_data, ShowSet) # noqa: S101
Expand Down
2 changes: 2 additions & 0 deletions mediux_posters/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ def sync_posters( # noqa: C901
ctx = ProcessContext(
mediux=mediux,
service=service,
covers_cache=settings.covers.path,
priority_usernames=settings.priority_usernames,
excluded_usernames=settings.exclude_usernames,
kometa_integration=settings.kometa_integration,
store_cover=settings.covers.store,
)
entry_count = len(entries)
for idx, entry in enumerate(entries, start=1):
Expand Down
Loading
Loading