Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
37d418b
Improve YouTube quota tracking and search caching
choompuy Oct 17, 2026
9bb70a7
Harden Twitch authentication and error handling
choompuy Oct 17, 2026
868dd85
Harden localization and Twitch chat messaging
choompuy Oct 17, 2026
492b711
Harden API errors and Twitch authentication flows
choompuy Oct 17, 2026
e69a376
Harden persistence and avoid redundant state updates
choompuy Oct 17, 2026
a8cef23
Merge branch 'main' into feat/backend-improve
choompuy Oct 17, 2026
1f3bad0
Centralize API errors and input validation
choompuy Oct 18, 2026
414e1bd
Harden persistence, Twitch auth, and YouTube URL handling
choompuy Oct 18, 2026
ae999ba
Standardize HTTP errors and harden request handling
choompuy Oct 18, 2026
85bf294
Centralize store updates and simplify SSE events
choompuy Oct 18, 2026
53d3504
Harden playback state and centralize store persistence
choompuy Oct 19, 2026
759497a
Unify track completion and harden queue advancement
choompuy Oct 19, 2026
8ca5785
Expand playlist loading and improve error handling
choompuy Oct 19, 2026
f9540e8
Unify fallback track labeling and player lifecycle handling
choompuy Oct 19, 2026
8731f88
Add strict TypeScript checking for tests
choompuy Oct 19, 2026
489130d
Centralize networking and harden application lifecycle
choompuy Oct 20, 2026
5aeb4eb
Simplify track duration filtering and improve playability checks
choompuy Oct 20, 2026
588269e
Preserve unsaved settings and improve playlist status feedback
choompuy Oct 20, 2026
9f28913
Improve remote-device handling and clarify queue status
choompuy Oct 20, 2026
69361e1
Improve server initialization and harden error handling
choompuy Oct 21, 2026
e5df8fc
Refactor Twitch integration into focused modules
choompuy Oct 21, 2026
d1b84ec
Improve Twitch lifecycle handling and track display
choompuy Oct 21, 2026
a56ab41
Merge branch 'main' into feat/backend-improve
choompuy Oct 21, 2026
1878852
Merge main: harden frontend requests and playback handling
choompuy Oct 21, 2026
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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ Most configuration is editable from the web control panel.
| Max queue size | 20 |
| Max active requests per user | 4 |
| Region | none |
| Allow Shorts | disabled |
| Allow live streams | disabled |
| Fallback playlist | disabled until configured |

Expand Down Expand Up @@ -191,14 +190,19 @@ Application state is stored as JSON next to the executable:
```text
data/
config.json
settings.json
server.json
twitch-config.json
playlists.json
blocklist.json
activity.json
queue-state.json
secrets.json
logs/
app.log

cache/
queue-state.json
youtube-cache.json

logs/
fallback-tracks.json
```

Your YouTube API key and Twitch OAuth state are stored locally. The application communicates with external services only when needed for its integrations, primarily the YouTube Data API and Twitch APIs.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"test:frontend": "node --test \"tests/**/*.test.js\"",
"test:smoke": "node scripts/smoke-frontend.mjs",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"typecheck": "npm run typecheck:src && npm run typecheck:tests",
"typecheck:src": "tsc --noEmit",
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
"build": "tsc",
"build:sea": "node scripts/build-sea.mjs",
"build:release": "node scripts/build-release.mjs"
Expand Down
28 changes: 19 additions & 9 deletions public/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,17 @@ button:disabled {
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
gap: 0.5rem;
background-color: var(--bg-elevated);
flex-wrap: wrap;
}

.section-tab-actions {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
margin-left: auto;
gap: 0.5rem;
}

