Skip to content

Per-media-type metadata extraction at injectable seam - #28

Merged
mikeallisonJS merged 2 commits into
mainfrom
issue-21-per-media-metadata
May 29, 2026
Merged

Per-media-type metadata extraction at injectable seam#28
mikeallisonJS merged 2 commits into
mainfrom
issue-21-per-media-metadata

Conversation

@mikeallisonJS

@mikeallisonJS mikeallisonJS commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces monolithic metadata_extract.py with a metadata_extract/ package: per-type modules (audio, video, image, ebook), shared common helpers, and MEDIA_TYPE_EXTRACTORS registry used by extract_metadata.
  • Preserves the existing organize_plan injection seam (MetadataExtractor / _default_metadata_extractor); public imports unchanged.
  • Adds focused tests for video (MediaInfo + fallback) and ebook (EPUB, PDF without pypdf), plus organize-plan smoke coverage for all four media types.

Closes #21

Test plan

  • python -m pytest -q (80 passed)
  • Manual Preview: sample audio/video/image/ebook files show expected placeholder values
  • Manual Organize run: destination paths still sensible per media type

Made with Cursor

Summary by CodeRabbit

  • Refactor

    • Reorganized metadata extraction into a modular package structure for improved maintainability and extensibility across audio, video, image, and ebook file types.
    • Enhanced error handling and logging for metadata operations.
  • Tests

    • Added comprehensive test coverage for the new modular architecture across multiple media types.

Review Change Stack

Split the monolithic module into a package with per-type extractors behind a registry, add video and ebook test coverage, and verify organize-plan smoke paths for all four media types.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mikeallisonJS, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c85b29cc-8aa0-450c-8df3-b9c5b2189d50

📥 Commits

Reviewing files that changed from the base of the PR and between afd6d3a and c36c71e.

📒 Files selected for processing (4)
  • PROJECT.md
  • metadata_extract/image.py
  • metadata_extract/video.py
  • tests/test_metadata_extract.py
📝 Walkthrough

Walkthrough

The monolithic metadata_extract.py module has been refactored into a modular metadata_extract/ package. Four independent per-media-type extractors (audio, video, image, ebook) replace the prior single-module implementation. A registry maps media types to extractors, while the package API maintains backward compatibility. Tests have been expanded to cover all media types and optional dependency fallbacks.

Changes

Metadata extraction package refactor

Layer / File(s) Summary
Shared helpers and detection
metadata_extract/common.py, metadata_extract/detect.py
best_creation_timestamp() and add_common_file_metadata() provide platform-aware file metadata enrichment; detect_media_type() maps file extensions to media categories.
Per-media-type metadata extractors
metadata_extract/audio.py, metadata_extract/video.py, metadata_extract/image.py, metadata_extract/ebook.py
Four independent extractors handle their respective formats: audio via TinyTag; video via pymediainfo with TinyTag fallback; image via Pillow and EXIF; ebook dispatches PDF (pypdf), EPUB (ZIP + OPF XML), and MOBI (external library). Each includes graceful error handling and optional-dependency fallbacks.
Registry and package API
metadata_extract/registry.py, metadata_extract/__init__.py, PROJECT.md
MEDIA_TYPE_EXTRACTORS registry maps media types to extractor functions. High-level extract_metadata() entry point derives media type when omitted, dispatches to registered extractor, always adds common file metadata, and logs errors. Backward-compatible _best_creation_timestamp alias exported. Documentation updated.
Test infrastructure and validation
tests/conftest.py, tests/test_metadata_extract.py, tests/test_build_file_plan_paths.py
Test fixture write_minimal_epub() creates minimal EPUB archives. Existing tests updated to use module-specific patch targets. New coverage includes video with MediaInfo and fallback paths, EPUB DC field extraction, and PDF handling when pypdf is unavailable. Smoke test validates build_file_plan across all four media types with correct destination path segments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #21: Directly implements per-media-type metadata extraction behind the injectable organize_plan seam with expanded test coverage and graceful fallback handling for all media types.

Possibly related PRs

  • mikeallisonJS/archimedius#15: This PR refactors the metadata extraction layer that PR #15 depends on; the new modular structure enables composition between detection/extraction and destination path resolution.

Poem

🐰 From one big file to four small parts,
Each media type plays its part,
Audio, video, images bright,
Ebooks tucked in late at night—
A modular warren, organized just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main architectural change: moving from monolithic metadata extraction to per-media-type modules behind an injectable seam.
Linked Issues check ✅ Passed All four acceptance criteria from issue #21 are met: extractors are organized by media type in a registry [#21], tests cover all four media types including previously uncovered video/ebook paths [#21], sample files yield sensible destination paths [#21], and full test suite passes (80 tests) [#21].
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #21 objectives: metadata extraction restructuring, registry implementation, per-type modules, test coverage, and documentation updates. No unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-21-per-media-metadata

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
PROJECT.md (1)

114-118: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update PROJECT.md dependency docs to match the refactor (tinytag + pypdf).

PROJECT.md still references Mutagen and PyPDF2, but the codebase and requirements.txt use tinytag (TinyTag) for audio (and video fallback) and pypdf (PdfReader) for ebook/PDF metadata—Mutagen/PyPDF2 don’t appear in the implementation.

📝 Suggested doc fix
 - **Tkinter**: GUI framework
