Skip to content

Commit 9ced905

Browse files
Covers cache changes (#97)
- Configurable covers cache location - Configure if you want to store images in the cache
1 parent 692034a commit 9ced905

13 files changed

Lines changed: 142 additions & 96 deletions

File tree

.github/workflows/linting.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ jobs:
2222
runs-on: ubuntu-latest
2323

2424
steps:
25-
- uses: actions/checkout@v6
26-
- uses: astral-sh/setup-uv@v7
25+
- uses: actions/checkout@v6.0.2
26+
- uses: astral-sh/setup-uv@v8.0.0
2727
with:
2828
python-version: ${{ env.PY_VERSION }}
2929
- name: Install project

.github/workflows/publishing.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ jobs:
2222
runs-on: ubuntu-latest
2323

2424
steps:
25-
- uses: actions/checkout@v6
26-
- uses: astral-sh/setup-uv@v7
25+
- uses: actions/checkout@v6.0.2
26+
- uses: astral-sh/setup-uv@v8.0.0
2727
with:
2828
python-version: ${{ env.PY_VERSION }}
2929
- name: Install project
3030
run: uv sync --locked --no-dev
3131
- name: Run build
3232
run: uv build
33-
- uses: actions/upload-artifact@v7
33+
- uses: actions/upload-artifact@v7.0.1
3434
with:
3535
name: python-package-distributions
3636
path: dist/
@@ -42,11 +42,11 @@ jobs:
4242
runs-on: ubuntu-latest
4343

4444
steps:
45-
- uses: actions/download-artifact@v8
45+
- uses: actions/download-artifact@v8.0.1
4646
with:
4747
name: python-package-distributions
4848
path: dist/
49-
- uses: astral-sh/setup-uv@v7
49+
- uses: astral-sh/setup-uv@v8.0.0
5050
with:
5151
python-version: ${{ env.PY_VERSION }}
5252
- name: Validate dist
@@ -65,8 +65,8 @@ jobs:
6565
runs-on: ubuntu-latest
6666

6767
steps:
68-
- uses: actions/download-artifact@v8
68+
- uses: actions/download-artifact@v8.0.1
6969
with:
7070
name: python-package-distributions
7171
path: dist/
72-
- uses: pypa/gh-action-pypi-publish@v1
72+
- uses: pypa/gh-action-pypi-publish@v1.14.0

.github/workflows/rich-codex.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@ jobs:
2727
runs-on: ubuntu-latest
2828

2929
steps:
30-
- uses: actions/checkout@v6
31-
- uses: astral-sh/setup-uv@v7
30+
- uses: actions/checkout@v6.0.2
31+
- uses: astral-sh/setup-uv@v8.0.0
3232
with:
3333
python-version: ${{ env.PY_VERSION }}
3434
- name: Install project
3535
run: uv sync --locked --dev --all-extras --managed-python
36-
- name: Generate terminal images with rich-codex
37-
uses: ewels/rich-codex@v1.2.11
36+
- uses: ewels/rich-codex@v1.2.11
3837
with:
3938
commit_changes: true
4039
clean_img_paths: docs/img/*.svg

.github/workflows/testing.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ jobs:
3737
runs-on: ${{ matrix.os }}
3838

3939
steps:
40-
- uses: actions/checkout@v6
41-
- uses: astral-sh/setup-uv@v7
40+
- uses: actions/checkout@v6.0.2
41+
- uses: astral-sh/setup-uv@v8.0.0
4242
with:
4343
python-version: ${{ matrix.python-version }}
4444
- name: Install project
4545
run: uv sync --locked --no-dev --all-extras --group tests --managed-python
4646
- name: Run tests
47-
run: uv run pytest
47+
run: uv run --no-dev pytest
4848
env:
4949
MEDIUX__BASE_URL: ${{ secrets.MEDIUX__BASE_URL }}
5050
MEDIUX__TOKEN: ${{ secrets.MEDIUX__TOKEN }}

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ kometa_integration = false
7979
only_priority_usernames = false
8080
priority_usernames = []
8181

82+
[covers]
83+
path = "~/.cache/mediux-posters/covers"
84+
store = true
85+
8286
[jellyfin]
8387
base_url = "http://127.0.0.1:8096"
8488
token = "<Token>"
@@ -94,6 +98,14 @@ token = "<Token>"
9498

9599
### Details
96100

101+
- `covers.path`
102+
103+
Folder location as to where to store downloaded covers.
104+
105+
- `covers.store`
106+
107+
Wether to store the images in the cache between runs, useful when running multiple services to not have to redownload images.
108+
97109
- `exclude_usernames`
98110

99111
A list of usernames whose sets should be ignored when running a sync.

mediux_posters/cli/common.py

Lines changed: 99 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from dataclasses import dataclass
1414
from datetime import datetime, timezone
1515
from enum import Enum
16+
from pathlib import Path
1617
from platform import python_version
1718
from typing import Final, Protocol, TypeVar
1819

@@ -35,7 +36,7 @@
3536
)
3637
from mediux_posters.services.service_cache import CacheData, CacheKey, ServiceCache
3738
from mediux_posters.settings import Settings
38-
from mediux_posters.utils import delete_folder, get_cached_image, slugify
39+
from mediux_posters.utils import delete_folder, slugify
3940

4041
LOGGER = logging.getLogger(__name__)
4142
DEFAULT_CREATOR_RANK: Final[int] = 1_000_000
@@ -69,10 +70,12 @@ def __str__(self) -> str:
6970
class ProcessContext:
7071
mediux: Mediux
7172
service: BaseService
73+
covers_cache: Path
7274
priority_usernames: list[str]
7375
excluded_usernames: list[str]
7476
force: bool = False
7577
kometa_integration: bool = False
78+
store_cover: bool = True
7679

7780

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

97+
settings = Settings.load().save()
98+
9499
if clean:
100+
LOGGER.info("Cleaning covers directory: '%s'", settings.covers.path)
101+
delete_folder(folder=settings.covers.path)
95102
LOGGER.info("Cleaning cache directory: '%s'", get_cache_root())
96103
delete_folder(folder=get_cache_root())
97104

98-
settings = Settings.load().save()
99105
if not settings.mediux.token:
100106
LOGGER.error("Missing Mediux token, check your settings")
101107
raise Abort
@@ -230,7 +236,79 @@ def determine_action( # noqa: PLR0911
230236
return Action.UPLOAD
231237

232238

233-
def process_image( # noqa: PLR0911
239+
def download_image(
240+
image_file: Path,
241+
ctx: ProcessContext,
242+
file: File,
243+
parent: str,
244+
cache_key: CacheKey,
245+
existing: CacheData | None,
246+
set_data: ShowSet | CollectionSet | MovieSet,
247+
) -> bool:
248+
image_file.unlink(missing_ok=True)
249+
try:
250+
ctx.mediux.download_image(file_id=file.id, output=image_file, parent_str=parent)
251+
except ServiceError as err:
252+
LOGGER.error("[Mediux] %s", err)
253+
ctx.service.cache.delete(key=cache_key)
254+
return False
255+
if not existing:
256+
ctx.service.cache.insert(
257+
key=cache_key,
258+
creator=set_data.username,
259+
set_id=set_data.id,
260+
last_updated=file.last_updated,
261+
)
262+
else:
263+
ctx.service.cache.update(
264+
key=cache_key,
265+
creator=set_data.username,
266+
set_id=set_data.id,
267+
last_updated=file.last_updated,
268+
)
269+
return True
270+
271+
272+
def upload_image(
273+
image_file: Path,
274+
ctx: ProcessContext,
275+
obj: Show | Season | Episode | Collection | Movie,
276+
cache_key: CacheKey,
277+
uploaded_attr: str,
278+
) -> None:
279+
if image_file.stat().st_size >= MAX_IMAGE_SIZE:
280+
LOGGER.warning(
281+
"[%s] Image file '%s' is larger than %d MB, skipping upload",
282+
type(ctx.service).__name__,
283+
image_file,
284+
MAX_IMAGE_SIZE / 1000 / 1000,
285+
)
286+
return
287+
upload_success = ctx.service.upload_image(
288+
object_id=obj.id,
289+
image_file=image_file,
290+
file_type=cache_key.type,
291+
kometa_integration=ctx.kometa_integration,
292+
)
293+
if not ctx.store_cover:
294+
image_file.unlink(missing_ok=True)
295+
if not upload_success:
296+
ctx.service.cache.update_service(
297+
key=cache_key,
298+
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
299+
timestamp=None,
300+
)
301+
setattr(obj, uploaded_attr, False)
302+
return
303+
ctx.service.cache.update_service(
304+
key=cache_key,
305+
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
306+
timestamp=datetime.now(tz=timezone.utc),
307+
)
308+
setattr(obj, uploaded_attr, True)
309+
310+
311+
def process_image(
234312
obj: Show | Season | Episode | Collection | Movie,
235313
cache_key: CacheKey,
236314
id_value: int | str,
@@ -247,7 +325,7 @@ def process_image( # noqa: PLR0911
247325
if not file:
248326
return should_log
249327

250-
image_file = get_cached_image(parent, filename)
328+
image_file = ctx.covers_cache.joinpath(parent, filename)
251329
existing = ctx.service.cache.select(key=cache_key)
252330
service_timestamp = ctx.service.cache.get_timestamp(
253331
key=cache_key,
@@ -265,6 +343,8 @@ def process_image( # noqa: PLR0911
265343
)
266344
if action is Action.SKIP:
267345
return should_log
346+
if not ctx.store_cover:
347+
action = Action.DOWNLOAD
268348
if should_log:
269349
LOGGER.info(
270350
"[Mediux] %sing '%s' by '%s'",
@@ -274,56 +354,21 @@ def process_image( # noqa: PLR0911
274354
)
275355
should_log = False
276356
if action is Action.DOWNLOAD or not image_file.exists():
277-
image_file.unlink(missing_ok=True)
278-
try:
279-
ctx.mediux.download_image(file_id=file.id, output=image_file, parent_str=parent)
280-
except ServiceError as err:
281-
LOGGER.error("[Mediux] %s", err)
282-
ctx.service.cache.delete(key=cache_key)
357+
download_success = download_image(
358+
image_file=image_file,
359+
ctx=ctx,
360+
file=file,
361+
parent=parent,
362+
cache_key=cache_key,
363+
existing=existing,
364+
set_data=set_data,
365+
)
366+
if not download_success:
283367
setattr(obj, uploaded_attr, False)
284368
return should_log
285-
if not existing:
286-
ctx.service.cache.insert(
287-
key=cache_key,
288-
creator=set_data.username,
289-
set_id=set_data.id,
290-
last_updated=file.last_updated,
291-
)
292-
else:
293-
ctx.service.cache.update(
294-
key=cache_key,
295-
creator=set_data.username,
296-
set_id=set_data.id,
297-
last_updated=file.last_updated,
298-
)
299-
300-
if image_file.stat().st_size >= MAX_IMAGE_SIZE:
301-
LOGGER.warning(
302-
"[%s] Image file '%s' is larger than %d MB, skipping upload",
303-
type(ctx.service).__name__,
304-
image_file,
305-
MAX_IMAGE_SIZE / 1000 / 1000,
306-
)
307-
return should_log
308-
if not ctx.service.upload_image(
309-
object_id=obj.id,
310-
image_file=image_file,
311-
file_type=cache_key.type,
312-
kometa_integration=ctx.kometa_integration,
313-
):
314-
ctx.service.cache.update_service(
315-
key=cache_key,
316-
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
317-
timestamp=None,
318-
)
319-
setattr(obj, uploaded_attr, False)
320-
return should_log
321-
ctx.service.cache.update_service(
322-
key=cache_key,
323-
service=type(ctx.service).__name__, # ty: ignore[invalid-argument-type]
324-
timestamp=datetime.now(tz=timezone.utc),
369+
upload_image(
370+
image_file=image_file, ctx=ctx, obj=obj, cache_key=cache_key, uploaded_attr=uploaded_attr
325371
)
326-
setattr(obj, uploaded_attr, True)
327372
return should_log
328373

329374

@@ -349,10 +394,7 @@ def process_entry_images(
349394

350395

351396
def process_show_data(entry: Show, set_data: ShowSet, ctx: ProcessContext) -> None:
352-
should_log = True
353-
should_log = process_entry_images(
354-
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
355-
)
397+
should_log = process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)
356398
show_parent = slugify(value=entry.display_name)
357399
try:
358400
seasons = ctx.service.list_seasons(show_id=entry.id)
@@ -410,10 +452,7 @@ def process_show_data(entry: Show, set_data: ShowSet, ctx: ProcessContext) -> No
410452
def process_collection_data(
411453
entry: Collection, set_data: CollectionSet, ctx: ProcessContext
412454
) -> None:
413-
should_log = True
414-
should_log = process_entry_images(
415-
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
416-
)
455+
should_log = process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)
417456
try:
418457
movies = ctx.service.list_collection_movies(collection_id=entry.id)
419458
except ServiceError as err:
@@ -439,7 +478,4 @@ def process_collection_data(
439478

440479

441480
def process_movie_data(entry: Movie, set_data: MovieSet, ctx: ProcessContext) -> None:
442-
should_log = True
443-
should_log = process_entry_images(
444-
entry=entry, set_data=set_data, ctx=ctx, should_log=should_log
445-
)
481+
process_entry_images(entry=entry, set_data=set_data, ctx=ctx, should_log=True)

mediux_posters/cli/media.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,11 @@ def media_posters( # noqa: C901
117117
ctx = ProcessContext(
118118
mediux=mediux,
119119
service=service,
120+
covers_cache=settings.covers.path,
120121
priority_usernames=settings.priority_usernames,
121122
excluded_usernames=settings.exclude_usernames,
122123
kometa_integration=settings.kometa_integration,
124+
store_cover=settings.covers.store,
123125
)
124126
for set_data in filtered_sets:
125127
if media_type is MediaType.SHOW:

mediux_posters/cli/set.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ def set_posters( # noqa: C901
113113
mediux=mediux,
114114
service=service,
115115
force=True,
116+
covers_cache=settings.covers.path,
116117
priority_usernames=settings.priority_usernames,
117118
excluded_usernames=settings.exclude_usernames,
118119
kometa_integration=settings.kometa_integration,
120+
store_cover=settings.covers.store,
119121
)
120122
if media_type is MediaType.SHOW:
121123
assert isinstance(set_data, ShowSet) # noqa: S101

mediux_posters/cli/sync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ def sync_posters( # noqa: C901
111111
ctx = ProcessContext(
112112
mediux=mediux,
113113
service=service,
114+
covers_cache=settings.covers.path,
114115
priority_usernames=settings.priority_usernames,
115116
excluded_usernames=settings.exclude_usernames,
116117
kometa_integration=settings.kometa_integration,
118+
store_cover=settings.covers.store,
117119
)
118120
entry_count = len(entries)
119121
for idx, entry in enumerate(entries, start=1):

0 commit comments

Comments
 (0)