Skip to content
Closed
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 buzz/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ RUN apk add --no-cache \

# Install dependencies first to improve Docker layer caching
COPY pyproject.toml README.md buzz.dist.yml /app/
RUN mkdir -p /app/buzz/core && touch /app/buzz/__init__.py /app/buzz/core/__init__.py
RUN uv pip install --system .
RUN mkdir -p /app/buzz/core && touch /app/buzz/__init__.py /app/buzz/core/__init__.py && \
uv pip install --system .

# Copy application code and scripts
COPY buzz /app/buzz
Expand Down
263 changes: 143 additions & 120 deletions buzz/core/curator.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,63 @@
return report


def _process_movie_file(
path: Path,
source_root: Path,
target_root: Path,
all_source_root: Path,
overrides: dict,
used_targets: set[str],
report: dict,
mapping: list[dict],
) -> bool:
rel_path = source_relpath(all_source_root, path)
source_rel = path.relative_to(source_root)
folder = source_rel.parts[0] if len(source_rel.parts) > 1 else ""

parsed = parse_movie(path.stem, folder=folder)
override = overrides.get(rel_path, {})
if parsed is None and not override:
report["skipped_movies"].append(
{"source": rel_path, "reason": "unable to parse movie title/year"}
)
return False
if parsed is None:
parsed = {"title": "", "year": 0}
apply_movie_override(parsed, override)
if not parsed.get("title") or not parsed.get("year"):
report["skipped_movies"].append(
{"source": rel_path, "reason": "movie override missing title/year"}
)
return False

folder_name = movie_folder_name(parsed)
target_file = target_root / folder_name / f"{folder_name}{path.suffix.lower()}"
target_key = target_file.as_posix()
if target_key in used_targets:
report["skipped_movies"].append(
{"source": rel_path, "reason": "duplicate canonical movie target"}
)
return False

ensure_symlink(path, target_file)
used_targets.add(target_key)
mapping.append(
{
"source": rel_path,
"target": target_file.relative_to(target_root.parent).as_posix(),
"type": "movie",
}
)
report["movies"] += 1

for companion in find_companion_files(path):
extra = companion.name[len(path.stem) :]
companion_target = target_root / folder_name / f"{folder_name}{extra}"
ensure_symlink(companion, companion_target)
return True


