propresenter-php/AGENTS.md
Thorsten Bus 813d30dd12 test(playlist): add integration tests and update AGENTS.md
- Add ProPlaylistIntegrationTest with 8 round-trip tests
- All 4 .proplaylist test files validated in ProPlaylistReaderTest
- Update AGENTS.md with playlist module documentation
- Document reading, writing, generating, CLI usage
- Add notepad learnings from Wave 4 tasks
2026-03-01 21:28:18 +01:00

9.5 KiB

Analyze a file format of a song.

Spec

File: ./Test.pro (file ext are always .pro)

  • 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

Status

  1. Analyse the file structure and find all of the described specs.
  2. Test and verify if the definition is correct - there is a all-songs directory with lot of examples.
  3. 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. 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. Create a simple PHP cli tool, which receive a param with a song file and show the structure of the song.

PHP Module Usage

The ProPresenter song parser is available as a PHP module in ./php. Use it to read, parse, and modify .pro song files.

Reading a Song

use ProPresenter\Parser\ProFileReader;
use ProPresenter\Parser\ProFileWriter;

$song = ProFileReader::read('path/to/song.pro');

Accessing Song Structure

// Basic song info
echo $song->getName();        // Song name
echo $song->getUuid();        // Song UUID

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

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

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

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

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

use ProPresenter\Parser\ProPlaylistReader;
use ProPresenter\Parser\ProPlaylistWriter;

$archive = ProPlaylistReader::read('path/to/playlist.proplaylist');

Accessing Playlist Structure

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

$archive->setName("New Playlist Name");
$archive->setNotes("Updated notes");
ProPlaylistWriter::write($archive, 'output.proplaylist');

Generating a New Playlist

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:

php php/bin/parse-playlist.php path/to/playlist.proplaylist

Format Specification

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