Skip to content
Open
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
99 changes: 98 additions & 1 deletion AdminInterface/www/mupi.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,36 @@
$CHANGE_TXT=$CHANGE_TXT."<li>Press Button delay set to ".$_POST['pressDelay']. " seconds</li>";
$change=2;
}
$playtime_changed = false;
if( $_POST['playtime_save'] )
{
if( !isset($data["playtimeLimit"]) || !is_array($data["playtimeLimit"]) )
{
$data["playtimeLimit"] = array(
"enabled" => false,
"resetHour" => 0,
"maxOverrunMinutes" => 10,
"limitsMinutes" => array("mon"=>60,"tue"=>60,"wed"=>60,"thu"=>60,"fri"=>60,"sat"=>60,"sun"=>60),
);
}
if( !isset($data["playtimeLimit"]["limitsMinutes"]) || !is_array($data["playtimeLimit"]["limitsMinutes"]) )
{
$data["playtimeLimit"]["limitsMinutes"] = array("mon"=>60,"tue"=>60,"wed"=>60,"thu"=>60,"fri"=>60,"sat"=>60,"sun"=>60);
}
$data["playtimeLimit"]["enabled"] = (isset($_POST['playtime_enabled']) && $_POST['playtime_enabled'] === '1');
$data["playtimeLimit"]["resetHour"] = max(0, min(23, intval($_POST['playtime_resetHour'])));
$data["playtimeLimit"]["maxOverrunMinutes"] = max(0, min(60, intval($_POST['playtime_maxOverrunMinutes'])));
$playtime_days = array('mon','tue','wed','thu','fri','sat','sun');
foreach( $playtime_days as $d )
{
$field = 'playtime_limit_' . $d;
$val = isset($_POST[$field]) ? intval($_POST[$field]) : 60;
$data["playtimeLimit"]["limitsMinutes"][$d] = max(0, min(1440, $val));
}
$playtime_changed = true;
$CHANGE_TXT = $CHANGE_TXT."<li>Playtime limit settings saved (player restarting...)</li>";
$change = 2;
}
if( $data["shim"]["ledPin"]!=$_POST['ledPin'] && $_POST['ledPin'])
{
$data["shim"]["ledPin"]=$_POST['ledPin'];
Expand Down Expand Up @@ -569,7 +599,12 @@
exec("sudo mv /tmp/.mupiboxconfig.json /etc/mupibox/mupiboxconfig.json");
exec("sudo /usr/local/bin/mupibox/./setting_update.sh");
}

if( $playtime_changed )
{
// Player caches mupiboxconfig.json at startup via require(); restart so the new playtime values take effect.
exec("sudo -i -u dietpi pm2 restart spotify-control");
}

$CHANGE_TXT=$CHANGE_TXT."</ul></div>";
?>

Expand Down Expand Up @@ -695,6 +730,68 @@
</ul>
</details>

<details id="playtimelimit">
<summary><i class="fa-solid fa-hourglass-half"></i> Daily playtime limit</summary>
<ul>
<li id="li_1">
<h2>About</h2>
<p>Caps the total daily listening time on the box. When the limit is reached, playback stops and new playback is refused until the next day. Set a day to <b>0</b> to block playback completely on that day. Settings take effect after saving (the player is restarted automatically).</p>
</li>
<li id="li_1">
<h2>Status</h2>
<?php
$playtime_enabled_state = ( isset($data["playtimeLimit"]["enabled"]) && $data["playtimeLimit"]["enabled"] ) ? true : false;
$playtime_resetHour = isset($data["playtimeLimit"]["resetHour"]) ? intval($data["playtimeLimit"]["resetHour"]) : 0;
$playtime_limits = isset($data["playtimeLimit"]["limitsMinutes"]) && is_array($data["playtimeLimit"]["limitsMinutes"]) ? $data["playtimeLimit"]["limitsMinutes"] : array();
echo '<p>Currently: <b>'.($playtime_enabled_state ? 'ENABLED' : 'DISABLED').'</b></p>';
?>
<p>Enable / disable the daily limit:</p>
<select name="playtime_enabled">
<option value="1" <?php echo $playtime_enabled_state ? 'selected' : ''; ?>>Enabled</option>
<option value="0" <?php echo !$playtime_enabled_state ? 'selected' : ''; ?>>Disabled</option>
</select>
</li>
<li id="li_1">
<h2>Reset hour (0 - 23)</h2>
<p>Hour of day at which the counter resets to 0. <b>0</b> = midnight. Use e.g. <b>4</b> if you don't want a reset to interrupt late evening listening.</p>
<input type="number" name="playtime_resetHour" min="0" max="23" step="1" value="<?php echo $playtime_resetHour; ?>">
</li>
<li id="li_1">
<h2>Grace period (minutes)</h2>
<p>When the daily limit is reached, allow playback to continue for up to this many additional minutes so the current track can finish naturally. The player stops at the next track boundary (for local files / radio / RSS) or at the latest when this grace runs out. <b>0</b> = stop immediately at the limit. Default: <b>10</b>. Maximum: 60.</p>
<?php $playtime_maxOverrunMinutes = isset($data["playtimeLimit"]["maxOverrunMinutes"]) ? intval($data["playtimeLimit"]["maxOverrunMinutes"]) : 10; ?>
<input type="number" name="playtime_maxOverrunMinutes" min="0" max="60" step="1" value="<?php echo $playtime_maxOverrunMinutes; ?>"> min
</li>
<li id="li_1">
<h2>Daily limit per weekday (minutes)</h2>
<p>Set <b>0</b> to block playback entirely on that day. Maximum 1440 (= 24 h).</p>
<table class="version">
<tr><th>Day</th><th>Minutes per day</th></tr>
<?php
$playtime_day_labels = array(
'mon' => 'Monday',
'tue' => 'Tuesday',
'wed' => 'Wednesday',
'thu' => 'Thursday',
'fri' => 'Friday',
'sat' => 'Saturday',
'sun' => 'Sunday',
);
foreach( $playtime_day_labels as $key => $label )
{
$val = isset($playtime_limits[$key]) ? intval($playtime_limits[$key]) : 60;
echo '<tr><td>'.$label.'</td><td><input type="number" name="playtime_limit_'.$key.'" min="0" max="1440" step="1" value="'.$val.'"> min</td></tr>';
}
?>
</table>
</li>
<li class="buttons">
<input type="hidden" name="form_id" value="37271" />
<input id="saveForm" class="button_text" type="submit" name="playtime_save" value="Save playtime settings" />
</li>
</ul>
</details>

