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

7.5 KiB
Raw Permalink Blame History

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):

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:

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_ngdevedeng/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:

    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).