Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ modules/*
!modules/NowPlaying
!modules/Logger
!modules/Tutorial
!modules/SampleData
.*.swp
.*.swo
.well-known
Expand Down
39 changes: 39 additions & 0 deletions modules/SampleData/SampleData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

// Copyright 2012-2026 OpenBroadcaster, Inc.
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OpenBroadcaster\Modules\SampleData;

use OpenBroadcaster\Base\Module;

class SampleData extends Module
{
public $name = 'SampleData v1.0';
public $description = 'Seed sample data profiles into a fresh Observer instance.';

public function callbacks()
{
}

public function install()
{
$this->permission_enable('administration', 'import_sample_data', 'import sample data profiles into Observer');

return true;
}

public function uninstall()
{
$this->permission_disable('import_sample_data');

return true;
}

public function purge()
{
$this->permission_delete('import_sample_data');

return true;
}
}
57 changes: 57 additions & 0 deletions modules/SampleData/controllers/SampleData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

// Copyright 2012-2026 OpenBroadcaster, Inc.
// SPDX-License-Identifier: AGPL-3.0-or-later

/**
* Sample data import module.
*
* @package Controller.SampleData
*/
namespace OpenBroadcaster\Modules\SampleData\Controllers;

use OpenBroadcaster\Base\Controller;

class SampleData extends Controller
{
public function __construct()
{
parent::__construct();
$this->user->require_permission('import_sample_data');
$this->SampleDataModel = $this->load->model('SampleData', 'SampleData');
}

/**
* @route GET /profiles
*/
public function listProfiles()
{
return [true, 'Sample data profiles.', $this->SampleDataModel('listProfiles')];
}

/**
* The envelope is always success=true at the API level; the real outcome
* (success/log/error) is in the data payload so OB.API.request (which
* returns only data) can read it.
*
* @route POST /run
*/
public function runProfile()
{
$profile = trim((string) $this->data('profile'));

// Fallback while MODULES_5.5.md "v2 API integration for JS with modules"
// TODO is open: under X-Auth session auth, api.php skips the JSON body
// parse, so $this->data() returns nothing. Read php://input ourselves.
if ($profile === '') {
$body = json_decode(file_get_contents('php://input'), true);
$profile = is_array($body) ? trim((string) ($body['profile'] ?? '')) : '';
}

if ($profile === '' || !preg_match('/^[a-z0-9_]+$/', $profile)) {
return [true, 'Invalid profile name.', ['success' => false, 'log' => [], 'error' => 'Invalid profile name.']];
}

return [true, 'Sample data import attempted.', $this->SampleDataModel('runProfile', $profile)];
}
}
23 changes: 23 additions & 0 deletions modules/SampleData/html/sampledata.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--/*
Copyright 2012-2026 OpenBroadcaster, Inc.
SPDX-License-Identifier: AGPL-3.0-or-later
*/-->

<h1>Import Sample Data</h1>

<p id="sampledata_module-info">Pick a sample data profile and click "Import" to seed your installation. The action is idempotent — items that already exist are skipped, never overwritten.</p>

<table class="table">
<tr>
<td><label for="sampledata_module-profile">Profile</label></td>
<td><select id="sampledata_module-profile"></select></td>
</tr>
<tr>
<td colspan="2"><p id="sampledata_module-description"></p></td>
</tr>
<tr>
<td colspan="2"><button id="sampledata_module-import" class="button">Import Selected Profile</button></td>
</tr>
</table>

<pre id="sampledata_module-log" style="display: none; white-space: pre-wrap; background: #111; color: #eee; padding: 1em; border-radius: 4px; margin-top: 1em;"></pre>
76 changes: 76 additions & 0 deletions modules/SampleData/js/sampledata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2012-2026 OpenBroadcaster, Inc.
// SPDX-License-Identifier: AGPL-3.0-or-later

OBModules.SampleData = new Object();

OBModules.SampleData.init = function () {
OB.Callbacks.add('ready', 0, OBModules.SampleData.initMenu);
};

OBModules.SampleData.initMenu = function () {
OB.UI.addSubMenuItem('admin', 'Import Sample Data', 'import_sample_data', OBModules.SampleData.importPage, 110, 'import_sample_data');
};

OBModules.SampleData.importPage = async function () {
// Cache key preserves the module dir's actual case (core/controllers/UI.php
// find_module_html_files) — must be 'SampleData', not 'sampledata'.
OB.UI.replaceMain('modules/SampleData/sampledata.html');

OBModules.SampleData.profiles = {};
$('#sampledata_module-profile').html('<option value="">Loading…</option>');
$('#sampledata_module-import').prop('disabled', true);
$('#sampledata_module-log').hide().text('');

const profiles = await OB.API.request({ endpoint: 'module/SampleData/profiles', method: 'GET' });
var $select = $('#sampledata_module-profile').empty();

if (!profiles || profiles.length === 0) {
$select.append('<option value="">No profiles found</option>');
$('#sampledata_module-info').text('No sample data profiles found.');
return;
}

$.each(profiles, function (_, profile) {
OBModules.SampleData.profiles[profile.directory] = profile;
$select.append($('<option></option>').val(profile.directory).text(profile.name));
});

OBModules.SampleData.updateDescription();
$('#sampledata_module-import').prop('disabled', false);

$('#sampledata_module-profile').off('change').on('change', OBModules.SampleData.updateDescription);
$('#sampledata_module-import').off('click').on('click', function () { OBModules.SampleData.runImport(false); });
};

OBModules.SampleData.updateDescription = function () {
var profile = OBModules.SampleData.profiles[$('#sampledata_module-profile').val()];
$('#sampledata_module-description').text(profile ? (profile.description || '') : '');
};

OBModules.SampleData.runImport = function (confirmed) {
var dir = $('#sampledata_module-profile').val();
if (!dir) return;

if (!confirmed) {
OB.UI.confirm(
'Import sample data profile "' + dir + '"?\n\nThis will create permission groups, users, playlists, a sample player and schedule, and apply settings. The action is idempotent — existing items are skipped — but it cannot be undone.',
function () { OBModules.SampleData.runImport(true); },
'Yes, Import', 'No, Cancel', 'delete'
);
return;
}

$('#sampledata_module-info').text('Importing… (first run downloads ~73MB of media; this can take a minute)');
$('#sampledata_module-import').prop('disabled', true);
$('#sampledata_module-log').show().text('');

OB.API.request({ endpoint: 'module/SampleData/run', method: 'POST', data: { profile: dir } }).then(function (result) {
$('#sampledata_module-import').prop('disabled', false);
if (!result) {
$('#sampledata_module-info').text('Import failed: no response from server.');
return;
}
$('#sampledata_module-log').text((result.log || []).join('\n'));
$('#sampledata_module-info').text(result.success ? 'Sample data imported.' : 'Import failed: ' + (result.error || 'unknown error'));
});
};
Loading
Loading