<details id="systemsettings">
<summary><i class="fa-solid fa-screwdriver-wrench"></i> System settings</summary>
<ul>
Expand Down
14 changes: 14 additions & 0 deletions config/templates/mupiboxconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@
"idleDisplayOff": "10",
"pressDelay": "2"
},
"playtimeLimit": {
"enabled": false,
"resetHour": 0,
"maxOverrunMinutes": 10,
"limitsMinutes": {
"mon": 60,
"tue": 60,
"wed": 60,
"thu": 60,
"fri": 60,
"sat": 60,
"sun": 60
}
},
"shim": {
"poweroffPin": "4",
"triggerPin": "17",
Expand Down
3 changes: 3 additions & 0 deletions src/backend-api/src/models/mupibox-config.model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { PlaytimeLimitConfig } from './playtime.model'

export interface MupiboxConfig {
spotify?: {
disableScraperForPlaylists?: boolean
[key: string]: unknown
}
playtimeLimit?: PlaytimeLimitConfig
[key: string]: unknown
}
33 changes: 33 additions & 0 deletions src/backend-api/src/models/playtime.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type PlaytimeDayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'

export type PlaytimeLimitsMinutes = Partial<Record<PlaytimeDayKey, number>>

export interface PlaytimeLimitConfig {
enabled: boolean
resetHour?: number
maxOverrunMinutes?: number
limitsMinutes?: PlaytimeLimitsMinutes
}

// 'normal' → under daily limit, playback unrestricted
// 'grace' → over limit, current track allowed to finish; new commands blocked
// 'blocked' → fully stopped, frontend overlays the screen
export type PlaytimePlayState = 'normal' | 'grace' | 'blocked'

export type PlaytimeStatus = PlaytimeStatusEnabled | PlaytimeStatusDisabled

export interface PlaytimeStatusDisabled {
enabled: false
}

export interface PlaytimeStatusEnabled {
enabled: true
state: PlaytimePlayState
date: string
dayKey: PlaytimeDayKey
limitMinutes: number
usedSeconds: number
remainingSeconds: number
graceEndsInSeconds: number
resetHour: number
}
22 changes: 22 additions & 0 deletions src/backend-api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ky from 'ky'
import xmlparser from 'xml-js'
import { LogRequest, LogResponse } from './models/log.model'
import type { MupiboxConfig } from './models/mupibox-config.model'
import type { PlaytimeStatus } from './models/playtime.model'
import { ServerConfig } from './models/server.model'
import type { SpotifyValidationRequest, SpotifyValidationResponse } from './models/spotify-api.model'
import { SpotifyApiService } from './services/spotify-api.service'
Expand Down Expand Up @@ -62,6 +63,7 @@ const wlanFile = `${configBasePath}/wlan.json`
const monitorFile = `${configBasePath}/monitor.json`
const albumstopFile = `${configBasePath}/albumstop.json`
const mupihat = '/tmp/mupihat.json'
const playtimeFile = '/tmp/playtime.json'
const dataLock = '/tmp/.data.lock'
const resumeLock = '/tmp/.resume.lock'

Expand Down Expand Up @@ -163,6 +165,26 @@ app.get('/api/mupihat', (_req, res) => {
}
})

// Playback time tracking written by backend-player to /tmp/playtime.json (tmpfs).
// Missing/unreadable file means the player hasn't ticked yet or the feature is off —
// either way, surfaces as "disabled" so the frontend can hide the UI safely.
app.get('/api/playtime', (_req, res) => {
const disabled: PlaytimeStatus = { enabled: false }
if (!fs.existsSync(playtimeFile)) {
res.json(disabled)
return
}
jsonfile.readFile(playtimeFile, (error, data) => {
if (error) {
console.log(`${new Date().toLocaleString()}: [MuPiBox-Server] Error /api/playtime read playtime.json`)
console.log(`${new Date().toLocaleString()}: [MuPiBox-Server] ${error}`)
res.json(disabled)
} else {
res.json(data)
}
})
})

app.get('/api/activeresume', (_req, res) => {
if (fs.existsSync(activeresumeFile)) {
jsonfile.readFile(activeresumeFile, (error, data) => {
Expand Down
Loading