def build_movies(
source_root: Path,
target_root: Path,
Expand All @@ -299,70 +356,119 @@
target_root.mkdir(parents=True, exist_ok=True)
if not source_root.exists():
return
used_targets = set()
used_targets: set[str] = set()
for path in iter_files(source_root):
if not is_video_file(path):
continue
rel_path = source_relpath(all_source_root, path)
_process_movie_file(
path, source_root, target_root, all_source_root,
overrides, used_targets, report, mapping,
)

# Determine torrent folder name if file is in a subdirectory
source_rel = path.relative_to(source_root)
folder = source_rel.parts[0] if len(source_rel.parts) > 1 else ""

parsed = parse_movie(path.stem, folder=folder)
def _plan_show_group(
files: list[Path],
source_root: Path,

Check warning on line 371 in buzz/core/curator.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "source_root".

See more on https://sonarcloud.io/project/issues?id=gchamon_buzz&issues=AZ3BwXVUXx17URvulg-F&open=AZ3BwXVUXx17URvulg-F&pullRequest=22
target_root: Path,
all_source_root: Path,
overrides: dict,
global_targets: set[str],
) -> tuple[list[dict], list[dict]]:
planned = []
group_errors = []
group_series = None
used_targets: set[str] = set()
for path in sorted(files):
rel_path = source_relpath(all_source_root, path)
parsed = parse_show(path.stem)
override = overrides.get(rel_path, {})
if parsed is None and not override:
report["skipped_movies"].append(
group_errors.append(
{"source": rel_path, "reason": "unable to parse show season/episode"}
)
continue
if parsed is None:
parsed = {"series": "", "season": 0, "episode": 0}
apply_show_override(parsed, override)
if (
not parsed.get("series")
or parsed.get("season") is None
or parsed.get("episode") is None
):
group_errors.append(
{
"source": rel_path,
"reason": "unable to parse movie title/year",
"reason": "show override missing series/season/episode",
}
)
continue
if parsed is None:
parsed = {"title": "", "year": 0}
apply_movie_override(parsed, override)
if not parsed.get("title") or not parsed.get("year"):
report["skipped_movies"].append(
series_name = show_series_name(parsed)
if group_series is None:
group_series = series_name
elif group_series != series_name:
group_errors.append(
{
"source": rel_path,
"reason": "movie override missing title/year",
"reason": "inconsistent parsed show name within torrent",
}
)
continue
folder_name = movie_folder_name(parsed)
season_dir = f"Season {int(parsed['season']):02d}"
base_name = (
f"{series_name} S{int(parsed['season']):02d}"
f"E{int(parsed['episode']):02d}"
)
target_file = (
target_root
/ folder_name
/ f"{folder_name}{path.suffix.lower()}"
/ series_name
/ season_dir
/ f"{base_name}{path.suffix.lower()}"
)
target_key = target_file.as_posix()
if target_key in used_targets:
report["skipped_movies"].append(
{
"source": rel_path,
"reason": "duplicate canonical movie target",
}
if target_key in used_targets or target_key in global_targets:
group_errors.append(
{"source": rel_path, "reason": "duplicate season/episode target"}
)
continue
ensure_symlink(path, target_file)
used_targets.add(target_key)
planned.append(
{
"path": path,
"rel_path": rel_path,
"target_file": target_file,
"base_name": base_name,
}
)
return planned, group_errors


def _apply_show_planned(
planned: list[dict],
target_root: Path,
global_targets: set[str],
mapping: list[dict],
report: dict,
) -> None:
for item in planned:
path = item["path"]
rel_path = item["rel_path"]
target_file = item["target_file"]
base_name = item["base_name"]
ensure_symlink(path, target_file)
global_targets.add(target_file.as_posix())
mapping.append(
{
"source": rel_path,
"target": target_file.relative_to(
target_root.parent
).as_posix(),
"type": "movie",
"type": "show",
}
)
report["movies"] += 1

report["show_files"] += 1
for companion in find_companion_files(path):
extra = companion.name[len(path.stem) :]
companion_target = (
target_root / folder_name / f"{folder_name}{extra}"
)
companion_target = target_file.parent / f"{base_name}{extra}"
ensure_symlink(companion, companion_target)


Expand All @@ -378,8 +484,8 @@
target_root.mkdir(parents=True, exist_ok=True)
if not source_root.exists():
return
grouped = {}
global_targets = set()
grouped: dict[str, list[Path]] = {}
global_targets: set[str] = set()
for path in iter_files(source_root):
if not is_video_file(path):
continue
Expand All @@ -388,99 +494,16 @@
grouped.setdefault(group_key, []).append(path)

for group_name, files in sorted(grouped.items()):
planned = []
group_errors = []
group_series = None
used_targets = set()
for path in sorted(files):
rel_path = source_relpath(all_source_root, path)
parsed = parse_show(path.stem)
override = overrides.get(rel_path, {})
if parsed is None and not override:
group_errors.append(
{
"source": rel_path,
"reason": "unable to parse show season/episode",
}
)
continue
if parsed is None:
parsed = {"series": "", "season": 0, "episode": 0}
apply_show_override(parsed, override)
if (
not parsed.get("series")
or parsed.get("season") is None
or parsed.get("episode") is None
):
group_errors.append(
{
"source": rel_path,
"reason": (
"show override missing series/season/episode"
),
}
)
continue
if group_series is None:
group_series = show_series_name(parsed)
elif group_series != show_series_name(parsed):
group_errors.append(
{
"source": rel_path,
"reason": (
"inconsistent parsed show name within torrent"
),
}
)
continue
season_dir = f"Season {int(parsed['season']):02d}"
base_name = (
f"{show_series_name(parsed)} "
f"S{int(parsed['season']):02d}"
f"E{int(parsed['episode']):02d}"
)
target_file = (
target_root
/ show_series_name(parsed)
/ season_dir
/ f"{base_name}{path.suffix.lower()}"
)
target_key = target_file.as_posix()
if target_key in used_targets or target_key in global_targets:
group_errors.append(
{
"source": rel_path,
"reason": "duplicate season/episode target",
}
)
continue
used_targets.add(target_key)
planned.append((path, rel_path, target_file))

planned, group_errors = _plan_show_group(
files, source_root, target_root, all_source_root,
overrides, global_targets,
)
if group_errors:
report["skipped_shows"].append(
{"group": group_name, "errors": group_errors}
)
continue

for path, rel_path, target_file in planned:
ensure_symlink(path, target_file)
global_targets.add(target_file.as_posix())
mapping.append(
{
"source": rel_path,
"target": target_file.relative_to(
target_root.parent
).as_posix(),
"type": "show",
}
)
report["show_files"] += 1
base_name = target_file.stem
for companion in find_companion_files(path):
extra = companion.name[len(path.stem) :]
companion_target = target_file.parent / f"{base_name}{extra}"
ensure_symlink(companion, companion_target)
_apply_show_planned(planned, target_root, global_targets, mapping, report)


def build_anime(
Expand Down
4 changes: 2 additions & 2 deletions buzz/core/media_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ def trigger_jellyfin_selective_refresh(
},
)
try:
with request.urlopen(req, timeout=30):
pass
with request.urlopen(req, timeout=30) as resp:
_ = resp.read()
except Exception as exc:
record_event(
f"Failed to refresh Jellyfin library '{name}': {exc}",
Expand Down
Loading
Loading