diff --git a/AGENTS.md b/AGENTS.md index 5bf3af8..a8fcacb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,9 +56,28 @@ foreach ($song->getGroups() as $group) { $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(); + } } } @@ -90,6 +109,11 @@ $song = ProFileGenerator::generate( ['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'], @@ -105,6 +129,32 @@ $song = ProFileGenerator::generate( 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: diff --git a/php/bin/parse-song.php b/php/bin/parse-song.php index 175eed1..dee6aa8 100755 --- a/php/bin/parse-song.php +++ b/php/bin/parse-song.php @@ -116,6 +116,21 @@ foreach ($groups as $index => $group) { } } } + + $label = $slide->getLabel(); + if ($label !== '') { + echo " Label: " . $label . "\n"; + } + + if ($slide->hasMacro()) { + echo " Macro: " . ($slide->getMacroName() ?? '') . " (" . ($slide->getMacroUuid() ?? '') . ")\n"; + } + + if ($slide->hasMedia()) { + $format = $slide->getMediaFormat(); + $formatSuffix = $format !== null && $format !== '' ? ' [' . $format . ']' : ''; + echo " Media: " . ($slide->getMediaUrl() ?? '') . $formatSuffix . "\n"; + } } } diff --git a/spec/pp_song_spec.md b/spec/pp_song_spec.md index 3fdb6f5..6861a48 100644 --- a/spec/pp_song_spec.md +++ b/spec/pp_song_spec.md @@ -41,15 +41,22 @@ Presentation (rv.data.Presentation) │ └── cue_identifiers[] (rv.data.UUID, field 2) ← Slide UUID references ├── cues[] (rv.data.Cue, field 13) ← Slides │ ├── uuid (rv.data.UUID, field 1) -│ └── actions[0] (rv.data.Action, field 10) -│ └── slide (rv.data.Action.SlideType, field 23) -│ └── presentation (rv.data.PresentationSlide, field 2) -│ └── base_slide (rv.data.Slide, field 1) -│ └── elements[] (rv.data.Slide.Element, field 1) -│ └── element (rv.data.Graphics.Element, field 1) -│ ├── name (string, field 2) ← Label like "Orginal", "Deutsch" -│ └── text (rv.data.Graphics.Text, field 13) -│ └── rtf_data (bytes, field 3) ← RTF-encoded text +│ ├── name (string, field 2) ← Optional slide label/title +│ └── actions[] (rv.data.Action, field 10) +│ ├── actions[0] slide (type=11) +│ │ └── slide (rv.data.Action.SlideType, field 23) +│ │ └── presentation (rv.data.PresentationSlide, field 2) +│ │ └── base_slide (rv.data.Slide, field 1) +│ │ └── elements[] (rv.data.Slide.Element, field 1) +│ │ └── element (rv.data.Graphics.Element, field 1) +│ │ ├── name (string, field 2) ← Label like "Orginal", "Deutsch" +│ │ └── text (rv.data.Graphics.Text, field 13) +│ │ └── rtf_data (bytes, field 3) ← RTF-encoded text +│ ├── actions[n] media (type=2) [optional] +│ │ └── media (rv.data.Action.MediaType, field 20) +│ │ └── element (rv.data.Media, field 5) +│ └── actions[n] macro (type=23) [optional] +│ └── macro (rv.data.Action.MacroType, field 40) └── arrangements[] (rv.data.Presentation.Arrangement, field 11) ├── name (string, field 2) ├── uuid (rv.data.UUID, field 1) @@ -154,13 +161,17 @@ CCLI (Christian Copyright Licensing International) metadata. Present in 157 out | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| | `uuid` | `rv.data.UUID` | 1 | Unique identifier for the slide | -| `actions[]` | `rv.data.Action` | 10 | Array of actions (slides use `actions[0]`) | +| `name` | `string` | 2 | Optional slide label/title shown in UI | +| `actions[]` | `rv.data.Action` | 10 | Array of actions (slide action at index 0, optional media/macro actions after it) | ### Action (rv.data.Action) | Field Path | Protobuf Type | Field Number | Description | |------------|---------------|--------------|-------------| +| `type` | `rv.data.Action.ActionType` | 9 | Action type enum (`11` slide, `2` media, `23` macro) | | `slide` | `rv.data.Action.SlideType` | 23 | Slide data (oneof field) | +| `media` | `rv.data.Action.MediaType` | 20 | Media/image action payload (oneof field) | +| `macro` | `rv.data.Action.MacroType` | 40 | Macro action payload (oneof field) | ### Action.SlideType @@ -253,6 +264,26 @@ Cue → actions[0] → slide → presentation → base_slide → elements[] ### Slides Without Text Some slides contain only media (images, videos) or shapes. These slides have `elements[]` with no `text` field set. +### Slide Labels (Cue.name) +- **Location:** `Cue.name` +- **Meaning:** Optional title/label for a slide in ProPresenter UI. +- **Example:** `Seniorennachmittag März.jpg` + +### Media/Image Actions +- **Detection:** Any cue action where `Action.type == ACTION_TYPE_MEDIA (2)`. +- **Path:** `Cue.actions[n].media.element` +- **URL:** `media.element.url.absolute_string` (typically `file:///...`) +- **Format:** `media.element.metadata.format` (e.g., `JPG`) +- **Image Marker:** `media.element.image` oneof is set (`ImageTypeProperties`) +- **Text Relation:** Image slides often still include `actions[0]` slide action with an empty `base_slide.elements[]`. + +### Macro Actions +- **Detection:** Any cue action where `Action.type == ACTION_TYPE_MACRO (23)`. +- **Path:** `Cue.actions[n].macro.identification` +- **Macro Name/UUID:** `identification.parameter_name`, `identification.parameter_uuid.string` +- **Collection Name/UUID:** `identification.parent_collection.parameter_name`, `identification.parent_collection.parameter_uuid.string` +- **Observed Collection:** `--MAIN--` with UUID `8D02FC57-83F8-4042-9B90-81C229728426` in sample files. + ### UUID References Groups reference slides by UUID. Use `Cue.uuid` to match slides to group references.