Compare commits
2
Commits
4daf4e960d
..
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b3ec81b1b | ||
|
|
364b515e3c |
+9
-5
@@ -1,4 +1,8 @@
|
|||||||
# Python
|
# --- Rust ---------------------------------------------------------------
|
||||||
|
# Cargo build output (debug/release builds, incremental caches)
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# --- Python -------------------------------------------------------------
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
@@ -11,15 +15,16 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
# Environment and secrets
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Logs (loguru file sink)
|
# Logs (loguru file sink)
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
# PyInstaller build artifacts (dist/ is committed — see DESIGN_DOCUMENT §13)
|
# PyInstaller build artifacts (dist/ is committed — see DESIGN_DOCUMENT §13)
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# --- Common -------------------------------------------------------------
|
||||||
|
# Environment and secrets
|
||||||
|
.env
|
||||||
|
|
||||||
# Curator runtime metadata (user/machine-specific, regenerated)
|
# Curator runtime metadata (user/machine-specific, regenerated)
|
||||||
.Curator.!gtag
|
.Curator.!gtag
|
||||||
*.!gtag
|
*.!gtag
|
||||||
@@ -35,4 +40,3 @@ AGENTS.md
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
DESIGN_DOCUMENT.md
|
DESIGN_DOCUMENT.md
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -19,21 +19,31 @@ Each version entry uses these sections (include only those that apply):
|
|||||||
- Always update this file before bumping the version in `pyproject.toml`
|
- Always update this file before bumping the version in `pyproject.toml`
|
||||||
- Document changes as they are made, not all at once at release time
|
- Document changes as they are made, not all at once at release time
|
||||||
|
|
||||||
## Unreleased
|
## 1.14.0 — 2026-10-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **The codebase is mypy-clean** and plain `uv run mypy` works (`[tool.mypy]`
|
||||||
|
in `pyproject.toml` lists the targets). Qt enums are now fully qualified
|
||||||
|
(`QDialog.DialogCode.Accepted`, `QMessageBox.StandardButton.Yes`,
|
||||||
|
`Qt.GlobalColor.red`, …) instead of PySide6's short aliases, and the few
|
||||||
|
loosely typed spots (tag schema, transforms, `on_files_changed`, dialog cell
|
||||||
|
widgets) are annotated. No behaviour change.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `src/core/media_utils.py` — Tagger-era leftovers (`load_icon` returned a
|
||||||
|
Tkinter image, `add_video_resolution_tag` logged via `print()`); nothing in
|
||||||
|
the app called either.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Removed `pillow` — `media_utils` was its only user.
|
||||||
|
|
||||||
|
## 1.13.0 — 2026-10-12
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **The standalone build is now a single file.** `Curator.spec` produces one
|
- **The standalone build is now a single file.** `Curator.spec` produces one
|
||||||
self-contained `dist/Curator` executable (onefile) instead of a
|
self-contained `dist/Curator` executable (onefile) instead of a
|
||||||
`dist/Curator/` folder of libraries. It unpacks to a temp folder on every
|
`dist/Curator/` folder of libraries. It unpacks to a temp folder on every
|
||||||
start, so launching takes a few seconds longer.
|
start, so launching takes a few seconds longer.
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Added `pyinstaller` to the dev group — `uv run pyinstaller Curator.spec`
|
|
||||||
now works out of the box (it was used for builds but never declared).
|
|
||||||
|
|
||||||
## 1.13.0 — 2026-10-12
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **Custom attributes are gone from the GUI.** The attribute table in
|
- **Custom attributes are gone from the GUI.** The attribute table in
|
||||||
*Upravit film* and *Filmy → "Nastavit atribut…"* (menu and context menu) are
|
*Upravit film* and *Filmy → "Nastavit atribut…"* (menu and context menu) are
|
||||||
removed. Values already stored keep working in miniDLNA filename templates
|
removed. Values already stored keep working in miniDLNA filename templates
|
||||||
@@ -473,6 +483,8 @@ Each version entry uses these sections (include only those that apply):
|
|||||||
Commands are now `uv sync` / `uv add` / `uv run` (`DESIGN_DOCUMENT_PYTHON.md`
|
Commands are now `uv sync` / `uv add` / `uv run` (`DESIGN_DOCUMENT_PYTHON.md`
|
||||||
§11). Dependency lower bounds moved up to the versions uv resolved; Poetry's
|
§11). Dependency lower bounds moved up to the versions uv resolved; Poetry's
|
||||||
upper bounds (`<7.0.0`, …) are gone.
|
upper bounds (`<7.0.0`, …) are gone.
|
||||||
|
- Added `pyinstaller` to the dev group — `uv run pyinstaller Curator.spec`
|
||||||
|
now works out of the box (it was used for builds but never declared).
|
||||||
- **ruff 0.15 → 0.17** (also mypy 2.4, pytest 9.1, PySide6 6.12, Pillow 12.3,
|
- **ruff 0.15 → 0.17** (also mypy 2.4, pytest 9.1, PySide6 6.12, Pillow 12.3,
|
||||||
python-dotenv 1.2.4). ruff 0.17 enables a much wider default rule set; the
|
python-dotenv 1.2.4). ruff 0.17 enables a much wider default rule set; the
|
||||||
whole codebase was brought in line with it — see *Changed* below.
|
whole codebase was brought in line with it — see *Changed* below.
|
||||||
|
|||||||
@@ -1,38 +1,63 @@
|
|||||||
# Curator
|
# Curator
|
||||||
|
|
||||||
Curator is a desktop **movie-library manager (Filmotéka)**. You import video
|
Curator is a PySide6 desktop app for managing a **Jellyfin-first movie and series
|
||||||
files into a managed **pool**, tag them, and Curator generates a browsable
|
library**. You import video files into a managed **pool**; Curator identifies them
|
||||||
**Filmotéka** — a directory tree of hardlinks into the pool — without ever
|
through **TMDb**, names and lays them out the way Jellyfin expects, and can
|
||||||
duplicating the actual video data on disk.
|
generate a genre-split **miniDLNA / Filmotéka** tree of hardlinks for dumb DLNA
|
||||||
|
players — without ever duplicating the video data on disk.
|
||||||
|
|
||||||
Curator is a fork of the former *Tagger* project; the tagging, filtering and
|
Metadata (posters, plots, `.nfo` files) is Jellyfin's job; Curator owns the files,
|
||||||
hardlink-tree machinery is inherited and extended into a full library workflow.
|
their names and the library structure.
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
- **Pool** — the managed repository of files and the **single source of truth**.
|
- **Pool** — the managed folder and the **single source of truth**. Curator
|
||||||
Curator owns it: it imports and removes files itself, so nothing moves behind
|
imports, renames and removes files itself, so nothing moves behind its back.
|
||||||
its back. The pool has two top-level folders:
|
Each top-level folder is a **library** with a kind:
|
||||||
- `Filmy` — movies, organized by tags into the generated tree.
|
|
||||||
- `Seriály` — series (and any other **copy-as-is** folder), mirrored verbatim.
|
|
||||||
- **Filmotéka (output)** — a generated tree of **hardlinks** into the pool. It is
|
|
||||||
fully disposable: delete it and nothing is lost, because it is regenerated from
|
|
||||||
the pool on demand.
|
|
||||||
- **Pool index** — all pool metadata lives in a single `.Curator.!index` JSON
|
|
||||||
file at the pool root (title, ČSFD link, tags, date per file).
|
|
||||||
|
|
||||||
## Workflow
|
| Kind | Holds | Layout |
|
||||||
|
|------|-------|--------|
|
||||||
|
| `movie` | movies | `Title (rok) [tmdbid].ext`, or a folder once a film has several versions |
|
||||||
|
| `series` | series | `Series (rok) [tmdbid]/Season NN/SxxEyy - Series.ext` |
|
||||||
|
| `mixed` | movies and series together | as above, side by side |
|
||||||
|
| `homevideo` | files without a TMDb id | mirrored as-is |
|
||||||
|
|
||||||
1. Configure the **pool** and the **Filmotéka output** folder.
|
- **Pool index** — Curator's own metadata (TMDb id, ČSFD link, versions, tags,
|
||||||
2. **Import a movie**: pick a video, enter its **Title** and a **ČSFD link**. The
|
library settings) lives in one `.Curator.!index` JSON file at the pool root.
|
||||||
file is copied (non-destructively) into `pool/Filmy` as `Title.ext` and
|
- **miniDLNA output** — a generated tree of **hardlinks** into the pool, built
|
||||||
recorded in the index. If a ČSFD link is given, Curator fetches the movie from
|
from the genres/years/ratings in Jellyfin's NFO files. It is disposable: delete
|
||||||
[ČSFD.cz](https://www.csfd.cz) and assigns **Žánr / Rok / Země původu** tags
|
it and regenerate it at any time.
|
||||||
automatically (use "Načíst tagy z ČSFD" to (re)fetch later).
|
|
||||||
3. **Tag** movies (Rok, Žánr, Hodnocení, …) and filter them in the UI.
|
## Features
|
||||||
4. **Generate the Filmotéka**: movies become a `Category/Tag/film` hardlink tree
|
|
||||||
(from the `Rok` / `Žánr` / `Hodnocení` categories); copy-as-is folders such as
|
- Import movies and episodes with a mandatory **TMDb id**; Czech titles come from
|
||||||
`Seriály` are mirrored 1:1 as hardlinks.
|
TMDb, with **ČSFD** as a fallback and as a bridge for finding the right TMDb id.
|
||||||
|
- **Multiple versions** of one film (2D/3D, 4K, director's cut) and **multi-part**
|
||||||
|
films and episodes, named so Jellyfin groups them.
|
||||||
|
- Subtitle sidecars follow their video through renames.
|
||||||
|
- Duplicate check, video integrity check (FFmpeg), library statistics.
|
||||||
|
- Jellyfin integration: trigger a library scan, read what Jellyfin has.
|
||||||
|
- miniDLNA tree scoped per library, with "Tipy dne" and "Nově přidané" folders.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [uv](https://docs.astral.sh/uv/) — manages Python 3.14 and all dependencies
|
||||||
|
- `ffmpeg` / `ffprobe` on `PATH` — optional, for the integrity check
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Secrets and settings are read from a `.env` file in the project root (never
|
||||||
|
committed):
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `TMDB_API_KEY` | TMDb API key — required for identifying movies and series |
|
||||||
|
| `TMDB_LANGUAGE` | TMDb language, default `cs-CZ` |
|
||||||
|
| `JELLYFIN_URL` | Jellyfin server URL — optional, for the Jellyfin menu |
|
||||||
|
| `JELLYFIN_API_KEY` | Jellyfin API key — optional |
|
||||||
|
| `ENV_DEBUG` | `true` enables debug mode |
|
||||||
|
|
||||||
|
The pool and output folders are chosen in the app (*Pool* menu).
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
@@ -41,20 +66,23 @@ uv sync
|
|||||||
uv run python Curator.py
|
uv run python Curator.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The GUI is built with **PySide6**.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run pytest # tests
|
uv run pytest # tests
|
||||||
uv run ruff check # lint / format
|
uv run ruff check # lint
|
||||||
|
uv run ruff format # format
|
||||||
uv run mypy # type checking
|
uv run mypy # type checking
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Detailed documentation — architecture and a per-module reference — is in
|
||||||
|
[`docs/`](docs/README.md).
|
||||||
|
|
||||||
## Building a standalone executable
|
## Building a standalone executable
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run pyinstaller Curator.spec
|
uv run python prebuild.py
|
||||||
|
uv run pyinstaller Curator.spec --noconfirm
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting `dist/Curator` is the distribution artifact.
|
The result is a single self-contained `dist/Curator` executable.
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ Filmotéka tree.
|
|||||||
| [`src/core/jellyfin.py`](modules/core.jellyfin.md) | Jellyfin server API client — thin, hybrid integration. |
|
| [`src/core/jellyfin.py`](modules/core.jellyfin.md) | Jellyfin server API client — thin, hybrid integration. |
|
||||||
| [`src/core/legacy_meta.py`](modules/core.legacy_meta.md) | Readers for Curator's *pre-index* metadata layouts. |
|
| [`src/core/legacy_meta.py`](modules/core.legacy_meta.md) | Readers for Curator's *pre-index* metadata layouts. |
|
||||||
| [`src/core/list_manager.py`](modules/core.list_manager.md) | Sorting of file lists by name or by date added. |
|
| [`src/core/list_manager.py`](modules/core.list_manager.md) | Sorting of file lists by name or by date added. |
|
||||||
| [`src/core/media_utils.py`](modules/core.media_utils.md) | Helpers around media files: loading small icons for Qt widgets and detecting the vertical video resolution with `ffprobe`, mapping it to a `Rozlišení/<height>p` tag. |
|
|
||||||
| [`src/core/migrate.py`](modules/core.migrate.md) | One-off migration of an existing pool to the Jellyfin naming convention. |
|
| [`src/core/migrate.py`](modules/core.migrate.md) | One-off migration of an existing pool to the Jellyfin naming convention. |
|
||||||
| [`src/core/movie.py`](modules/core.movie.md) | The movie object — metadata first, files second. |
|
| [`src/core/movie.py`](modules/core.movie.md) | The movie object — metadata first, files second. |
|
||||||
| [`src/core/naming.py`](modules/core.naming.md) | Pool file naming convention. |
|
| [`src/core/naming.py`](modules/core.naming.md) | Pool file naming convention. |
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# `src/core/media_utils.py`
|
|
||||||
|
|
||||||
Helpers around media files: loading small icons for Qt widgets and detecting the vertical video resolution with `ffprobe`, mapping it to a `Rozlišení/<height>p` tag.
|
|
||||||
|
|
||||||
Source: [`src/core/media_utils.py`](../../src/core/media_utils.py)
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### `load_icon`
|
|
||||||
|
|
||||||
*function · line 14*
|
|
||||||
|
|
||||||
```python
|
|
||||||
load_icon(path) -> ImageTk.PhotoImage
|
|
||||||
```
|
|
||||||
|
|
||||||
Load an image file and scale it to 16×16 px as a Qt-compatible icon.
|
|
||||||
|
|
||||||
### `add_video_resolution_tag`
|
|
||||||
|
|
||||||
*function · line 19*
|
|
||||||
|
|
||||||
```python
|
|
||||||
add_video_resolution_tag(file_obj: File, tagmanager: TagManager)
|
|
||||||
```
|
|
||||||
|
|
||||||
Read the vertical resolution of a video with `ffprobe` and tag the file with `Rozlišení/<height>p`. Needs FFmpeg on the PATH.
|
|
||||||
+4
-2
@@ -1,11 +1,10 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "curator"
|
name = "curator"
|
||||||
version = "1.13.0"
|
version = "1.14.0"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"beautifulsoup4>=4.15.0",
|
"beautifulsoup4>=4.15.0",
|
||||||
"loguru>=0.7.3",
|
"loguru>=0.7.3",
|
||||||
"pillow>=12.3.0",
|
|
||||||
"pyside6>=6.12.0",
|
"pyside6>=6.12.0",
|
||||||
"python-dotenv>=1.2.4",
|
"python-dotenv>=1.2.4",
|
||||||
"requests>=2.34.2",
|
"requests>=2.34.2",
|
||||||
@@ -22,3 +21,6 @@ dev = [
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
files = ["Curator.py", "prebuild.py", "src", "tests", "scripts"]
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
"""Auto-generated — do not edit manually."""
|
"""Auto-generated — do not edit manually."""
|
||||||
__version__ = "1.13.0"
|
__version__ = "1.14.0"
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ GLOBAL_CONFIG_FILE = Path(__file__).parent.parent.parent / ".Curator.!gtag"
|
|||||||
# folders (Dle …, Tipy dne, Nově přidané) before the genre folders at the root.
|
# folders (Dle …, Tipy dne, Nově přidané) before the genre folders at the root.
|
||||||
# This lives in the code as the default; the effective schema is stored per-pool
|
# This lives in the code as the default; the effective schema is stored per-pool
|
||||||
# in the index (``PoolIndex.settings["tag_schema"]``).
|
# in the index (``PoolIndex.settings["tag_schema"]``).
|
||||||
DEFAULT_TAG_SCHEMA = [
|
DEFAULT_TAG_SCHEMA: list[dict] = [
|
||||||
{
|
{
|
||||||
"category": "Žánr",
|
"category": "Žánr",
|
||||||
"csfd_field": "genres",
|
"csfd_field": "genres",
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ class FileManager:
|
|||||||
def __init__(self, tagmanager: TagManager):
|
def __init__(self, tagmanager: TagManager):
|
||||||
self.filelist: list[File] = []
|
self.filelist: list[File] = []
|
||||||
self.tagmanager = tagmanager
|
self.tagmanager = tagmanager
|
||||||
self.on_files_changed = None # callback do GUI
|
self.on_files_changed: Callable[[list[File]], None] | None = (
|
||||||
|
None # callback do GUI
|
||||||
|
)
|
||||||
self.global_config = load_global_config()
|
self.global_config = load_global_config()
|
||||||
self.index: PoolIndex | None = None # unified pool metadata + settings
|
self.index: PoolIndex | None = None # unified pool metadata + settings
|
||||||
|
|
||||||
@@ -689,7 +691,7 @@ class FileManager:
|
|||||||
total_size = 0
|
total_size = 0
|
||||||
with_nfo = 0
|
with_nfo = 0
|
||||||
untagged = 0
|
untagged = 0
|
||||||
ratings: list[int] = []
|
ratings: list[float] = []
|
||||||
categories: dict[str, Counter] = {}
|
categories: dict[str, Counter] = {}
|
||||||
|
|
||||||
for f in files:
|
for f in files:
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
# Module header
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .file import File
|
|
||||||
from .tag_manager import TagManager
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit("This module is not intended to be executed as the main program.")
|
|
||||||
|
|
||||||
# Imports
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
|
|
||||||
|
|
||||||
# Functions
|
|
||||||
def load_icon(path) -> ImageTk.PhotoImage:
|
|
||||||
img = Image.open(path)
|
|
||||||
img = img.resize((16, 16), Image.Resampling.LANCZOS)
|
|
||||||
return ImageTk.PhotoImage(img)
|
|
||||||
|
|
||||||
|
|
||||||
def add_video_resolution_tag(file_obj: File, tagmanager: TagManager):
|
|
||||||
"""
|
|
||||||
Zjistí vertikální rozlišení videa a přiřadí tag Rozlišení/{výška}p.
|
|
||||||
Vyžaduje ffprobe (FFmpeg).
|
|
||||||
"""
|
|
||||||
path = str(file_obj.file_path)
|
|
||||||
try:
|
|
||||||
# ffprobe vrátí width a height ve formátu JSON
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"ffprobe",
|
|
||||||
"-v",
|
|
||||||
"error",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=width,height",
|
|
||||||
"-of",
|
|
||||||
"csv=p=0:s=x",
|
|
||||||
path,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
res = result.stdout.strip() # např. "1920x1080"
|
|
||||||
if "x" not in res:
|
|
||||||
return
|
|
||||||
_width, height = map(int, res.split("x"))
|
|
||||||
tag_name = f"Rozlišení/{height}p"
|
|
||||||
tag_obj = tagmanager.add_tag("Rozlišení", f"{height}p")
|
|
||||||
file_obj.add_tag(tag_obj)
|
|
||||||
print(f"Přiřazen tag {tag_name} k {file_obj.filename}")
|
|
||||||
except (OSError, subprocess.SubprocessError, ValueError) as e:
|
|
||||||
print(f"Chyba při získávání rozlišení videa {file_obj.filename}: {e}")
|
|
||||||
@@ -6,6 +6,7 @@ value (e.g. rating 87 → tag "Hodnocení/87"), while the folder groups it
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
def rating_band(rating: int) -> str:
|
def rating_band(rating: int) -> str:
|
||||||
@@ -18,14 +19,14 @@ def rating_band(rating: int) -> str:
|
|||||||
return f"{low}–{high} %"
|
return f"{low}–{high} %"
|
||||||
|
|
||||||
|
|
||||||
def _decade_band(value) -> str:
|
def _decade_band(value: str) -> str:
|
||||||
"""``decade_band`` transform: pull the leading integer and bucket it."""
|
"""``decade_band`` transform: pull the leading integer and bucket it."""
|
||||||
match = re.search(r"\d+", str(value))
|
match = re.search(r"\d+", str(value))
|
||||||
return rating_band(int(match.group())) if match else str(value)
|
return rating_band(int(match.group())) if match else str(value)
|
||||||
|
|
||||||
|
|
||||||
# Named value transforms usable from the tag schema (config-driven, extensible).
|
# Named value transforms usable from the tag schema (config-driven, extensible).
|
||||||
TAG_TRANSFORMS = {
|
TAG_TRANSFORMS: dict[str | None, Callable[[str], str]] = {
|
||||||
None: str,
|
None: str,
|
||||||
"identity": str,
|
"identity": str,
|
||||||
"decade_band": _decade_band,
|
"decade_band": _decade_band,
|
||||||
|
|||||||
+234
-170
File diff suppressed because it is too large
Load Diff
@@ -1,76 +0,0 @@
|
|||||||
import tempfile
|
|
||||||
import tkinter as tk
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
|
|
||||||
from src.core.media_utils import load_icon
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def tk_root():
|
|
||||||
"""Fixture pro inicializaci Tkinteru (nutné pro ImageTk)."""
|
|
||||||
root = tk.Tk()
|
|
||||||
yield root
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_icon_returns_photoimage(tk_root):
|
|
||||||
"""Test že load_icon vrací PhotoImage"""
|
|
||||||
# vytvoříme dočasný obrázek
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
|
||||||
tmp_path = Path(tmp.name)
|
|
||||||
try:
|
|
||||||
# vytvoříme 100x100 červený obrázek
|
|
||||||
img = Image.new("RGB", (100, 100), color="red")
|
|
||||||
img.save(tmp_path)
|
|
||||||
|
|
||||||
icon = load_icon(tmp_path)
|
|
||||||
|
|
||||||
# musí být PhotoImage
|
|
||||||
assert isinstance(icon, ImageTk.PhotoImage)
|
|
||||||
|
|
||||||
# ověříme velikost 16x16
|
|
||||||
assert icon.width() == 16
|
|
||||||
assert icon.height() == 16
|
|
||||||
finally:
|
|
||||||
tmp_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_icon_resizes_image(tk_root):
|
|
||||||
"""Test že load_icon správně změní velikost obrázku"""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
|
||||||
tmp_path = Path(tmp.name)
|
|
||||||
try:
|
|
||||||
# vytvoříme velký obrázek 500x500
|
|
||||||
img = Image.new("RGB", (500, 500), color="blue")
|
|
||||||
img.save(tmp_path)
|
|
||||||
|
|
||||||
icon = load_icon(tmp_path)
|
|
||||||
|
|
||||||
# i velký obrázek by měl být zmenšen na 16x16
|
|
||||||
assert icon.width() == 16
|
|
||||||
assert icon.height() == 16
|
|
||||||
finally:
|
|
||||||
tmp_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_icon_different_formats(tk_root):
|
|
||||||
"""Test načítání různých formátů obrázků"""
|
|
||||||
formats = [".png", ".jpg", ".bmp"]
|
|
||||||
|
|
||||||
for fmt in formats:
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=fmt, delete=False) as tmp:
|
|
||||||
tmp_path = Path(tmp.name)
|
|
||||||
try:
|
|
||||||
img = Image.new("RGB", (32, 32), color="green")
|
|
||||||
img.save(tmp_path)
|
|
||||||
|
|
||||||
icon = load_icon(tmp_path)
|
|
||||||
|
|
||||||
assert isinstance(icon, ImageTk.PhotoImage)
|
|
||||||
assert icon.width() == 16
|
|
||||||
assert icon.height() == 16
|
|
||||||
finally:
|
|
||||||
tmp_path.unlink(missing_ok=True)
|
|
||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for the movie object and the pool layout it derives (src.core.movie)."""
|
"""Tests for the movie object and the pool layout it derives (src.core.movie)."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -79,8 +80,8 @@ class TestIdentityKey:
|
|||||||
assert identity_key(None, "Nějaký film", None) == "name-nejaky-film"
|
assert identity_key(None, "Nějaký film", None) == "name-nejaky-film"
|
||||||
|
|
||||||
|
|
||||||
def _movie(**kwargs) -> Movie:
|
def _movie(**kwargs: Any) -> Movie:
|
||||||
base = {
|
base: dict[str, Any] = {
|
||||||
"key": "tmdb-19995",
|
"key": "tmdb-19995",
|
||||||
"title": "Avatar",
|
"title": "Avatar",
|
||||||
"year": "2009",
|
"year": "2009",
|
||||||
|
|||||||
@@ -206,12 +206,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curator"
|
name = "curator"
|
||||||
version = "1.13.0"
|
version = "1.14.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pyside6" },
|
{ name = "pyside6" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
@@ -230,7 +229,6 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.15.0" },
|
{ name = "beautifulsoup4", specifier = ">=4.15.0" },
|
||||||
{ name = "loguru", specifier = ">=0.7.3" },
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "pillow", specifier = ">=12.3.0" },
|
|
||||||
{ name = "pyside6", specifier = ">=6.12.0" },
|
{ name = "pyside6", specifier = ">=6.12.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.4" },
|
{ name = "python-dotenv", specifier = ">=1.2.4" },
|
||||||
{ name = "requests", specifier = ">=2.34.2" },
|
{ name = "requests", specifier = ">=2.34.2" },
|
||||||
@@ -436,56 +434,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "12.3.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user