-- **Mutagen**: Audio metadata extraction
+- **tinytag**: Audio (and video fallback) metadata extraction
 - **Pillow (PIL)**: Image processing and metadata extraction
-- **PyPDF2**: PDF metadata extraction
+- **pypdf**: PDF metadata extraction
 - **MediaInfo** (optional): Enhanced video metadata extraction
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PROJECT.md` around lines 114 - 118, PROJECT.md still lists Mutagen and PyPDF2
but the code uses TinyTag (TinyTag) for audio/video fallback and pypdf
(PdfReader) for PDFs; update the dependency list to remove Mutagen and PyPDF2
and replace them with tinytag and pypdf, updating descriptions to mention
"TinyTag (tinytag) — audio and video metadata extraction" and "pypdf (PdfReader)
— PDF/ebook metadata extraction" and ensure the names match the identifiers used
in the codebase (TinyTag, PdfReader) and requirements.txt.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@metadata_extract/image.py`:
- Around line 22-35: Replace uses of the private _getexif() with the public
img.getexif(): call img.getexif() once, store its return in a local variable
(e.g., exif = img.getexif()), check that exif is truthy, then iterate
exif.items() and map tag numbers via the existing exif_tags dict into the
metadata dict (preserving the current keys camera_make, camera_model, date_time,
date_taken, copyright); remove the duplicate _getexif() calls and ensure you
reference img.getexif(), exif_tags, and metadata exactly as currently named.

In `@metadata_extract/video.py`:
- Around line 26-64: Initialize sensible defaults for all metadata keys used by
templates (e.g., set metadata["album"], metadata["artist"],
metadata["director"], metadata["year"], metadata["genre"], metadata["duration"],
metadata["width"], metadata["height"], metadata["frame_rate"],
metadata["codec"], metadata["bit_depth"] before the MEDIAINFO_AVAILABLE block)
so custom templates won't break when tags are absent, and when populating year
from MediaInfo use a robust parser instead of slicing: in the MediaInfo.parse
loop (see MediaInfo.parse, track.recorded_date) extract the first 4‑digit year
via regex (e.g., search for r"\b(19|20)\d{2}\b") or use a date parser to set
metadata["year"] only when a valid year is found; leave the default year if none
found.
- Around line 44-45: Replace the naive slice recorded_date[:4] with a
regex-based year extraction: add "import re" to metadata_extract/video.py, then
where you set metadata["year"] (inside the hasattr(track, "recorded_date") and
track.recorded_date check) run a regex like re.search(r'\b(\d{4})\b',
track.recorded_date) and if it matches assign metadata["year"] = match.group(1);
otherwise skip setting the year. This updates the code around the recorded_date
handling to reliably pull a four-digit year from strings like "UTC YYYY-MM-DD
..." without slicing.

---

Outside diff comments:
In `@PROJECT.md`:
- Around line 114-118: PROJECT.md still lists Mutagen and PyPDF2 but the code
uses TinyTag (TinyTag) for audio/video fallback and pypdf (PdfReader) for PDFs;
update the dependency list to remove Mutagen and PyPDF2 and replace them with
tinytag and pypdf, updating descriptions to mention "TinyTag (tinytag) — audio
and video metadata extraction" and "pypdf (PdfReader) — PDF/ebook metadata
extraction" and ensure the names match the identifiers used in the codebase
(TinyTag, PdfReader) and requirements.txt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d698dfbc-371d-4a8d-bea9-613f2a4a619a

📥 Commits

Reviewing files that changed from the base of the PR and between 47a172e and afd6d3a.

📒 Files selected for processing (13)
  • PROJECT.md
  • metadata_extract.py
  • metadata_extract/__init__.py
  • metadata_extract/audio.py
  • metadata_extract/common.py
  • metadata_extract/detect.py
  • metadata_extract/ebook.py
  • metadata_extract/image.py
  • metadata_extract/registry.py
  • metadata_extract/video.py
  • tests/conftest.py
  • tests/test_build_file_plan_paths.py
  • tests/test_metadata_extract.py
💤 Files with no reviewable changes (1)
  • metadata_extract.py

Comment thread metadata_extract/image.py Outdated
Comment thread metadata_extract/video.py
Comment thread metadata_extract/video.py Outdated
Use Pillow getexif(), parse video recorded_date year via regex, and align PROJECT.md dependency names with requirements.txt.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mikeallisonJS

Copy link
Copy Markdown
Owner Author

Review feedback addressed (c36c71e)

Fixed:

  • metadata_extract/image.py — use public Image.getexif() instead of _getexif()
  • metadata_extract/video.py — extract year from recorded_date via regex (handles UTC YYYY-MM-DD); test added
  • PROJECT.md — dependency list now matches requirements.txt (tinytag, pypdf)

Challenged:

  • metadata_extract/video.py (pre-fill all video keys with Unknown) — resolve_destination_path already maps missing placeholders to Unknown; default video template does not need those keys pre-populated. See inline reply on that thread.

Skipped:

  • Docstring coverage pre-merge warning — out of scope for this refactor PR; no inline thread.

@mikeallisonJS
mikeallisonJS merged commit 85503f3 into main May 29, 2026
3 checks passed
@mikeallisonJS
mikeallisonJS deleted the issue-21-per-media-metadata branch May 29, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-media-type metadata extraction at injectable seam

1 participant