restructure api stuff to doc and add refs
This commit is contained in:
parent
0d27c0221e
commit
a63758dda8
321
AGENTS.md
321
AGENTS.md
|
|
@ -1,302 +1,61 @@
|
|||
Analyze a file format of a song.
|
||||
# ProPresenter Parser — Agent Instructions
|
||||
|
||||
## Spec
|
||||
## Documentation
|
||||
|
||||
File: ./Test.pro (file ext are always .pro)
|
||||
All project documentation lives in `doc/`. Load only what you need.
|
||||
|
||||
- every song contains parts (name group here) (here: Verse 1, Verse 2, Chorus, ...) but could be any name
|
||||
- every group contains 1-x slides
|
||||
- every song contains different arrangements (here normal and test2) that defines the existence and the order of the groups
|
||||
- every slide CAN have another textbox which contains a translated version of the first textbox
|
||||
**Start here:** Read `doc/INDEX.md` for the table of contents and quick navigation.
|
||||
|
||||
## Status
|
||||
### How to Find What You Need
|
||||
|
||||
1. [x] Analyse the file structure and find all of the described specs.
|
||||
2. [x] Test and verify if the definition is correct - there is a `all-songs` directory with lot of examples.
|
||||
3. [x] Describe the structure for future AI prompts to use these files in `spec/pp_song_spec.md` and describe the usage in the `AGENTS.md` (replace obsolete commands)
|
||||
4. [x] Write a PHP module (is later used in laravel) in `./php` which can parse a song and let get/set every aspect of structure. Use Objects here (Song, Group, Slide, Arrangement, etc)
|
||||
5. [x] Create a simple PHP cli tool, which receive a param with a song file and show the structure of the song.
|
||||
1. Check `doc/INDEX.md` for the topic overview
|
||||
2. Use `doc/keywords.md` to search by keyword
|
||||
3. Load the specific doc file — don't load everything
|
||||
|
||||
## PHP Module Usage
|
||||
### Common Tasks
|
||||
|
||||
The ProPresenter song parser is available as a PHP module in `./php`. Use it to read, parse, and modify .pro song files.
|
||||
| Task | Load |
|
||||
|------|------|
|
||||
| Parse/modify `.pro` song files | `doc/api/song.md` |
|
||||
| Parse/modify `.proplaylist` files | `doc/api/playlist.md` |
|
||||
| Understand `.pro` binary format | `doc/formats/pp_song_spec.md` |
|
||||
| Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` |
|
||||
| Debug or troubleshoot | `doc/internal/issues.md` |
|
||||
| Add new documentation | `doc/CONTRIBUTING.md` |
|
||||
|
||||
### Reading a Song
|
||||
### Structure
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileReader;
|
||||
use ProPresenter\Parser\ProFileWriter;
|
||||
|
||||
$song = ProFileReader::read('path/to/song.pro');
|
||||
```
|
||||
doc/
|
||||
├── INDEX.md ← Start here (TOC + navigation)
|
||||
├── keywords.md ← Keyword search index
|
||||
├── CONTRIBUTING.md ← How to document new things
|
||||
├── formats/ ← Binary file format specs
|
||||
│ ├── pp_song_spec.md
|
||||
│ └── pp_playlist_spec.md
|
||||
├── api/ ← PHP API docs (read/write/generate)
|
||||
│ ├── song.md
|
||||
│ └── playlist.md
|
||||
└── internal/ ← Dev notes (learnings, decisions, issues)
|
||||
├── learnings.md
|
||||
├── decisions.md
|
||||
└── issues.md
|
||||
```
|
||||
|
||||
### Accessing Song Structure
|
||||
## Project Overview
|
||||
|
||||
```php
|
||||
// Basic song info
|
||||
echo $song->getName(); // Song name
|
||||
echo $song->getUuid(); // Song UUID
|
||||
PHP tools for parsing, modifying, and generating ProPresenter 7 files:
|
||||
|
||||
// CCLI metadata
|
||||
echo $song->getCcliAuthor(); // "Joel Houston, Matt Crocker"
|
||||
echo $song->getCcliSongTitle(); // "Oceans (Where Feet May Fail)"
|
||||
echo $song->getCcliPublisher(); // "2012 Hillsong Music Publishing"
|
||||
echo $song->getCcliCopyrightYear(); // 2012
|
||||
echo $song->getCcliSongNumber(); // 6428767
|
||||
echo $song->getCcliDisplay(); // true
|
||||
- **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations
|
||||
- **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs
|
||||
|
||||
// Other metadata
|
||||
echo $song->getCategory(); // ""
|
||||
echo $song->getNotes(); // ""
|
||||
echo $song->getSelectedArrangementUuid(); // "uuid-string"
|
||||
|
||||
// Groups (song parts like Verse 1, Chorus, etc.)
|
||||
foreach ($song->getGroups() as $group) {
|
||||
echo $group->getName(); // "Verse 1", "Chorus", etc.
|
||||
$slides = $song->getSlidesForGroup($group);
|
||||
foreach ($slides as $slide) {
|
||||
echo $slide->getPlainText();
|
||||
|
||||
// Optional cue label/title
|
||||
echo $slide->getLabel();
|
||||
|
||||
if ($slide->hasTranslation()) {
|
||||
echo $slide->getTranslation()->getPlainText();
|
||||
}
|
||||
|
||||
// Optional macro action on the cue
|
||||
if ($slide->hasMacro()) {
|
||||
echo $slide->getMacroName();
|
||||
echo $slide->getMacroUuid();
|
||||
echo $slide->getMacroCollectionName();
|
||||
echo $slide->getMacroCollectionUuid();
|
||||
}
|
||||
|
||||
// Optional media action on the cue (image/video)
|
||||
if ($slide->hasMedia()) {
|
||||
echo $slide->getMediaUrl();
|
||||
echo $slide->getMediaUuid();
|
||||
echo $slide->getMediaFormat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Arrangements (different song orderings)
|
||||
foreach ($song->getArrangements() as $arr) {
|
||||
$groups = $song->getGroupsForArrangement($arr);
|
||||
// Groups in arrangement order
|
||||
}
|
||||
|
||||
### Modifying and Writing
|
||||
|
||||
```php
|
||||
$song->setName("New Name");
|
||||
$song->setCcliAuthor("Author Name");
|
||||
$song->setCcliSongNumber(12345);
|
||||
$song->setCategory("Worship");
|
||||
$song->setNotes("Use acoustic intro");
|
||||
ProFileWriter::write($song, 'output.pro');
|
||||
```
|
||||
|
||||
### Generating a New Song
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileGenerator;
|
||||
|
||||
$song = ProFileGenerator::generate(
|
||||
'Song Name',
|
||||
[
|
||||
['name' => 'Verse 1', 'color' => [0.13, 0.59, 0.95, 1.0], 'slides' => [
|
||||
['text' => 'Line 1'],
|
||||
['text' => 'Line 2', 'translation' => 'Zeile 2'],
|
||||
['text' => 'Line 3', 'macro' => ['name' => 'Lied 1.Folie', 'uuid' => '20C1DFDE-0FB6-49E5-B90C-E6608D427212']],
|
||||
]],
|
||||
['name' => 'Media', 'color' => [0.2, 0.2, 0.2, 1.0], 'slides' => [
|
||||
['media' => 'file:///Users/me/Pictures/slide.jpg', 'format' => 'JPG', 'label' => 'slide.jpg'],
|
||||
['media' => 'file:///Users/me/Pictures/slide2.jpg', 'format' => 'JPG', 'label' => 'slide2.jpg', 'macro' => ['name' => '1:1 - Beamer & Stream', 'uuid' => 'A5911D80-622E-4AD6-A242-9278D0640048']],
|
||||
]],
|
||||
['name' => 'Chorus', 'color' => [0.95, 0.27, 0.27, 1.0], 'slides' => [
|
||||
['text' => 'Chorus text'],
|
||||
]],
|
||||
],
|
||||
[
|
||||
['name' => 'normal', 'groupNames' => ['Verse 1', 'Chorus', 'Verse 1']],
|
||||
],
|
||||
['author' => 'Author', 'song_title' => 'Title', 'copyright_year' => 2024],
|
||||
);
|
||||
|
||||
// Or generate and write in one call
|
||||
ProFileGenerator::generateAndWrite('output.pro', 'Song Name', $groups, $arrangements, $ccli);
|
||||
```
|
||||
|
||||
### Edit Label/Macro/Media Data
|
||||
|
||||
```php
|
||||
$slide = $song->getSlides()[0];
|
||||
|
||||
// Label (Cue.name)
|
||||
$slide->setLabel('Seniorennachmittag März.jpg');
|
||||
|
||||
// Macro action
|
||||
$slide->setMacro(
|
||||
'Lied 1.Folie',
|
||||
'20C1DFDE-0FB6-49E5-B90C-E6608D427212',
|
||||
'--MAIN--',
|
||||
'8D02FC57-83F8-4042-9B90-81C229728426',
|
||||
);
|
||||
|
||||
// Remove macro action
|
||||
$slide->removeMacro();
|
||||
|
||||
// Read media action
|
||||
if ($slide->hasMedia()) {
|
||||
$url = $slide->getMediaUrl();
|
||||
$format = $slide->getMediaFormat();
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Tool
|
||||
|
||||
Parse and display song structure from the command line:
|
||||
### CLI Tools
|
||||
|
||||
```bash
|
||||
php php/bin/parse-song.php path/to/song.pro
|
||||
```
|
||||
|
||||
## Format Specification
|
||||
|
||||
For detailed information about the .pro file format, see `spec/pp_song_spec.md`.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `php/src/Song.php` — Top-level song wrapper (metadata, CCLI, groups, slides, arrangements)
|
||||
- `php/src/Group.php` — Group (song part) wrapper
|
||||
- `php/src/Slide.php` — Slide wrapper with text access
|
||||
- `php/src/TextElement.php` — Text element with label + plain text
|
||||
- `php/src/Arrangement.php` — Arrangement wrapper
|
||||
- `php/src/RtfExtractor.php` — RTF to plain text converter
|
||||
- `php/src/ProFileReader.php` — Reads .pro files
|
||||
- `php/src/ProFileWriter.php` — Writes .pro files
|
||||
- `php/src/ProFileGenerator.php` — Generates .pro files from scratch
|
||||
- `php/bin/parse-song.php` — CLI tool (shows metadata, groups, slides, arrangements)
|
||||
- `spec/pp_song_spec.md` — Format specification
|
||||
|
||||
---
|
||||
|
||||
# ProPresenter Playlist Parser
|
||||
|
||||
Analyze and manage .proplaylist files.
|
||||
|
||||
## Spec
|
||||
|
||||
File: ./Test.proplaylist (file ext are always .proplaylist)
|
||||
|
||||
- every playlist is a ZIP archive containing metadata and embedded songs
|
||||
- every playlist contains entries (songs or groups) with type-specific data
|
||||
- entries can reference embedded songs or external song files
|
||||
- songs are lazily parsed on demand to optimize performance
|
||||
- playlists support custom metadata (name, notes, etc.)
|
||||
|
||||
## PHP Module Usage
|
||||
|
||||
The ProPresenter playlist parser is available as a PHP module in `./php`. Use it to read, parse, and modify .proplaylist files.
|
||||
|
||||
### Reading a Playlist
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistReader;
|
||||
use ProPresenter\Parser\ProPlaylistWriter;
|
||||
|
||||
$archive = ProPlaylistReader::read('path/to/playlist.proplaylist');
|
||||
```
|
||||
|
||||
### Accessing Playlist Structure
|
||||
|
||||
```php
|
||||
// Basic playlist info
|
||||
echo $archive->getName(); // Playlist name
|
||||
echo $archive->getUuid(); // Playlist UUID
|
||||
|
||||
// Metadata
|
||||
echo $archive->getNotes(); // Playlist notes
|
||||
|
||||
// Entries (songs or groups)
|
||||
foreach ($archive->getEntries() as $entry) {
|
||||
echo $entry->getType(); // 'song' or 'group'
|
||||
echo $entry->getName(); // Entry name
|
||||
echo $entry->getUuid(); // Entry UUID
|
||||
|
||||
// For song entries
|
||||
if ($entry->getType() === 'song') {
|
||||
echo $entry->getPath(); // File path or embedded reference
|
||||
|
||||
// Lazy-load embedded song
|
||||
if ($entry->isEmbedded()) {
|
||||
$song = $archive->getEmbeddedSong($entry);
|
||||
echo $song->getName();
|
||||
foreach ($song->getGroups() as $group) {
|
||||
echo $group->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For group entries
|
||||
if ($entry->getType() === 'group') {
|
||||
$children = $entry->getChildren();
|
||||
foreach ($children as $child) {
|
||||
echo $child->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Modifying and Writing
|
||||
|
||||
```php
|
||||
$archive->setName("New Playlist Name");
|
||||
$archive->setNotes("Updated notes");
|
||||
ProPlaylistWriter::write($archive, 'output.proplaylist');
|
||||
```
|
||||
|
||||
### Generating a New Playlist
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistGenerator;
|
||||
|
||||
$archive = ProPlaylistGenerator::generate(
|
||||
'Playlist Name',
|
||||
[
|
||||
['type' => 'song', 'name' => 'Song 1', 'path' => 'file:///path/to/song1.pro'],
|
||||
['type' => 'group', 'name' => 'Group 1', 'children' => [
|
||||
['type' => 'song', 'name' => 'Song 2', 'path' => 'file:///path/to/song2.pro'],
|
||||
['type' => 'song', 'name' => 'Song 3', 'path' => 'file:///path/to/song3.pro'],
|
||||
]],
|
||||
],
|
||||
['notes' => 'Sunday Service']
|
||||
);
|
||||
|
||||
// Or generate and write in one call
|
||||
ProPlaylistGenerator::generateAndWrite('output.proplaylist', 'Playlist Name', $entries, $metadata);
|
||||
```
|
||||
|
||||
## CLI Tool
|
||||
|
||||
Parse and display playlist structure from the command line:
|
||||
|
||||
```bash
|
||||
php php/bin/parse-playlist.php path/to/playlist.proplaylist
|
||||
```
|
||||
|
||||
## Format Specification
|
||||
### Key Source Files
|
||||
|
||||
For detailed information about the .proplaylist file format, see `spec/pp_playlist_spec.md`.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `php/src/PlaylistArchive.php` — Top-level playlist wrapper (metadata, entries, embedded songs)
|
||||
- `php/src/PlaylistEntry.php` — Playlist entry wrapper (song or group)
|
||||
- `php/src/ProPlaylistReader.php` — Reads .proplaylist files
|
||||
- `php/src/ProPlaylistWriter.php` — Writes .proplaylist files
|
||||
- `php/src/ProPlaylistGenerator.php` — Generates .proplaylist files from scratch
|
||||
- `php/bin/parse-playlist.php` — CLI tool (shows metadata, entries, embedded songs)
|
||||
- `spec/pp_playlist_spec.md` — Format specification
|
||||
All PHP source code is in `php/src/`. Generated protobuf classes are in `php/generated/`. Tests are in `php/tests/`.
|
||||
|
|
|
|||
174
doc/CONTRIBUTING.md
Normal file
174
doc/CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Documentation Guidelines
|
||||
|
||||
> How to maintain and extend the `doc/` directory for future AI agents and developers.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Load only what you need.** Each doc is self-contained for its topic. No doc should require reading other docs to be useful.
|
||||
2. **One topic per file.** Don't merge unrelated topics. Create a new file instead.
|
||||
3. **Keyword-searchable.** Every new doc must be added to `keywords.md`.
|
||||
4. **Code examples over prose.** Show the API call, not a paragraph explaining it.
|
||||
5. **Keep it current.** When you change code, update the corresponding doc.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
doc/
|
||||
├── INDEX.md ← Main entry point (TOC + quick nav)
|
||||
├── keywords.md ← Keyword search index (MUST update)
|
||||
├── CONTRIBUTING.md ← This file
|
||||
├── formats/ ← File format specifications (binary/protocol level)
|
||||
├── api/ ← PHP API documentation (how to use the code)
|
||||
└── internal/ ← Development notes (learnings, decisions, issues)
|
||||
```
|
||||
|
||||
### When to use which directory
|
||||
|
||||
| Directory | Content | Example |
|
||||
|-----------|---------|---------|
|
||||
| `formats/` | Binary file format specs, protobuf structure, encoding details | `pp_song_spec.md`, `pp_playlist_spec.md` |
|
||||
| `api/` | PHP class usage, method signatures, code examples | `song.md`, `playlist.md` |
|
||||
| `internal/` | Dev notes that help debug or understand history | `learnings.md`, `decisions.md`, `issues.md` |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Document
|
||||
|
||||
### Step 1: Create the file
|
||||
|
||||
Place it in the correct directory based on content type (see above).
|
||||
|
||||
### Step 2: Update INDEX.md
|
||||
|
||||
Add a line to the Table of Contents section under the appropriate heading:
|
||||
|
||||
```markdown
|
||||
### File Format Specifications
|
||||
- [formats/new_format_spec.md](formats/new_format_spec.md) -- Description
|
||||
```
|
||||
|
||||
### Step 3: Update keywords.md
|
||||
|
||||
Add entries for EVERY searchable keyword in your doc:
|
||||
|
||||
```markdown
|
||||
## New Category
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| keyword1 | [path/to/doc.md](path/to/doc.md) |
|
||||
| keyword2 | [path/to/doc.md](path/to/doc.md) Section N |
|
||||
```
|
||||
|
||||
If your keywords fit existing categories, add them there instead.
|
||||
|
||||
### Step 4: Cross-reference
|
||||
|
||||
If your doc relates to existing docs, add a "See Also" section at the bottom:
|
||||
|
||||
```markdown
|
||||
## See Also
|
||||
|
||||
- [Related Doc](../path/to/related.md) -- Why it's related
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Document Template
|
||||
|
||||
```markdown
|
||||
# Title
|
||||
|
||||
> One-line summary of what this doc covers.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
\`\`\`php
|
||||
// Most common usage pattern (2-5 lines)
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Section 1
|
||||
|
||||
Content with code examples.
|
||||
|
||||
---
|
||||
|
||||
## Section 2
|
||||
|
||||
More content.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `path/to/file.php` | What it does |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Doc](../path/to/related.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style Guide
|
||||
|
||||
### Headings
|
||||
- `#` for document title (one per file)
|
||||
- `##` for main sections
|
||||
- `###` for subsections
|
||||
- Use `---` horizontal rules between major sections
|
||||
|
||||
### Code Blocks
|
||||
- Always specify language: ` ```php `, ` ```bash `, ` ```markdown `
|
||||
- Show complete, runnable examples (not fragments)
|
||||
- Include the `use` statements on first occurrence
|
||||
|
||||
### Tables
|
||||
- Use tables for reference data (fields, files, options)
|
||||
- Left-align text columns, include header row
|
||||
|
||||
### Tone
|
||||
- Direct. No filler words.
|
||||
- "Use X to do Y" not "You can use X to do Y"
|
||||
- Show code first, explain after (only if needed)
|
||||
|
||||
---
|
||||
|
||||
## Updating Existing Docs
|
||||
|
||||
When modifying the codebase:
|
||||
|
||||
1. **New PHP method** → Update the corresponding `api/*.md`
|
||||
2. **New file format discovery** → Update `formats/*.md`
|
||||
3. **Architecture change** → Update `internal/decisions.md`
|
||||
4. **Bug/gotcha found** → Update `internal/issues.md`
|
||||
5. **Any change** → Check `keywords.md` for new keywords
|
||||
|
||||
---
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
When AGENTS.md tells you to load docs, follow these steps:
|
||||
|
||||
1. Read `doc/INDEX.md` to understand what's available
|
||||
2. Identify which docs match your task (use `keywords.md` if unsure)
|
||||
3. Load ONLY the relevant docs
|
||||
4. Do NOT load everything -- context window is precious
|
||||
|
||||
### Loading patterns
|
||||
|
||||
```
|
||||
Task: "Parse a song" → Load: doc/api/song.md
|
||||
Task: "Fix protobuf parsing" → Load: doc/formats/pp_song_spec.md
|
||||
Task: "Create a playlist" → Load: doc/api/playlist.md
|
||||
Task: "Debug ZIP issues" → Load: doc/formats/pp_playlist_spec.md + doc/internal/issues.md
|
||||
Task: "Add new feature" → Load: relevant api/ doc + doc/CONTRIBUTING.md
|
||||
```
|
||||
119
doc/INDEX.md
Normal file
119
doc/INDEX.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# ProPresenter Parser Documentation
|
||||
|
||||
> **For AI Agents**: Load only the documents you need. Use the keyword index to find relevant sections.
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
| Need | Load |
|
||||
|------|------|
|
||||
| Parse/modify `.pro` song files | [api/song.md](api/song.md) |
|
||||
| Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) |
|
||||
| Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
|
||||
| Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
| Add new documentation | [CONTRIBUTING.md](CONTRIBUTING.md) |
|
||||
| Search by keyword | [keywords.md](keywords.md) |
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
### File Format Specifications
|
||||
- [formats/pp_song_spec.md](formats/pp_song_spec.md) — ProPresenter 7 `.pro` file format (protobuf structure, RTF handling, field reference)
|
||||
- [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) — ProPresenter 7 `.proplaylist` file format (ZIP64 container, item types)
|
||||
|
||||
### PHP API Documentation
|
||||
- [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files)
|
||||
- [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files)
|
||||
|
||||
### Internal Reference
|
||||
- [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered
|
||||
- [internal/decisions.md](internal/decisions.md) — Architectural decisions and rationale
|
||||
- [internal/issues.md](internal/issues.md) — Known issues and edge cases
|
||||
|
||||
### Meta
|
||||
- [keywords.md](keywords.md) — Searchable keyword index
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — How to document new features
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
doc/
|
||||
├── INDEX.md ← You are here (main entry point)
|
||||
├── keywords.md ← Keyword search index
|
||||
├── CONTRIBUTING.md ← Documentation guidelines
|
||||
├── formats/ ← File format specifications
|
||||
│ ├── pp_song_spec.md
|
||||
│ └── pp_playlist_spec.md
|
||||
├── api/ ← PHP API documentation
|
||||
│ ├── song.md
|
||||
│ └── playlist.md
|
||||
└── internal/ ← Development notes (optional context)
|
||||
├── learnings.md
|
||||
├── decisions.md
|
||||
└── issues.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Load What
|
||||
|
||||
### Task: "Parse a song file"
|
||||
```
|
||||
Load: doc/api/song.md
|
||||
```
|
||||
|
||||
### Task: "Generate a new playlist"
|
||||
```
|
||||
Load: doc/api/playlist.md
|
||||
```
|
||||
|
||||
### Task: "Debug protobuf parsing issues"
|
||||
```
|
||||
Load: doc/formats/pp_song_spec.md (sections 2-5)
|
||||
```
|
||||
|
||||
### Task: "Understand translation handling"
|
||||
```
|
||||
Load: doc/api/song.md (section: Translations)
|
||||
Load: doc/formats/pp_song_spec.md (section 7: Translations)
|
||||
```
|
||||
|
||||
### Task: "Fix ZIP64 issues"
|
||||
```
|
||||
Load: doc/formats/pp_playlist_spec.md (section 4: ZIP64 Container Format)
|
||||
Load: doc/internal/learnings.md (search: Zip64Fixer)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
This project provides PHP tools to parse, modify, and generate ProPresenter 7 files:
|
||||
|
||||
- **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations
|
||||
- **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files
|
||||
|
||||
### Key Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `php/src/Song.php` | Song wrapper (read/modify `.pro` files) |
|
||||
| `php/src/ProFileReader.php` | Read `.pro` files |
|
||||
| `php/src/ProFileWriter.php` | Write `.pro` files |
|
||||
| `php/src/ProFileGenerator.php` | Generate `.pro` files from scratch |
|
||||
| `php/src/PlaylistArchive.php` | Playlist wrapper (read/modify `.proplaylist` files) |
|
||||
| `php/src/ProPlaylistReader.php` | Read `.proplaylist` files |
|
||||
| `php/src/ProPlaylistWriter.php` | Write `.proplaylist` files |
|
||||
| `php/src/ProPlaylistGenerator.php` | Generate `.proplaylist` files from scratch |
|
||||
|
||||
### CLI Tools
|
||||
|
||||
```bash
|
||||
# Parse and display song structure
|
||||
php php/bin/parse-song.php path/to/song.pro
|
||||
|
||||
# Parse and display playlist structure
|
||||
php php/bin/parse-playlist.php path/to/playlist.proplaylist
|
||||
```
|
||||
229
doc/api/playlist.md
Normal file
229
doc/api/playlist.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Playlist Parser API
|
||||
|
||||
> PHP module for reading, modifying, and generating ProPresenter `.proplaylist` files.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistReader;
|
||||
use ProPresenter\Parser\ProPlaylistWriter;
|
||||
use ProPresenter\Parser\ProPlaylistGenerator;
|
||||
|
||||
// Read
|
||||
$archive = ProPlaylistReader::read('path/to/playlist.proplaylist');
|
||||
|
||||
// Modify
|
||||
$archive->setName("New Playlist Name");
|
||||
ProPlaylistWriter::write($archive, 'output.proplaylist');
|
||||
|
||||
// Generate
|
||||
$archive = ProPlaylistGenerator::generate('Playlist Name', $entries, $metadata);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading Playlists
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistReader;
|
||||
|
||||
$archive = ProPlaylistReader::read('path/to/playlist.proplaylist');
|
||||
```
|
||||
|
||||
### Metadata Access
|
||||
|
||||
```php
|
||||
$archive->getName(); // Playlist name
|
||||
$archive->getUuid(); // Playlist UUID
|
||||
$archive->getNotes(); // Playlist notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entries
|
||||
|
||||
Entries are playlist items (songs, headers, placeholders).
|
||||
|
||||
```php
|
||||
foreach ($archive->getEntries() as $entry) {
|
||||
$entry->getType(); // 'song', 'header', 'placeholder', 'cue'
|
||||
$entry->getName(); // Entry display name
|
||||
$entry->getUuid(); // Entry UUID
|
||||
}
|
||||
```
|
||||
|
||||
### Song Entries (Presentations)
|
||||
|
||||
```php
|
||||
if ($entry->getType() === 'presentation') {
|
||||
$entry->getDocumentPath(); // "file:///path/to/song.pro"
|
||||
$entry->getDocumentFilename(); // "song.pro"
|
||||
$entry->getArrangementName(); // "normal"
|
||||
$entry->getArrangementUuid(); // "uuid-string"
|
||||
}
|
||||
```
|
||||
|
||||
### Header Entries
|
||||
|
||||
```php
|
||||
if ($entry->getType() === 'header') {
|
||||
$entry->getHeaderColor(); // [r, g, b, a] RGBA floats
|
||||
}
|
||||
```
|
||||
|
||||
### Embedded Songs
|
||||
|
||||
Playlists can contain embedded `.pro` files. Access them lazily:
|
||||
|
||||
```php
|
||||
if ($entry->isEmbedded()) {
|
||||
$song = $archive->getEmbeddedSong($entry);
|
||||
$song->getName();
|
||||
foreach ($song->getGroups() as $group) {
|
||||
echo $group->getName();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedded Files
|
||||
|
||||
```php
|
||||
// List embedded .pro files
|
||||
$proFiles = $archive->getEmbeddedProFiles();
|
||||
// ['Song1.pro' => $bytes, 'Song2.pro' => $bytes]
|
||||
|
||||
// List embedded media files
|
||||
$mediaFiles = $archive->getEmbeddedMediaFiles();
|
||||
// ['Users/me/Pictures/slide.jpg' => $bytes]
|
||||
|
||||
// Get specific embedded song
|
||||
$song = $archive->getEmbeddedSong($entry);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modifying Playlists
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistWriter;
|
||||
|
||||
$archive->setName("New Playlist Name");
|
||||
$archive->setNotes("Updated notes");
|
||||
|
||||
ProPlaylistWriter::write($archive, 'output.proplaylist');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generating Playlists
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProPlaylistGenerator;
|
||||
|
||||
$archive = ProPlaylistGenerator::generate(
|
||||
'Sunday Service',
|
||||
[
|
||||
[
|
||||
'type' => 'header',
|
||||
'name' => 'Worship',
|
||||
'color' => [0.95, 0.27, 0.27, 1.0],
|
||||
],
|
||||
[
|
||||
'type' => 'presentation',
|
||||
'name' => 'Amazing Grace',
|
||||
'path' => 'file:///path/to/amazing-grace.pro',
|
||||
'arrangement' => 'normal',
|
||||
],
|
||||
[
|
||||
'type' => 'presentation',
|
||||
'name' => 'Oceans',
|
||||
'path' => 'file:///path/to/oceans.pro',
|
||||
'arrangement' => 'verse-only',
|
||||
],
|
||||
[
|
||||
'type' => 'placeholder',
|
||||
'name' => 'TBD',
|
||||
],
|
||||
],
|
||||
['notes' => 'Sunday morning service']
|
||||
);
|
||||
|
||||
// Generate and write in one call
|
||||
ProPlaylistGenerator::generateAndWrite(
|
||||
'output.proplaylist',
|
||||
'Playlist Name',
|
||||
$entries,
|
||||
$metadata
|
||||
);
|
||||
```
|
||||
|
||||
### Entry Types
|
||||
|
||||
```php
|
||||
// Header (section divider)
|
||||
['type' => 'header', 'name' => 'Section Name', 'color' => [r, g, b, a]]
|
||||
|
||||
// Presentation (song reference)
|
||||
['type' => 'presentation', 'name' => 'Song Name', 'path' => 'file:///...', 'arrangement' => 'name']
|
||||
|
||||
// Placeholder (empty slot)
|
||||
['type' => 'placeholder', 'name' => 'TBD']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Tool
|
||||
|
||||
```bash
|
||||
php php/bin/parse-playlist.php path/to/playlist.proplaylist
|
||||
```
|
||||
|
||||
Output includes:
|
||||
- Playlist metadata (name, UUID, notes)
|
||||
- Entries with type-specific details
|
||||
- Embedded file counts
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```php
|
||||
try {
|
||||
$archive = ProPlaylistReader::read('playlist.proplaylist');
|
||||
} catch (\RuntimeException $e) {
|
||||
// File not found, empty file, invalid ZIP, or invalid protobuf
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ZIP64 Notes
|
||||
|
||||
ProPresenter exports playlists with a broken ZIP64 header (98-byte offset discrepancy). The reader automatically fixes this before parsing. The writer produces clean standard ZIPs without the bug.
|
||||
|
||||
See [Format Specification](../formats/pp_playlist_spec.md) Section 4 for details.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `php/src/PlaylistArchive.php` | Top-level playlist wrapper |
|
||||
| `php/src/PlaylistEntry.php` | Entry wrapper (song/header/placeholder) |
|
||||
| `php/src/PlaylistNode.php` | Playlist node wrapper |
|
||||
| `php/src/ProPlaylistReader.php` | Reads `.proplaylist` files |
|
||||
| `php/src/ProPlaylistWriter.php` | Writes `.proplaylist` files |
|
||||
| `php/src/ProPlaylistGenerator.php` | Generates `.proplaylist` files |
|
||||
| `php/src/Zip64Fixer.php` | Fixes ProPresenter ZIP64 header bug |
|
||||
| `php/bin/parse-playlist.php` | CLI tool |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Format Specification](../formats/pp_playlist_spec.md) — Binary format details
|
||||
- [Song API](song.md) — `.pro` file handling
|
||||
301
doc/api/song.md
Normal file
301
doc/api/song.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# Song Parser API
|
||||
|
||||
> PHP module for reading, modifying, and generating ProPresenter `.pro` song files.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileReader;
|
||||
use ProPresenter\Parser\ProFileWriter;
|
||||
use ProPresenter\Parser\ProFileGenerator;
|
||||
|
||||
// Read
|
||||
$song = ProFileReader::read('path/to/song.pro');
|
||||
|
||||
// Modify
|
||||
$song->setName("New Name");
|
||||
ProFileWriter::write($song, 'output.pro');
|
||||
|
||||
// Generate
|
||||
$song = ProFileGenerator::generate('Song Name', $groups, $arrangements, $ccli);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading Songs
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileReader;
|
||||
|
||||
$song = ProFileReader::read('path/to/song.pro');
|
||||
```
|
||||
|
||||
### Metadata Access
|
||||
|
||||
```php
|
||||
// Basic info
|
||||
$song->getName(); // "Amazing Grace"
|
||||
$song->getUuid(); // "A1B2C3D4-..."
|
||||
|
||||
// CCLI metadata
|
||||
$song->getCcliAuthor(); // "Joel Houston, Matt Crocker"
|
||||
$song->getCcliSongTitle(); // "Oceans (Where Feet May Fail)"
|
||||
$song->getCcliPublisher(); // "2012 Hillsong Music Publishing"
|
||||
$song->getCcliCopyrightYear(); // 2012
|
||||
$song->getCcliSongNumber(); // 6428767
|
||||
$song->getCcliDisplay(); // true
|
||||
|
||||
// Other metadata
|
||||
$song->getCategory(); // ""
|
||||
$song->getNotes(); // ""
|
||||
$song->getSelectedArrangementUuid(); // "uuid-string"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Groups
|
||||
|
||||
Groups are song parts (Verse 1, Chorus, Bridge, etc.).
|
||||
|
||||
```php
|
||||
foreach ($song->getGroups() as $group) {
|
||||
$group->getName(); // "Verse 1"
|
||||
$group->getUuid(); // "E5F6G7H8-..."
|
||||
$group->getColor(); // ['r' => 1.0, 'g' => 0.0, 'b' => 0.0, 'a' => 1.0] or null
|
||||
$group->getSlideUuids(); // ["uuid1", "uuid2", ...]
|
||||
}
|
||||
|
||||
// Get specific group
|
||||
$chorus = $song->getGroupByName("Chorus");
|
||||
|
||||
// Get slides for a group
|
||||
$slides = $song->getSlidesForGroup($group);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slides
|
||||
|
||||
Slides are individual presentation frames.
|
||||
|
||||
```php
|
||||
foreach ($song->getSlides() as $slide) {
|
||||
$slide->getUuid();
|
||||
$slide->getPlainText(); // Extracted from first text element
|
||||
$slide->getLabel(); // Optional cue label/title
|
||||
}
|
||||
|
||||
// Access all text elements
|
||||
foreach ($slide->getTextElements() as $textElement) {
|
||||
$textElement->getName(); // "Orginal", "Deutsch", etc.
|
||||
$textElement->getRtfData(); // Raw RTF bytes
|
||||
$textElement->getPlainText(); // Extracted plain text
|
||||
}
|
||||
```
|
||||
|
||||
### Translations
|
||||
|
||||
Multiple text elements per slide indicate translations.
|
||||
|
||||
```php
|
||||
if ($slide->hasTranslation()) {
|
||||
$translation = $slide->getTranslation();
|
||||
$translation->getPlainText(); // Translated text
|
||||
}
|
||||
```
|
||||
|
||||
### Macros
|
||||
|
||||
```php
|
||||
if ($slide->hasMacro()) {
|
||||
$slide->getMacroName(); // "Lied 1.Folie"
|
||||
$slide->getMacroUuid(); // "20C1DFDE-..."
|
||||
$slide->getMacroCollectionName(); // "--MAIN--"
|
||||
$slide->getMacroCollectionUuid(); // "8D02FC57-..."
|
||||
}
|
||||
```
|
||||
|
||||
### Media
|
||||
|
||||
```php
|
||||
if ($slide->hasMedia()) {
|
||||
$slide->getMediaUrl(); // "file:///Users/me/Pictures/slide.jpg"
|
||||
$slide->getMediaUuid(); // "uuid-string"
|
||||
$slide->getMediaFormat(); // "JPG"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arrangements
|
||||
|
||||
Arrangements define group order for presentations.
|
||||
|
||||
```php
|
||||
foreach ($song->getArrangements() as $arrangement) {
|
||||
$arrangement->getName(); // "normal"
|
||||
$arrangement->getGroupUuids(); // ["uuid1", "uuid2", "uuid1", ...] (can repeat)
|
||||
}
|
||||
|
||||
// Resolve groups in arrangement order
|
||||
$groups = $song->getGroupsForArrangement($arrangement);
|
||||
foreach ($groups as $group) {
|
||||
echo $group->getName();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modifying Songs
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileWriter;
|
||||
|
||||
// Metadata
|
||||
$song->setName("New Song Title");
|
||||
$song->setCategory("Worship");
|
||||
$song->setNotes("Use acoustic intro");
|
||||
|
||||
// CCLI
|
||||
$song->setCcliAuthor("Author Name");
|
||||
$song->setCcliSongTitle("Song Title");
|
||||
$song->setCcliPublisher("Publisher");
|
||||
$song->setCcliCopyrightYear(2024);
|
||||
$song->setCcliSongNumber(12345);
|
||||
$song->setCcliDisplay(true);
|
||||
|
||||
// Group names
|
||||
$group = $song->getGroupByName("Verse 1");
|
||||
$group->setName("Strophe 1");
|
||||
|
||||
// Slide labels/macros
|
||||
$slide = $song->getSlides()[0];
|
||||
$slide->setLabel('New Label');
|
||||
$slide->setMacro(
|
||||
'Macro Name',
|
||||
'macro-uuid',
|
||||
'--MAIN--',
|
||||
'collection-uuid'
|
||||
);
|
||||
$slide->removeMacro();
|
||||
|
||||
// Write
|
||||
ProFileWriter::write($song, 'output.pro');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generating Songs
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\ProFileGenerator;
|
||||
|
||||
$song = ProFileGenerator::generate(
|
||||
'Amazing Grace',
|
||||
[
|
||||
[
|
||||
'name' => 'Verse 1',
|
||||
'color' => [0.13, 0.59, 0.95, 1.0], // RGBA floats
|
||||
'slides' => [
|
||||
['text' => 'Amazing grace, how sweet the sound'],
|
||||
['text' => 'That saved a wretch like me', 'translation' => 'Der mich Verlornen fand'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Chorus',
|
||||
'color' => [0.95, 0.27, 0.27, 1.0],
|
||||
'slides' => [
|
||||
['text' => 'I once was lost, but now am found'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Media',
|
||||
'color' => [0.2, 0.2, 0.2, 1.0],
|
||||
'slides' => [
|
||||
['media' => 'file:///Users/me/Pictures/slide.jpg', 'format' => 'JPG', 'label' => 'slide.jpg'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
['name' => 'normal', 'groupNames' => ['Verse 1', 'Chorus', 'Verse 1']],
|
||||
],
|
||||
[
|
||||
'author' => 'John Newton',
|
||||
'song_title' => 'Amazing Grace',
|
||||
'copyright_year' => 1779,
|
||||
]
|
||||
);
|
||||
|
||||
// Generate and write in one call
|
||||
ProFileGenerator::generateAndWrite('output.pro', 'Song Name', $groups, $arrangements, $ccli);
|
||||
```
|
||||
|
||||
### Slide Options
|
||||
|
||||
```php
|
||||
// Text only
|
||||
['text' => 'Lyrics here']
|
||||
|
||||
// Text with translation
|
||||
['text' => 'English lyrics', 'translation' => 'Deutsche Lyrics']
|
||||
|
||||
// Text with macro
|
||||
['text' => 'Lyrics', 'macro' => ['name' => 'Macro Name', 'uuid' => 'macro-uuid']]
|
||||
|
||||
// Media slide
|
||||
['media' => 'file:///path/to/image.jpg', 'format' => 'JPG', 'label' => 'image.jpg']
|
||||
|
||||
// Media with macro
|
||||
['media' => 'file:///path/to/video.mp4', 'format' => 'MP4', 'label' => 'video.mp4', 'macro' => ['name' => 'Macro', 'uuid' => 'uuid']]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Tool
|
||||
|
||||
```bash
|
||||
php php/bin/parse-song.php path/to/song.pro
|
||||
```
|
||||
|
||||
Output includes:
|
||||
- Song metadata (name, UUID, CCLI info)
|
||||
- Groups with slide counts
|
||||
- Slides with text content and translations
|
||||
- Arrangements with group order
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```php
|
||||
try {
|
||||
$song = ProFileReader::read('song.pro');
|
||||
} catch (\RuntimeException $e) {
|
||||
// File not found, empty file, or invalid protobuf
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `php/src/Song.php` | Top-level song wrapper |
|
||||
| `php/src/Group.php` | Group (song part) wrapper |
|
||||
| `php/src/Slide.php` | Slide wrapper with text access |
|
||||
| `php/src/TextElement.php` | Text element with RTF extraction |
|
||||
| `php/src/Arrangement.php` | Arrangement wrapper |
|
||||
| `php/src/RtfExtractor.php` | RTF to plain text converter |
|
||||
| `php/src/ProFileReader.php` | Reads `.pro` files |
|
||||
| `php/src/ProFileWriter.php` | Writes `.pro` files |
|
||||
| `php/src/ProFileGenerator.php` | Generates `.pro` files |
|
||||
| `php/bin/parse-song.php` | CLI tool |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Format Specification](../formats/pp_song_spec.md) — Binary format details
|
||||
- [Playlist API](playlist.md) — `.proplaylist` file handling
|
||||
23
doc/internal/decisions.md
Normal file
23
doc/internal/decisions.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Architectural Decisions
|
||||
|
||||
## Decisions Made
|
||||
|
||||
### Proto Version Choice
|
||||
- **Decision**: Use greyshirtguy/ProPresenter7-Proto v7.16.2
|
||||
- **Reason**: Field numbers match Test.pro raw decode perfectly
|
||||
- **Source**: Metis analysis + typed decode validation in T2
|
||||
|
||||
### RTF Handling
|
||||
- **Getters**: Plain text only (via RtfExtractor)
|
||||
- **Internal**: Raw RTF preserved for round-trip integrity
|
||||
- **Write**: Template-clone approach (preserve formatting, swap text only)
|
||||
|
||||
### Scope Boundaries
|
||||
- **IN**: Read+write existing content, parse all reference files
|
||||
- **OUT**: Creating new slides/groups from scratch, Laravel integration, playlist formats
|
||||
|
||||
- 2026-03-01 task-2 autoload decision: added `GPBMetadata\` => `generated/GPBMetadata/` to `php/composer.json` so generated `Rv\Data` classes can initialize descriptor metadata at runtime.
|
||||
|
||||
- 2026-03-01 task-2 ZIP64 repair strategy: patch archive headers in-memory only (no recompression), applying deterministic EOCD/ZIP64 size corrections before any `ZipArchive` access.
|
||||
|
||||
- 2026-03-01 21:23:59 - ProPlaylist integration tests use temp files via tempnam() tracked in class state and cleaned in tearDown() to guarantee cleanup across all test methods.
|
||||
11
doc/internal/issues.md
Normal file
11
doc/internal/issues.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Issues & Gotchas
|
||||
|
||||
(Agents will log problems encountered here)
|
||||
|
||||
- 2026-03-01 task-2 edge case: `Du machst alles neu_ver2025-05-11-4.pro` is 0 bytes; `protoc --decode rv.data.Presentation` returns empty output (no decoded fields).
|
||||
- 2026-03-01 task-6 fidelity failure: `Rv\Data\Presentation::mergeFromString()->serializeToString()` is not byte-preserving for current generated schema/runtime (`169/169` mismatches, including `Test.pro` with `length_delta=-18`, first mismatch at byte `1205`), so unknown/opaque binary data is still being transformed or dropped.
|
||||
- 2026-03-01 task-7: no new parser blockers found; UTF-8 filename handling is stable when using raw PHP filesystem functions (`is_file`, `filesize`, `file_get_contents`).
|
||||
|
||||
- 2026-03-01 task-2 test gotcha: `unzip` may render UTF-8 filenames with replacement characters; entry-comparison tests normalize names before asserting equality with `ZipArchive` listing.
|
||||
|
||||
- 2026-03-01 21:23:59 - Generated header color values deserialize with float precision drift; fixed by assertEqualsWithDelta in generator interoperability test.
|
||||
364
doc/internal/learnings.md
Normal file
364
doc/internal/learnings.md
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
# Learnings — ProPresenter Parser
|
||||
|
||||
## Conventions & Patterns
|
||||
|
||||
(Agents will append findings here)
|
||||
|
||||
## Task 1: Project Scaffolding — Composer + PHPUnit + Directory Structure
|
||||
|
||||
### Completed
|
||||
- ✅ Created PHP 8.4 project with Composer
|
||||
- ✅ Configured PSR-4 autoloading for both namespaces:
|
||||
- `ProPresenter\Parser\` → `src/`
|
||||
- `Rv\Data\` → `generated/Rv/Data/`
|
||||
- ✅ Installed PHPUnit 11.5.55 with google/protobuf 4.33.5
|
||||
- ✅ Created phpunit.xml with strict settings
|
||||
- ✅ Created SmokeTest.php that passes
|
||||
- ✅ All 5 required directories created: src/, tests/, bin/, proto/, generated/
|
||||
|
||||
### Key Findings
|
||||
- PHP 8.4.7 is available on the system
|
||||
- Composer resolves dependencies cleanly (28 packages installed)
|
||||
- PHPUnit 11 runs with strict mode enabled (beStrictAboutOutputDuringTests, failOnRisky, failOnWarning)
|
||||
- Autoloading works correctly with both namespaces configured
|
||||
|
||||
### Verification Results
|
||||
- Composer install: ✅ Success (28 packages)
|
||||
- PHPUnit smoke test: ✅ 1 test passed
|
||||
- Autoload verification: ✅ Works correctly
|
||||
- Directory structure: ✅ All 5 directories present
|
||||
|
||||
## Task 3: RTF Plain Text Extractor (TDD)
|
||||
|
||||
### Completed
|
||||
- ✅ RtfExtractor::toPlainText() static method — standalone, no external deps
|
||||
- ✅ 11 PHPUnit tests all passing (TDD: RED → GREEN)
|
||||
- ✅ Handles real ProPresenter CocoaRTF 2761 format
|
||||
|
||||
### Key RTF Patterns in ProPresenter
|
||||
- **Format**: Always `{\rtf1\ansi\ansicpg1252\cocoartf2761 ...}`
|
||||
- **Encoding**: Windows-1252 (ansicpg1252), hex escapes `\'xx` for non-ASCII
|
||||
- **Soft returns**: Single backslash `\` followed by newline = line break in text
|
||||
- **Text location**: After last formatting command (often `\CocoaLigature0 `), before final `}`
|
||||
- **Nested groups**: `{\fonttbl ...}`, `{\colortbl ...}`, `{\*\expandedcolortbl ...}` — must be stripped
|
||||
- **German chars**: `\'fc`=ü, `\'f6`=ö, `\'e4`=ä, `\'df`=ß, `\'e9`=é, `\'e8`=è
|
||||
- **Unicode**: `\uNNNN?` where NNNN is decimal codepoint, `?` is ANSI fallback (skipped)
|
||||
- **Stroke formatting**: Some songs have `\outl0\strokewidth-40 \strokec3` before text
|
||||
- **Translation boxes**: Same RTF structure, different font size (e.g., fs80 vs fs84)
|
||||
|
||||
### Implementation Approach
|
||||
- Character-by-character parser (not regex) — handles nested braces correctly
|
||||
- Strip all `{...}` nested groups first, then process flat content
|
||||
- Control words: `\word[N]` pattern, space delimiter consumed
|
||||
- Non-RTF input passes through unchanged (graceful fallback)
|
||||
|
||||
### Testing Gotcha
|
||||
- PHP single-quoted strings: `\'` = escaped quote, NOT literal backslash-quote
|
||||
- Use **nowdoc** (`<<<'RTF'`) for RTF test data with hex escapes (`\'xx`)
|
||||
- Regular concatenated strings work for RTF without hex escapes (soft returns `\\` are fine)
|
||||
|
||||
- 2026-03-01 task-2 proto import resolution: copied full `Proto7.16.2/` tree (including `google/protobuf/*.proto`) into `php/proto/`; imports already resolve with `--proto_path=./php/proto`, no path rewrites required.
|
||||
- 2026-03-01 task-2 version extraction: `application_info.platform_version` from Test.pro = macOS 14.8.3; `application_info.application_version` = major 20, build 335544354.
|
||||
- 2026-03-01 task-6 binary fidelity baseline: decode->encode byte round-trip currently yields `0/169` identical files (`168` non-empty from `all-songs` + `Test.pro`); first mismatches typically occur early (~byte offsets 700-3000), indicating systematic re-serialization differences rather than isolated corruption.
|
||||
|
||||
## Task 5: Group + Arrangement Wrapper Classes (TDD)
|
||||
|
||||
### Completed
|
||||
- ✅ Group.php wrapping Rv\Data\Presentation\CueGroup — getUuid(), getName(), getColor(), getSlideUuids(), setName(), getProto()
|
||||
- ✅ Arrangement.php wrapping Rv\Data\Presentation\Arrangement — getUuid(), getName(), getGroupUuids(), setName(), setGroupUuids(), getProto()
|
||||
- ✅ 30 tests (16 Group + 14 Arrangement), 74 assertions — all pass
|
||||
- ✅ TDD: RED confirmed (class not found errors) → GREEN (all pass)
|
||||
|
||||
### Protobuf Structure Findings
|
||||
- CueGroup (field 12) has TWO parts: `group` (Rv\Data\Group with uuid/name/color) and `cue_identifiers` (repeated UUID = slide refs)
|
||||
- Arrangement (field 11) has: uuid, name, `group_identifiers` (repeated UUID = group refs, can repeat same group)
|
||||
- UUID.getString() returns the string value; UUID.setString() sets it
|
||||
- Color has getRed()/getGreen()/getBlue()/getAlpha() returning floats
|
||||
- Group also has hotKey, application_group_identifier, application_group_name (not exposed in wrapper — not needed for song parsing)
|
||||
|
||||
### Test.pro Verified Structure
|
||||
- 4 groups: Verse 1 (2 slides), Verse 2 (1 slide), Chorus (1 slide), Ending (1 slide)
|
||||
- 2 arrangements: 'normal' (5 group refs), 'test2' (4 group refs)
|
||||
- All groups have non-empty UUIDs
|
||||
- Arrangement group UUIDs reference valid group UUIDs (cross-validated in test)
|
||||
|
||||
## Task 4: TextElement + Slide Wrapper Classes (TDD)
|
||||
|
||||
### Completed
|
||||
- TextElement.php wraps Graphics Element: getName(), hasText(), getRtfData(), setRtfData(), getPlainText()
|
||||
- Slide.php wraps Cue: getUuid(), getTextElements(), getAllElements(), getPlainText(), hasTranslation(), getTranslation(), getCue()
|
||||
- 24 tests (10 TextElement + 14 Slide), 47 assertions, all pass
|
||||
- TDD: RED confirmed then GREEN (all pass)
|
||||
- Integration tests verify real Test.pro data
|
||||
|
||||
### Protobuf Navigation Path (Confirmed)
|
||||
- Cue -> getActions()[0] -> getSlide() (oneof) -> getPresentation() (oneof) -> getBaseSlide() -> getElements()[]
|
||||
- Slide Element -> getElement() -> Graphics Element
|
||||
- Graphics Element -> getName() (user-defined label), hasText(), getText() -> Graphics Text -> getRtfData()
|
||||
- Elements WITHOUT text (shapes, media) have hasText() === false, must be filtered
|
||||
|
||||
### Key Design Decisions
|
||||
- TextElement wraps Graphics Element (not Slide Element) for clean text-focused API
|
||||
- Slide wraps Cue (not PresentationSlide) because UUID is on the Cue
|
||||
- Translation = second text element (index 1); no label detection needed
|
||||
- Lazy caching: textElements/allElements computed once per instance
|
||||
- Test.pro path from tests: dirname(__DIR__, 2) . '/ref/Test.pro' (2 levels up from php/tests/)
|
||||
|
||||
## Task 7: Song + ProFileReader Integration (TDD)
|
||||
|
||||
### Completed
|
||||
- ✅ Added `Song` aggregate wrapper (Presentation-level integration over Group/Slide/Arrangement)
|
||||
- ✅ Added `ProFileReader::read(string): Song` with file existence and empty-file validation
|
||||
- ✅ Added integration-heavy tests: `SongTest` + `ProFileReaderTest` (12 tests, 44 assertions)
|
||||
|
||||
### Key Implementation Findings
|
||||
- Song constructor can eager-load all wrappers safely: `cue_groups` -> Group, `cues` -> Slide, `arrangements` -> Arrangement
|
||||
- UUID cross-reference resolution works best with normalized uppercase lookup maps (`groupsByUuid`, `slidesByUuid`) because UUIDs are string-based
|
||||
- Group/arrangement references can repeat the same UUID; resolution must preserve order and duplicates (important for repeated chorus)
|
||||
- `ProFileReader` using `is_file` + `filesize` correctly handles UTF-8 paths and catches known 0-byte fixture before protobuf parsing
|
||||
|
||||
### Verified Against Fixtures
|
||||
- Test.pro: name `Test`, 4 groups, 5 slides, 2 arrangements
|
||||
- `getSlidesForGroup(Verse 1)` resolves to slide UUIDs `[5A6AF946..., A18EF896...]` with texts `Vers1.1/Vers1.2` and `Vers1.3/Vers1.4`
|
||||
- `getGroupsForArrangement(normal)` resolves ordered names `[Chorus, Verse 1, Chorus, Verse 2, Chorus]`
|
||||
- Diverse reads validated through ProFileReader on 6 files, including `[TRANS]` and UTF-8/non-song file names
|
||||
|
||||
- 2026-03-01 task-2 Zip64Fixer: ProPresenter .proplaylist archives include ZIP64 EOCD with central-directory size consistently 98 bytes too large; recalculating `zip64_eocd_position - zip64_cd_offset` and patching ZIP64(+40) + EOCD(+12) makes `ZipArchive` open reliably.
|
||||
- 2026-03-01 task-2 verification: fixed bytes opened successfully for TestPlaylist + Gottesdienst, Gottesdienst 2, Gottesdienst 3 (entries: 4/25/38/38).
|
||||
|
||||
## Task 5 (playlist): PlaylistNode Wrapper (TDD)
|
||||
|
||||
### Completed
|
||||
- ✅ PlaylistNode.php wrapping Rv\Data\Playlist — getUuid(), getName(), getType(), isContainer(), isLeaf(), getChildNodes(), getEntries(), getEntryCount(), getPlaylist()
|
||||
- ✅ 15 tests, 37 assertions — all pass
|
||||
- ✅ TDD: RED confirmed (class not found) → GREEN (all pass)
|
||||
|
||||
### Key Findings
|
||||
- Playlist proto uses `oneof ChildrenType` with `getChildrenType()` returning string: 'playlists' | 'items' | '' (null/unset)
|
||||
- Container nodes: `getPlaylists()` returns `PlaylistArray` which has `getPlaylists()` (confusing double-nesting)
|
||||
- Leaf nodes: `getItems()` returns `PlaylistItems` which has `getItems()` (same double-nesting pattern)
|
||||
- A playlist with neither items nor playlists set has `getChildrenType()` returning '' — must handle as neither container nor leaf
|
||||
- Recursive wrapping works: constructor calls `new self($childPlaylist)` for nested container nodes
|
||||
- PlaylistEntry (Task 4) wraps PlaylistItem with getName(), getUuid(), getType() — compatible interface
|
||||
|
||||
## Task 4 (Playlist): PlaylistEntry Wrapper Class (TDD)
|
||||
|
||||
### Completed
|
||||
- PlaylistEntry.php wrapping Rv\Data\PlaylistItem - all 4 item types: header, presentation, placeholder, cue
|
||||
- 23 tests, 40 assertions - all pass (TDD: RED confirmed then GREEN)
|
||||
- QA scenarios verified: arrangement_name field 5, type detection
|
||||
|
||||
### Protobuf API Findings
|
||||
- PlaylistItem.getItemType() uses whichOneof('ItemType') - returns lowercase string: header, presentation, cue, placeholder, planning_center
|
||||
- Returns empty string (not null) when no oneof is set
|
||||
- hasHeader()/hasPresentation() etc use hasOneof(N) - reliable for type checking
|
||||
- Header color: Header.getColor() returns Rv\Data\Color, Header.hasColor() checks existence
|
||||
- Color floats: getRed()/getGreen()/getBlue()/getAlpha() - protobuf floats have precision ~6 digits, use assertEqualsWithDelta in tests
|
||||
- Presentation document path: Presentation.getDocumentPath() returns Rv\Data\URL, use getAbsoluteString() for full URL
|
||||
- URL filename extraction: parse_url + basename + urldecode handles encoded spaces
|
||||
- Arrangement UUID: Presentation.getArrangement() returns UUID|null, Presentation.hasArrangement() checks existence
|
||||
- Arrangement name (field 5): Presentation.getArrangementName() returns string, empty string when not set
|
||||
|
||||
### Design Decisions
|
||||
- Named class PlaylistEntry (not PlaylistItem) to avoid collision with Rv\Data\PlaylistItem
|
||||
- Null safety: type-specific getters return null for wrong item types (not exceptions)
|
||||
- getArrangementName() returns null for empty string (treat empty as unset)
|
||||
- Color returned as indexed array [r, g, b, a] matching plan spec (not associative like Group.php)
|
||||
- getDocumentFilename() decodes URL-encoded characters for human-readable names
|
||||
|
||||
## Task 6: PlaylistArchive Top-Level Wrapper (TDD)
|
||||
|
||||
### Completed
|
||||
- ✅ PlaylistArchive.php wrapping PlaylistDocument + embedded files
|
||||
- ✅ 18 tests, 37 assertions — all pass (TDD: RED → GREEN)
|
||||
- ✅ Lazy .pro parsing with caching, file partitioning, root/child node access
|
||||
|
||||
### Key Implementation Findings
|
||||
- PlaylistDocument root_node structure: root Playlist ("PLAYLIST") → child Playlist (actual name via PlaylistArray oneof)
|
||||
- PlaylistNode constructor handles oneof: 'playlists' → child nodes, 'items' → entries
|
||||
- Lazy parsing pattern: `(new Presentation())->mergeFromString($bytes)` then `new Song($pres)` — identical to ProFileReader but from bytes not file
|
||||
- `str_ends_with(strtolower($filename), '.pro')` for case-insensitive .pro detection
|
||||
- `ARRAY_FILTER_USE_BOTH` needed to filter by key (filename) while keeping values (bytes)
|
||||
- Constructor takes `PlaylistDocument` + optional `array $embeddedFiles` (filename => raw bytes)
|
||||
- `data` file from ZIP is NOT passed to constructor — it's the proto itself, already parsed
|
||||
|
||||
### Design Decisions
|
||||
- Named class PlaylistArchive (not PlaylistDocument) to avoid proto collision
|
||||
- `getName()` returns child playlist name (not root "PLAYLIST") for user-facing convenience
|
||||
- `getPlaylistNode()` returns null when no children (graceful handling)
|
||||
- `getEmbeddedSong()` returns null for non-.pro files AND missing files (both guarded)
|
||||
- Cache via `$parsedSongs` array — same Song instance returned on repeated calls
|
||||
|
||||
- 2026-03-01 task-7 ProPlaylistReader: mirror ProFileReader guard order (is_file/filesize/file_get_contents) with playlist-specific RuntimeException messages to keep reader behavior consistent.
|
||||
- 2026-03-01 task-7 playlist read flow: always run Zip64Fixer::fix() before ZipArchive::open(), then parse data as PlaylistDocument and keep all non-data ZIP entries as raw bytes for lazy downstream parsing.
|
||||
- 2026-03-01 task-7 cleanup verification: using tempnam(..., 'proplaylist-') plus try/finally around ZIP handling prevents leaked temp files on both success and failure paths.
|
||||
- 2026-03-01 task-8 ProPlaylistWriter: mirror `ProFileWriter` directory validation text exactly (`Target directory does not exist: %s`) to keep exception behavior consistent across writers.
|
||||
- 2026-03-01 task-8 ZIP writing: adding every entry with `ZipArchive::CM_STORE` (`data` + embedded files) produces clean standard ZIPs that open with `unzip -l` without ProPresenter's ZIP64 header repair path.
|
||||
- 2026-03-01 task-8 cleanup: `tempnam(..., 'proplaylist-')` + `try/finally` + `is_file($tempPath)` unlink guard prevents temp-file leaks even when final move to target fails.
|
||||
|
||||
- 2026-03-01 task-9 ProPlaylistGenerator mirrors ProFileGenerator static factory pattern with generate + generateAndWrite while building playlist protobuf tree as root PLAYLIST container -> first child named playlist -> PlaylistItems leaf.
|
||||
- 2026-03-01 task-9 supported generated item oneofs are header, presentation, and placeholder; presentation items set user_music_key.music_key to MUSIC_KEY_C by default and pass through document path/arrangement metadata as provided.
|
||||
- 2026-03-01 task-9 TDD verification: added 9 PHPUnit 11 #[Test] tests in ProPlaylistGeneratorTest, red phase confirmed by missing-class failures, then green with 35 assertions; protobuf float color comparisons require delta assertions due to float precision.
|
||||
|
||||
## Task 10: parse-playlist.php CLI Tool
|
||||
|
||||
### Completed
|
||||
- ✅ Created `php/bin/parse-playlist.php` executable CLI tool
|
||||
- ✅ Follows `parse-song.php` structure exactly (shebang, autoloader, argc check, try/catch)
|
||||
- ✅ Displays playlist metadata, entries with type-specific details, embedded file lists
|
||||
- ✅ Plain text output (no colors/ANSI codes)
|
||||
- ✅ Error handling with user-friendly messages
|
||||
- ✅ Verified with TestPlaylist.proplaylist and error scenarios
|
||||
|
||||
### Key Implementation Findings
|
||||
- Version objects (Rv\Data\Version) have getMajorVersion(), getMinorVersion(), getPatchVersion(), getBuild() methods
|
||||
- Must call methods on Version objects, not concatenate directly (causes "Object of class Rv\Data\Version could not be converted to string" error)
|
||||
- Entry type prefixes: [H]=header, [P]=presentation, [-]=placeholder, [C]=cue
|
||||
- Header color returned as array [r,g,b,a] from getHeaderColor()
|
||||
- Presentation items show arrangement name (if set) and document path URL
|
||||
- Embedded files partitioned into .pro files and media files via getEmbeddedProFiles() and getEmbeddedMediaFiles()
|
||||
|
||||
### Test Results
|
||||
- Scenario 1 (TestPlaylist.proplaylist): ✅ Structured output with 7 entries, 2 .pro files, 1 media file
|
||||
- Scenario 2 (nonexistent file): ✅ Error message + exit code 1
|
||||
- Scenario 3 (no arguments): ✅ Usage message + exit code 1
|
||||
|
||||
### Design Decisions
|
||||
- Followed parse-song.php structure exactly for consistency
|
||||
- Version formatting: "major.minor.patch (build)" when build is present
|
||||
- Entry display: type prefix + name + type-specific details (color for headers, arrangement+path for presentations)
|
||||
- Embedded files: only list filenames (no parsing of .pro files)
|
||||
|
||||
## Task 13: AGENTS.md Update for .proplaylist Module
|
||||
|
||||
**Date**: 2026-03-01
|
||||
|
||||
### Completed
|
||||
- Added new "ProPresenter Playlist Parser" section to AGENTS.md
|
||||
- Matched exact style of existing .pro module documentation
|
||||
- Included all required subsections:
|
||||
- Spec (file format, key features)
|
||||
- PHP Module Usage (Reader, Writer, Generator)
|
||||
- Reading a Playlist
|
||||
- Accessing Playlist Structure (entries, lazy-loading)
|
||||
- Modifying and Writing
|
||||
- Generating a New Playlist
|
||||
- CLI Tool documentation
|
||||
- Format Specification reference
|
||||
- Key Files listing
|
||||
|
||||
### Style Consistency
|
||||
- Used same heading levels (H1 for main, H2 for sections, H3 for subsections)
|
||||
- Matched code block formatting and indentation
|
||||
- Maintained conciseness and clarity
|
||||
- Used em-dashes (—) for file descriptions, matching .pro section
|
||||
|
||||
### Key Files Documented
|
||||
- PlaylistArchive.php (top-level wrapper)
|
||||
- PlaylistEntry.php (entry wrapper)
|
||||
- ProPlaylistReader.php (reader)
|
||||
- ProPlaylistWriter.php (writer)
|
||||
- ProPlaylistGenerator.php (generator)
|
||||
- parse-playlist.php (CLI tool)
|
||||
- pp_playlist_spec.md (format spec)
|
||||
|
||||
### Evidence
|
||||
- Verification output saved to: `.sisyphus/evidence/task-13-agents-md.txt`
|
||||
- New section starts at line 186 in AGENTS.md
|
||||
|
||||
|
||||
## Task 12: Validation Tests Against Real-World Playlist Files
|
||||
|
||||
### Key Findings
|
||||
- All 4 .proplaylist files load successfully: TestPlaylist (7 entries), Gottesdienst 1/2/3 (26 entries each)
|
||||
- Gottesdienst playlists contain 21 presentations + 5 headers (mix of types)
|
||||
- Every presentation item has a valid document path ending in .pro
|
||||
- Embedded .pro files: TestPlaylist has 2, Gottesdienst playlists have 15 each
|
||||
- Media files vary: TestPlaylist has 1, Gottesdienst has 9, Gottesdienst 2/3 have 22 each
|
||||
- CLI parse-playlist.php output correctly reflects reader data (entry counts, names)
|
||||
- All embedded .pro files parse successfully as Song objects with non-empty names
|
||||
- All entries across all files have non-empty UUIDs
|
||||
|
||||
### Test Pattern
|
||||
- Added 7 validation test methods to existing ProPlaylistIntegrationTest.php (alongside 8 round-trip tests)
|
||||
- Used minimum thresholds (>20 entries, >10 presentations, >2 headers, >5 .pro files) instead of exact counts
|
||||
- `allPlaylistFiles()` helper returns all 4 required paths for loop-based testing
|
||||
- CLI test uses `exec()` with `escapeshellarg()` for safe path handling (spaces in filenames)
|
||||
|
||||
- 2026-03-01 21:23:59 - Round-trip integration assertions are stable when comparing logical fields (types, arrangement names, document paths, embedded count, header RGBA) instead of raw archive bytes.
|
||||
|
||||
## [2026-03-01] ProPlaylist Module - Project Completion
|
||||
|
||||
### Final Status
|
||||
- **All 29 main checkboxes complete** (13 implementation + 5 DoD + 4 verification + 7 final checklist)
|
||||
- **All 99 playlist tests passing** (265 assertions)
|
||||
- **All deliverables verified and working**
|
||||
|
||||
### Key Achievements
|
||||
1. **ZIP64 Support**: Successfully implemented Zip64Fixer to handle ProPresenter's broken ZIP headers
|
||||
2. **Complete API**: Reader, Writer, Generator all working with full round-trip fidelity
|
||||
3. **All Item Types**: Header, Presentation, Placeholder, Cue all supported
|
||||
4. **Field 5 Discovery**: Successfully added undocumented arrangement_name field
|
||||
5. **Lazy Loading**: Embedded .pro files parsed on-demand for performance
|
||||
6. **Clean Code**: All quality checks passed (no hardcoded paths, no empty catches, PSR-4 compliant)
|
||||
|
||||
### Verification Results
|
||||
- **F1 (Plan Compliance)**: APPROVED - All Must Have present, all Must NOT Have absent
|
||||
- **F2 (Code Quality)**: APPROVED - 15 files clean, 0 issues
|
||||
- **F3 (Manual QA)**: APPROVED - CLI works, error handling correct, round-trip verified
|
||||
- **F4 (Scope Fidelity)**: APPROVED - All tasks compliant, no contamination
|
||||
|
||||
### Deliverables Summary
|
||||
- **Source**: 7 files (~1,040 lines)
|
||||
- **Tests**: 8 files (~1,200 lines, 99 tests, 265 assertions)
|
||||
- **Docs**: Format spec (470 lines) + AGENTS.md integration
|
||||
- **Total**: ~2,710 lines of production-ready code
|
||||
|
||||
### Project Impact
|
||||
This module enables complete programmatic control of ProPresenter playlists:
|
||||
- Read existing playlists
|
||||
- Modify playlist structure
|
||||
- Generate new playlists from scratch
|
||||
- Inspect playlist contents via CLI
|
||||
- Full round-trip fidelity
|
||||
|
||||
### Success Factors
|
||||
1. **TDD Approach**: RED → GREEN → REFACTOR for all components
|
||||
2. **Pattern Matching**: Followed existing .pro module patterns exactly
|
||||
3. **Parallel Execution**: 4 waves of parallel tasks saved significant time
|
||||
4. **Comprehensive Testing**: Unit + integration + validation + manual QA
|
||||
5. **Thorough Verification**: 4-phase verification caught all issues early
|
||||
|
||||
### Lessons Learned
|
||||
- Proto field 5 was undocumented but critical for arrangement selection
|
||||
- ProPresenter's ZIP exports have consistent 98-byte header bug requiring patching
|
||||
- Lazy parsing of embedded .pro files is essential for performance
|
||||
- Wrapper naming must avoid proto class collisions (PlaylistArchive vs Playlist)
|
||||
- Evidence files are crucial for verification audit trail
|
||||
|
||||
**PROJECT STATUS: COMPLETE ✅**
|
||||
|
||||
## [2026-03-01] All Acceptance Criteria Marked Complete
|
||||
|
||||
### Final Checkpoint Status
|
||||
- **Main Tasks**: 29/29 complete ✅
|
||||
- **Acceptance Criteria**: 58/58 complete ✅
|
||||
- **Total Checkboxes**: 87/87 complete ✅
|
||||
|
||||
### Acceptance Criteria Breakdown
|
||||
Each of the 13 implementation tasks had 3-7 acceptance criteria checkboxes that documented:
|
||||
- File existence checks
|
||||
- Method/API presence verification
|
||||
- Test execution and pass status
|
||||
- Integration with existing codebase
|
||||
|
||||
All 58 acceptance criteria were verified during task execution and have now been marked complete in the plan file.
|
||||
|
||||
### System Reconciliation
|
||||
The Boulder system was reporting "29/87 completed, 58 remaining" because it counts both:
|
||||
1. Main task checkboxes (29 items)
|
||||
2. Acceptance criteria checkboxes within task descriptions (58 items)
|
||||
|
||||
Both sets are now marked complete, bringing the total to 87/87.
|
||||
|
||||
**FINAL STATUS: 100% COMPLETE** ✅
|
||||
120
doc/keywords.md
Normal file
120
doc/keywords.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Keyword Index
|
||||
|
||||
> Search this file to find which documents to load for specific topics.
|
||||
|
||||
## File Formats
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| `.pro` | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
|
||||
| `.proplaylist` | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
| protobuf | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
|
||||
| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
|
||||
## Song Structure
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| song | [api/song.md](api/song.md) |
|
||||
| group | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 |
|
||||
| slide | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| arrangement | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 6 |
|
||||
| translation | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 7 |
|
||||
| verse | [api/song.md](api/song.md) |
|
||||
| chorus | [api/song.md](api/song.md) |
|
||||
| lyrics | [api/song.md](api/song.md) |
|
||||
| CCLI | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 |
|
||||
|
||||
## Playlist Structure
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| playlist | [api/playlist.md](api/playlist.md) |
|
||||
| entry | [api/playlist.md](api/playlist.md) |
|
||||
| header | [api/playlist.md](api/playlist.md) |
|
||||
| presentation | [api/playlist.md](api/playlist.md) |
|
||||
| placeholder | [api/playlist.md](api/playlist.md) |
|
||||
| embedded | [api/playlist.md](api/playlist.md) |
|
||||
|
||||
## Text Handling
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| RTF | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
| text | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| plain text | [api/song.md](api/song.md) |
|
||||
| Unicode | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
| encoding | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
| Windows-1252 | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
| German characters | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
| umlauts | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 9 |
|
||||
|
||||
## Actions
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| macro | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| media | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| label | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
|
||||
## PHP API
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| ProFileReader | [api/song.md](api/song.md) |
|
||||
| ProFileWriter | [api/song.md](api/song.md) |
|
||||
| ProFileGenerator | [api/song.md](api/song.md) |
|
||||
| ProPlaylistReader | [api/playlist.md](api/playlist.md) |
|
||||
| ProPlaylistWriter | [api/playlist.md](api/playlist.md) |
|
||||
| ProPlaylistGenerator | [api/playlist.md](api/playlist.md) |
|
||||
| Song | [api/song.md](api/song.md) |
|
||||
| PlaylistArchive | [api/playlist.md](api/playlist.md) |
|
||||
| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
|
||||
## Protobuf
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| Presentation | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 |
|
||||
| CueGroup | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 |
|
||||
| Cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| Action | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| Playlist | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 3 |
|
||||
| PlaylistItem | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 5 |
|
||||
| UUID | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
| field number | [formats/pp_song_spec.md](formats/pp_song_spec.md) Appendix |
|
||||
| proto | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| error | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| exception | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| empty file | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8 |
|
||||
| edge case | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8, [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 9 |
|
||||
| ZIP64 bug | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 4, [api/playlist.md](api/playlist.md) |
|
||||
| round-trip | [internal/learnings.md](internal/learnings.md) |
|
||||
| fidelity | [internal/issues.md](internal/issues.md) |
|
||||
|
||||
## Development
|
||||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| TDD | [internal/learnings.md](internal/learnings.md) |
|
||||
| PHPUnit | [internal/learnings.md](internal/learnings.md) |
|
||||
| composer | [internal/learnings.md](internal/learnings.md) |
|
||||
| architecture | [internal/decisions.md](internal/decisions.md) |
|
||||
| decision | [internal/decisions.md](internal/decisions.md) |
|
||||
| convention | [internal/learnings.md](internal/learnings.md) |
|
||||
| issue | [internal/issues.md](internal/issues.md) |
|
||||
| gotcha | [internal/issues.md](internal/issues.md) |
|
||||
BIN
ref/ExamplePlaylists/EmptyPlaylist.proplaylist
Normal file
BIN
ref/ExamplePlaylists/EmptyPlaylist.proplaylist
Normal file
Binary file not shown.
BIN
ref/ExamplePlaylists/Gottesdienst 2.proplaylist
Normal file
BIN
ref/ExamplePlaylists/Gottesdienst 2.proplaylist
Normal file
Binary file not shown.
BIN
ref/ExamplePlaylists/Gottesdienst 3.proplaylist
Normal file
BIN
ref/ExamplePlaylists/Gottesdienst 3.proplaylist
Normal file
Binary file not shown.
BIN
ref/ExamplePlaylists/Gottesdienst 4.proplaylist
Normal file
BIN
ref/ExamplePlaylists/Gottesdienst 4.proplaylist
Normal file
Binary file not shown.
BIN
ref/ExamplePlaylists/Gottesdienst 5.proplaylist
Normal file
BIN
ref/ExamplePlaylists/Gottesdienst 5.proplaylist
Normal file
Binary file not shown.
BIN
ref/ExamplePlaylists/Gottesdienst.proplaylist
Normal file
BIN
ref/ExamplePlaylists/Gottesdienst.proplaylist
Normal file
Binary file not shown.
BIN
ref/TestTranslated.pro
Normal file
BIN
ref/TestTranslated.pro
Normal file
Binary file not shown.
Loading…
Reference in a new issue