devedeng-extended/CLAUDE.md
Luna 691664095b Add pytest suite and CI (roadmap A1)
37 headless tests covering the logic that is cheap to break:
- split planner: plan_disc_split, chapter helpers (whole-fit, multi-disc
  partition, single-movie chapter/even-cut split, contiguous coverage)
- High-profile fill-disc bitrate math (disc count + bounds + capacity)
- ffmpeg command generation: profiles, text/image hardsub, 2-pass, nice,
  progress-to-file, even pad/crop + positive offsets, -ss seek, and the
  AC3 448k audio-clamp regression
- ffprobe subtitle detection: text/image classification, tags/disposition

tests/conftest.py makes the in-tree package importable and installs the
gettext _ builtin so modules run headless. Forgejo Actions workflow runs
the suite on push/PR. Update CLAUDE.md's testing section accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:26:22 -07:00

64 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
This is a **fork** of DeVeDe-NG (a GTK3 video-DVD authoring app) that adds, on top of upstream: embedded-subtitle **hardsubbing** (burn-in), selectable DVD **encode profiles**, and **auto-splitting** of over-long projects across multiple ISOs. See `FORK_NOTES.md` for the feature-level description of the fork's additions; this file covers how to work in the code.
## Running and installing
Run straight from the source tree without installing (best for iterating):
```bash
PYTHONPATH=src python3 -c "import devedeng.devedeng as d; d.py()"
```
`d.py()` calls `Gtk.main()` and blocks. When driving it from a tool/agent, launch detached so an editor/terminal crash can't wedge it:
```bash
nohup env PYTHONPATH=src python3 -c "import devedeng.devedeng as d; d.py()" >/tmp/devede_run.log 2>&1 & disown
```
Install (system): `sudo python3 -m pip install .` (add `--break-system-packages` if pip refuses). Entry point is `devede_ng``devedeng/devedeng.py:py()`.
Runtime external tools (must be on PATH): `ffmpeg`/`ffprobe` (or `avconv`/`avprobe`), `dvdauthor`, `mkisofs`/`genisoimage`, `spumux`, and optionally a player (vlc/mpv/mplayer) and burner (brasero/k3b/xfburn). The full dependency list lives in `stdeb.cfg` / `stpacman.cfg`.
## Testing
There is a **pytest suite in `tests/`** (run `python3 -m pytest`; it needs `pytest` and PyGObject/`gi`). Tests are headless — no display, no actual encoding — and import the in-tree package via `tests/conftest.py` (which prepends `src/` to the path and installs the gettext `_` builtin). CI runs them on Codeberg via `.forgejo/workflows/tests.yml`. Coverage focuses on the two things that are cheap to test and easy to break:
1. **Pure logic** (no GTK/display) — the split planner and bitrate math: `plan_disc_split`, `_movie_chapter_seconds`, `_nearest_chapter_before`, and `compute_high_profile_bitrate` in `project.py` are deliberately module-level pure functions so they can be unit-tested without instantiating GTK. See `tests/test_split_planner.py`, `tests/test_high_profile_bitrate.py`.
2. **Generated commands** — build a converter and assert on `command_var` without running anything (`tests/test_ffmpeg_command.py`); detection is checked by feeding `ffprobe.process_json` canned JSON (`tests/test_ffprobe_detection.py`). The `FakeFileProject`/`FakeMovie` factories and the `ffmpeg_converter`/`arg_after` helpers live in `conftest.py`. The config singleton works headless:
```python
cfg = devedeng.configuration_data.configuration.get_config(); cfg.disc_type = "dvd"
conv = devedeng.ffmpeg.ffmpeg(); conv.convert_file(mock_file_movie, "/tmp/out.mpg", 0)
print(conv.command_var) # the exact ffmpeg argv
```
`convert_file` reads `hardsub_index`/`hardsub_kind` off the `file_project` and the encode profile from config. For a full integration check, run a real short encode through the executor's GLib loop and watch a fake progress bar; the *windowed* UI can only be verified by launching the app on a real display.
`contrib/dvd-safe-converter.sh` is the known-good baseline the **Compatibility** profile is modelled on — use it as a regression reference for the no-subtitle DVD encode (mpeg2video / 720x480 / 29.97 / AC3 192k / 48k).
## Architecture
### Pluggable backend layer (`converter.py`)
Every external program is wrapped in a small class (`ffmpeg.py`, `avconv.py`, `ffprobe.py`, `avprobe.py`, `mkisofs.py`, `dvdauthor_converter.py`, players, burners). Each declares capability flags (`supports_convert`, `supports_analize`, `supports_menu`, `supports_mkiso`, `supports_burn`) and a `check_is_installed()` staticmethod. `converter` is a singleton (`converter.get_converter()`) that probes which tools are installed and exposes the chosen analyzer/converter/menuer/mkiso/burner. **Implication:** there are two parallel implementations of most things — `ffmpeg`/`ffprobe` (default) and `avconv`/`avprobe` (legacy libav). A change to the encode path or analyzer usually needs mirroring across both backends.
### Process execution & progress (`executor.py`)
All external commands run through `executor` (subclassed by every backend). A command is built into `self.command_var` (an argv list), launched via `launch_process`, and its stdout/stderr are read **asynchronously** through `GLib.IOChannel` watches that call `process_stdout`/`process_stderr` — this keeps the GTK main loop alive during long encodes. `executor.run()` and `wait_end()` call optional `start_progress_polling()`/`stop_progress_polling()` hooks (ffmpeg uses them). **Critical constraint:** ffmpeg progress is written to a `-progress` *file* and polled on a timer, deliberately **not** a pipe — a pipe deadlocks the encoder if the GUI ever stops draining it.
### Job orchestration (`runner.py` + executor dependency graph)
A `runner` shows the progress window with one bar per core (`config.multicore`, default cores1) and runs queued processes in parallel. Processes express ordering via `add_dependency()` / `add_child_process()`; the runner launches a process only when its dependencies have finished, freeing the bar for the next. A typical DVD job: N parallel `convert_file` encodes → `dvdauthor` (depends on all encodes) → `mkisofs` (depends on dvdauthor).
### Project model & UI binding (`interface_manager.py`, `project.py`, `file_movie.py`)
`interface_manager` auto-binds named GLADE widgets to Python attributes via `add_toggle`/`add_text`/`add_group`/etc., and serializes only those registered fields (`serialize`/`unserialize`). `devede_project` (`project.py`) is the main window and owns the file list, disc settings, and the create-disc flow. `file_movie` is one movie/title. **Gotcha:** attributes that are *not* registered with the `add_*` helpers (e.g. `hardsub_index`, `hardsub_kind`) are not serialized automatically — they are persisted explicitly in `file_movie.store_element`/`restore_element`, and skipped in group ("multiproperties") mode. Some newer UI (the hardsub picker, the encode-profile selector) is built **in Python at runtime** and packed into existing containers rather than added to the `.ui` files.
### Bitrate / size model (the math that ties profiles + splitting together)
`file_movie.set_final_rates()` maps the encode profile to `video_rate_final` / `audio_rate_final` (Compatibility = 4500k/192k, High = audio 448k + disc-fit video, Custom = user). `get_estimated_size()` / `get_kbytes_per_second()` derive size from those rates. `project.get_dvd_size()` returns per-disc capacity (already with a ~10% margin). The pure `plan_disc_split()` uses these to distribute whole titles across discs and time-split a single over-long movie at the chapter boundary nearest each disc's capacity; `dvdauthor_converter.create_dvd_project` takes a parallel `segment_lengths` list so a split segment's chapter markers stay inside the segment. NTSC DVD audio is AC3 for all profiles in this fork (upstream used MP2 for NTSC).
## Conventions worth matching
- ffmpeg/avconv commands are assembled by appending to `command_var`; keep additions in that style and mirror across both backends where the feature applies to both.
- MPEG-2/yuv420p needs **even** width/height — round pad/crop targets and use positive crop offsets (see the pad/crop block in `ffmpeg.py`).
- All user-visible strings go through `_()` (gettext); `.ui` labels use `translatable="yes"`. Translations live in `po/`.
- The build writes compiled locale `.mo` files into `src/devedeng/data/locale/` (gitignored).