Skip to content

Commit 69a833e

Browse files
add SampleData module for in-UI sample data import
Replaces the earlier core/cli and tools/ iterations. SampleData is a proper 5.5 module: controller declares @route DocBlocks for v2 (GET /profiles, POST /run), model talks to $this->db directly, JS uses OB.API.request, admin menu entry gated by import_sample_data permission. Idempotent: every step checks existence before insert. Settings are overwrite-only (formats, core metadata flags, login message, welcome page) matching how the API works. Media library is seeded too: the seedMedia step downloads OB's official EN_Sample_Media_Ver_1.0.zip (~73MB), caches it under media_data/cache/sampledata/, extracts once, and imports each file with a SHA1-hash-based duplicate check. Files land in OB_MEDIA via the standard rand_file_location convention; default category is resolved by type from media.json. Includes a temporary php://input fallback in runProfile() to read the JSON body until the "v2 API integration for JS with modules" TODO in MODULES_5.5.md is closed -- api.php currently only parses v2 bodies under HTTP_AUTHORIZATION, not under X-Auth session headers. Fallback becomes dead code once that gate is widened.
1 parent 396c18c commit 69a833e

17 files changed

Lines changed: 1572 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ modules/*
99
!modules/NowPlaying
1010
!modules/Logger
1111
!modules/Tutorial
12+
!modules/SampleData
1213
.*.swp
1314
.*.swo
1415
.well-known

modules/SampleData/SampleData.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
// Copyright 2012-2026 OpenBroadcaster, Inc.
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
namespace OpenBroadcaster\Modules\SampleData;
7+
8+
use OpenBroadcaster\Base\Module;
9+
10+
class SampleData extends Module
11+
{
12+
public $name = 'SampleData v1.0';
13+
public $description = 'Seed sample data profiles into a fresh Observer instance.';
14+
15+
public function callbacks()
16+
{
17+
}
18+
19+
public function install()
20+
{
21+
$this->permission_enable('administration', 'import_sample_data', 'import sample data profiles into Observer');
22+
23+
return true;
24+
}
25+
26+
public function uninstall()
27+
{
28+
$this->permission_disable('import_sample_data');
29+
30+
return true;
31+
}
32+
33+
public function purge()
34+
{
35+
$this->permission_delete('import_sample_data');
36+
37+
return true;
38+
}
39+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
// Copyright 2012-2026 OpenBroadcaster, Inc.
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
/**
7+
* Sample data import module.
8+
*
9+
* @package Controller.SampleData
10+
*/
11+
namespace OpenBroadcaster\Modules\SampleData\Controllers;
12+
13+
use OpenBroadcaster\Base\Controller;
14+
15+
class SampleData extends Controller
16+
{
17+
public function __construct()
18+
{
19+
parent::__construct();
20+
21+
$this->user->require_permission('import_sample_data');
22+
$this->SampleDataModel = $this->load->model('SampleData', 'SampleData');
23+
}
24+
25+
/**
26+
* List the sample data profiles available to import.
27+
*
28+
* @return profiles
29+
*
30+
* @route GET /profiles
31+
*/
32+
public function listProfiles()
33+
{
34+
$profiles = $this->SampleDataModel('listProfiles');
35+
return [true, 'Sample data profiles.', $profiles];
36+
}
37+
38+
/**
39+
* Run a sample data profile by name. The result envelope is always
40+
* successful at the API level; the real outcome (success/log/error) is in
41+
* the data payload so the frontend can read it through OB.API.request,
42+
* which only returns the data field.
43+
*
44+
* @param profile
45+
* @return [success, log, error]
46+
*
47+
* @route POST /run
48+
*/
49+
public function runProfile()
50+
{
51+
$profile = trim((string) $this->data('profile'));
52+
53+
// Fallback while the MODULES_5.5.md "v2 API integration for JS with
54+
// modules" TODO is open: under X-Auth session auth, api.php skips the
55+
// JSON body parse (it only triggers for HTTP_AUTHORIZATION/appkey), so
56+
// $this->data() returns nothing. Read php://input ourselves. When the
57+
// core gate is widened to session auth this block becomes dead code
58+
// and can be removed.
59+
if ($profile === '') {
60+
$body = json_decode(file_get_contents('php://input'), true);
61+
if (is_array($body) && isset($body['profile'])) {
62+
$profile = trim((string) $body['profile']);
63+
}
64+
}
65+
66+
if ($profile === '' || !preg_match('/^[a-z0-9_]+$/', $profile)) {
67+
return [true, 'Invalid profile name.', [
68+
'success' => false,
69+
'log' => [],
70+
'error' => 'Invalid profile name.',
71+
]];
72+
}
73+
74+
$result = $this->SampleDataModel('runProfile', $profile);
75+
76+
return [true, 'Sample data import attempted.', $result];
77+
}
78+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!--/*
2+
Copyright 2012-2026 OpenBroadcaster, Inc.
3+
SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/-->
5+
6+
<h1>Import Sample Data</h1>
7+
8+
<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>
9+
10+
<table class="table">
11+
<tr>
12+
<td><label for="sampledata_module-profile">Profile</label></td>
13+
<td><select id="sampledata_module-profile"></select></td>
14+
</tr>
15+
<tr>
16+
<td colspan="2"><p id="sampledata_module-description"></p></td>
17+
</tr>
18+
<tr>
19+
<td colspan="2"><button id="sampledata_module-import" class="button">Import Selected Profile</button></td>
20+
</tr>
21+
</table>
22+
23+
<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>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2012-2026 OpenBroadcaster, Inc.
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
OBModules.SampleData = new Object();
5+
6+
OBModules.SampleData.init = function () {
7+
OB.Callbacks.add('ready', 0, OBModules.SampleData.initMenu);
8+
};
9+
10+
OBModules.SampleData.initMenu = function () {
11+
OB.UI.addSubMenuItem(
12+
'admin',
13+
'Import Sample Data',
14+
'import_sample_data',
15+
OBModules.SampleData.importPage,
16+
110,
17+
'import_sample_data'
18+
);
19+
};
20+
21+
OBModules.SampleData.importPage = async function () {
22+
// Cache key preserves the module directory's actual case (see core/controllers/UI.php
23+
// find_module_html_files), so this must be 'SampleData', not 'sampledata'.
24+
OB.UI.replaceMain('modules/SampleData/sampledata.html');
25+
26+
OBModules.SampleData.profiles = {};
27+
$('#sampledata_module-profile').html('<option value="">Loading…</option>');
28+
$('#sampledata_module-import').prop('disabled', true);
29+
$('#sampledata_module-log').hide().text('');
30+
31+
// v2 module endpoint: /api/v2/module/SampleData/profiles
32+
const profiles = await OB.API.request({
33+
endpoint: 'module/SampleData/profiles',
34+
method: 'GET',
35+
});
36+
37+
var $select = $('#sampledata_module-profile');
38+
$select.empty();
39+
40+
if (!profiles || profiles.length === 0) {
41+
$select.append('<option value="">No profiles found</option>');
42+
$('#sampledata_module-info').text('No sample data profiles found.');
43+
$('#sampledata_module-import').prop('disabled', true);
44+
return;
45+
}
46+
47+
$.each(profiles, function (_, profile) {
48+
OBModules.SampleData.profiles[profile.directory] = profile;
49+
$select.append($('<option></option>').val(profile.directory).text(profile.name));
50+
});
51+
52+
OBModules.SampleData.updateDescription();
53+
$('#sampledata_module-import').prop('disabled', false);
54+
55+
$('#sampledata_module-profile').off('change').on('change', OBModules.SampleData.updateDescription);
56+
$('#sampledata_module-import').off('click').on('click', function () {
57+
OBModules.SampleData.runImport(false);
58+
});
59+
};
60+
61+
OBModules.SampleData.updateDescription = function () {
62+
var dir = $('#sampledata_module-profile').val();
63+
var profile = OBModules.SampleData.profiles[dir];
64+
$('#sampledata_module-description').text(profile ? (profile.description || '') : '');
65+
};
66+
67+
OBModules.SampleData.runImport = function (confirmed) {
68+
var dir = $('#sampledata_module-profile').val();
69+
if (!dir) {
70+
return;
71+
}
72+
73+
if (!confirmed) {
74+
OB.UI.confirm(
75+
'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.',
76+
function () { OBModules.SampleData.runImport(true); },
77+
'Yes, Import',
78+
'No, Cancel',
79+
'delete'
80+
);
81+
return;
82+
}
83+
84+
$('#sampledata_module-info').text('Importing… (first run downloads ~73MB of media; this can take a minute)');
85+
$('#sampledata_module-import').prop('disabled', true);
86+
$('#sampledata_module-log').show().text('');
87+
88+
// v2 module endpoint: /api/v2/module/SampleData/run
89+
OB.API.request({
90+
endpoint: 'module/SampleData/run',
91+
method: 'POST',
92+
data: { profile: dir },
93+
}).then(function (result) {
94+
if (!result) {
95+
$('#sampledata_module-info').text('Import failed: no response from server.');
96+
$('#sampledata_module-import').prop('disabled', false);
97+
return;
98+
}
99+
100+
$('#sampledata_module-log').text((result.log || []).join('\n'));
101+
102+
if (result.success) {
103+
$('#sampledata_module-info').text('Sample data imported.');
104+
} else {
105+
$('#sampledata_module-info').text('Import failed: ' + (result.error || 'unknown error'));
106+
}
107+
108+
$('#sampledata_module-import').prop('disabled', false);
109+
});
110+
};

0 commit comments

Comments
 (0)