.row-menu {
Expand Down Expand Up @@ -427,14 +437,6 @@ button:disabled {
flex-shrink: 0;
}

.section-tab-actions {
display: inline-flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}

.row-list-wrapper {
flex: 1;
min-height: 8rem;
Expand Down Expand Up @@ -560,8 +562,9 @@ button:disabled {
.now-playing-wrapper .now-playing-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
margin-top: auto;
gap: 0.5rem;
}

.now-playing-wrapper .now-playing-controls {
Expand Down Expand Up @@ -655,6 +658,13 @@ button:disabled {
gap: 1rem;
}

.remote-note {
grid-column: 1 / -1;
padding: 0.75rem 1rem;
border: 1px solid var(--border);
color: var(--text-secondary);
}

.settings-section-column {
display: flex;
flex-direction: row;
Expand Down
22 changes: 22 additions & 0 deletions public/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ body {
display: none !important;
}

.remote-only {
display: none;
}

body.not-local .local-only {
display: none;
}

body.not-local .remote-only {
display: block;
}

.text-primary {
color: var(--text-primary);
}
Expand Down Expand Up @@ -92,6 +104,16 @@ body {
text-overflow: ellipsis;
}

.truncate-3line {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
white-space: wrap;
overflow: hidden;
text-overflow: ellipsis;
}

.box-shadow {
box-shadow: rgba(0, 0, 0, 0.4) 0 0.25rem 0.875rem 0;
}
84 changes: 36 additions & 48 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@
<div class="now-playing-top">
<div class="now-playing-empty empty" id="noPlaying" data-i18n="nowPlaying.nothingPlaying">Nothing playing</div>
<div class="now-playing-info hidden" id="nowPlaying">
<div class="text-lg text-bold" id="currentTitle"></div>
<div class="text-secondary text-md" id="currentChannel"></div>
<div class="text-lg text-bold truncate-3line" id="currentTitle"></div>
<div class="text-secondary text-md truncate" id="currentChannel"></div>
<div class="text-muted text-sm"><span id="currentDuration"></span> ▪ <span id="currentViews"></span></div>
<div class="text-green text-sm" id="currentRequester"></div>
<div class="text-green text-sm truncate" id="currentRequester"></div>
</div>

<div class="now-playing-controls">
Expand Down Expand Up @@ -191,26 +191,28 @@

<div class="section-tab section-tab-queue" data-section="queue">
<div class="section-tab-header">
<h2><span data-i18n="queue.title">Queue</span> <span class="text-sm text-secondary" id="queueCount">0/0</span></h2>
<button
type="button"
id="clearQueueBtn"
class="btn btn-secondary btn-danger"
data-action="clear-queue"
data-confirm="queue.clearConfirm"
data-i18n-title="queue.clear"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m18 6-.8 12.013c-.071 1.052-.106 1.578-.333 1.977a2 2 0 0 1-.866.81c-.413.2-.94.2-1.995.2H9.994c-1.055 0-1.582 0-1.995-.2a2 2 0 0 1-.866-.81c-.227-.399-.262-.925-.332-1.977L6 6M4 6h16m-4 0-.27-.812c-.263-.787-.394-1.18-.637-1.471a2 2 0 0 0-.803-.578C13.939 3 13.524 3 12.695 3h-1.388c-.829 0-1.244 0-1.596.139a2 2 0 0 0-.803.578c-.243.29-.374.684-.636 1.471L8 6m6 4v7m-4-7v7"
/>
</svg>
<span data-i18n="queue.clear">Clear</span>
</button>
<span class="text-sm text-secondary" id="queueCount">Queue is empty</span>
<div class="section-tab-actions">
<button
type="button"
id="clearQueueBtn"
class="btn btn-secondary btn-danger"
data-action="clear-queue"
data-confirm="queue.clearConfirm"
data-i18n-title="queue.clear"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m18 6-.8 12.013c-.071 1.052-.106 1.578-.333 1.977a2 2 0 0 1-.866.81c-.413.2-.94.2-1.995.2H9.994c-1.055 0-1.582 0-1.995-.2a2 2 0 0 1-.866-.81c-.227-.399-.262-.925-.332-1.977L6 6M4 6h16m-4 0-.27-.812c-.263-.787-.394-1.18-.637-1.471a2 2 0 0 0-.803-.578C13.939 3 13.524 3 12.695 3h-1.388c-.829 0-1.244 0-1.596.139a2 2 0 0 0-.803.578c-.243.29-.374.684-.636 1.471L8 6m6 4v7m-4-7v7"
/>
</svg>
<span data-i18n="queue.clear">Clear</span>
</button>
</div>
</div>

<div class="row-list-wrapper" id="queueListWrapper" data-list="queue">
Expand All @@ -221,10 +223,7 @@ <h2><span data-i18n="queue.title">Queue</span> <span class="text-sm text-seconda

<div class="section-tab section-tab-fallback hidden" data-section="playlist">
<div class="section-tab-header">
<div>
<h2 data-i18n="fallback.title">Playlist</h2>
<div id="fallbackInfo" class="text-sm text-secondary"></div>
</div>
<span id="fallbackInfo" class="text-sm text-secondary"></span>
<div class="section-tab-actions">
<button
type="button"
Expand Down Expand Up @@ -309,7 +308,7 @@ <h2 data-i18n="fallback.title">Playlist</h2>

<div class="section-tab section-tab-activity hidden" data-section="activity">
<div class="section-tab-header">
<h2 data-i18n="activity.title">Activity</h2>
<span id="activityCount" class="text-sm text-secondary"></span>
<div class="section-tab-actions">
<button
type="button"
Expand Down Expand Up @@ -339,9 +338,6 @@ <h2 data-i18n="activity.title">Activity</h2>
</div>

<div class="section-tab section-tab-blocklist hidden" data-section="blocklist">
<div class="section-tab-header">
<h2 data-i18n="blocklist.title">Blocklist</h2>
</div>
<div class="row-list-wrapper" id="blocklistListWrapper" data-list="blocklist">
<div class="row-list"></div>
<div class="empty" data-i18n="blocklist.empty">No blocked tracks</div>
Expand All @@ -351,10 +347,6 @@ <h2 data-i18n="blocklist.title">Blocklist</h2>
</section>

<section class="container page-tab" data-page-tab="playlists">
<div class="tab-header">
<h1 data-i18n="playlists.title">Playlists</h1>
</div>

<div class="playlist-add-wrapper panel section-tab flex-1">
<div class="row">
<input
Expand Down Expand Up @@ -384,10 +376,11 @@ <h1 data-i18n="playlists.title">Playlists</h1>
</section>

<section class="container page-tab" data-page-tab="settings">
<div class="tab-header">
<h1 data-i18n="settings.title">Settings</h1>
</div>
<div class="settings-wrapper">
<div id="remoteSettingsNote" class="remote-only remote-note text-sm" data-i18n="settings.remoteNote">
Opened from another device - settings that only this machine may change (the API key and the Twitch integration) are hidden. Change them
on the computer running the app
</div>
<div id="overlaySettingsPanel" class="section-tab panel">
<div class="section-tab-header">
<h2 data-i18n="settings.overlay.title">Overlay</h2>
Expand Down Expand Up @@ -487,7 +480,7 @@ <h2 data-i18n="settings.overlay.title">Overlay</h2>
<h2 data-i18n="settings.bot.title">Bot Settings</h2>
</div>

<div class="settings-section">
<div class="settings-section local-only">
<label class="field">
<span class="field-label" data-i18n="settings.bot.apiKey">API Key</span>
<input
Expand Down Expand Up @@ -518,7 +511,7 @@ <h2 data-i18n="settings.bot.title">Bot Settings</h2>

<label class="field">
<span class="field-label" data-i18n="settings.bot.limits.maxQueue">Max queue size</span>
<input class="settings-input input" type="number" id="cfgMaxQueue" name="maxQueueSize" min="1" />
<input class="settings-input input" type="number" id="cfgMaxQueue" name="maxQueueSize" min="0" />
</label>
<label class="field">
<span class="field-label" data-i18n="settings.bot.limits.maxPerUser">Max requests per user</span>
Expand All @@ -527,11 +520,6 @@ <h2 data-i18n="settings.bot.title">Bot Settings</h2>
</div>

<div class="settings-section">
<label class="field-row">
<span class="field-label" data-i18n="settings.bot.limits.allowShorts">Shorts</span>
<input class="settings-input checkbox" type="checkbox" id="cfgAllowShorts" name="allowShorts" />
</label>

<label class="field-row">
<span class="field-label" data-i18n="settings.bot.limits.allowLiveStreams">Live streams</span>
<input class="settings-input checkbox" type="checkbox" id="cfgAllowLiveStreams" name="allowLiveStreams" />
Expand Down Expand Up @@ -572,7 +560,7 @@ <h2 data-i18n="settings.bot.title">Bot Settings</h2>
</div>
</div>

<div class="section-tab panel">
<div class="section-tab panel local-only">
<div class="section-tab-header">
<h2 data-i18n="settings.twitch.integration">Twitch integration</h2>
</div>
Expand Down Expand Up @@ -659,7 +647,7 @@ <h2 data-i18n="settings.twitch.editReward">Edit reward</h2>
</label>
<label class="field">
<span class="field-label" data-i18n="settings.twitch.createRewardPrompt">Description (optional)</span>
<input class="settings-input input" type="text" id="twitchRewardPrompt" name="prompt" maxlength="140" autocomplete="off" />
<input class="settings-input input" type="text" id="twitchRewardPrompt" name="prompt" maxlength="200" autocomplete="off" />
</label>
<label class="field" for="twitchRewardBackgroundColor">
<span class="field-label" data-i18n="settings.twitch.createRewardBackgroundColor">Background color (hex, optional)</span>
Expand Down Expand Up @@ -744,7 +732,7 @@ <h2 data-i18n="settings.twitch.editReward">Edit reward</h2>
</div>
</div>

<div id="twitchChatCommandsPanel" class="section-tab panel hidden">
<div id="twitchChatCommandsPanel" class="section-tab panel local-only hidden">
<div class="section-tab-header">
<h2 data-i18n="settings.twitchChat.title">Twitch chat commands</h2>
</div>
Expand Down
4 changes: 4 additions & 0 deletions public/js/control/activity.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { api } from './api.js'
import { state } from './state.js'
import { dom } from './dom.js'
import { renderStats } from './stats.js'
import { views } from './views/index.js'
import { run } from './run.js'
import { t } from '../i18n.js'
import { setText } from '../shared.js'

function renderActivity() {
views.activity.render(state.activity)
setText(dom.activityCount, t('activity.requests', { count: state.activity.length }))
}

export function loadActivity(silent = false) {
Expand Down
Loading
Loading