Compare commits

...

5 commits

Author SHA1 Message Date
52ad6cc804
Slop: Add systemd user service for server 2026-02-18 20:39:54 -06:00
9bb7f8d147
Turn off barcode reader after 15m timeout
A future enhancement would be to make sure it only times out 15m after
an album is done playing (possibly by ignoring signals while an album is
playing, and then at the end of the album set the timer again?) but that
wasn't something I've implemented yet.
2026-02-18 20:38:34 -06:00
31d0bddc38
Reset on album end 2026-02-18 20:36:39 -06:00
0614fa1460
Extract out WELCOME_TEXT 2026-02-18 20:35:22 -06:00
ceebc2edb4
Generalize welcome message
This will be helpful in preparation for the next step: Timeout on idle.
2026-02-12 16:40:03 -06:00
3 changed files with 49 additions and 16 deletions

View file

@ -1,4 +1,4 @@
.PHONY: deploy clean start default frontend
.PHONY: deploy clean start debug frontend install-service
server/server: frontend
rm -rf server/dist
@ -8,19 +8,23 @@ server/server: frontend
frontend:
npm run build
deploy: server/server
scp server/server kiosk:
deploy: server/server install-service
ssh kiosk "systemctl --user stop digital-turntable.service" || true
scp server/server kiosk:~/server
ssh kiosk "chmod +x ~/server && systemctl --user start digital-turntable.service"
install-service:
ssh kiosk "mkdir -p ~/.config/systemd/user"
scp server/digital-turntable.service kiosk:~/.config/systemd/user/digital-turntable.service
ssh kiosk "systemctl --user daemon-reload && systemctl --user enable digital-turntable.service"
clean:
rm -rf dist server/dist server/server
start: deploy
# TODO: This doesn't kill weston nicely; I should handle that? Or background or something
ssh kiosk "pgrep -f './server' || ./server &"
ssh kiosk "pkill -f 'weston --shell=kiosk-shell.so'" || true
ssh kiosk weston --shell=kiosk-shell.so -- firefox --kiosk localhost:8000 --remote-debugging-port=9222
debug: deploy
ssh kiosk "pgrep -f './server' || ./server &"
ssh kiosk "pkill -f 'weston --shell=kiosk-shell.so'" || true
ssh -L 6000:localhost:6000 kiosk weston --shell=kiosk-shell.so -- firefox --kiosk localhost:8000 --start-debugger-server 6000

View file

@ -0,0 +1,13 @@
[Unit]
Description=Digital Turntable kiosk server
After=network.target
[Service]
Type=simple
WorkingDirectory=%h
ExecStart=%h/server
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target

View file

@ -52,20 +52,40 @@ function navidromeURL(path: string, params: Record<string, string>): string {
}).toString();
}
const WELCOME_TEXT = "Scan an album to begin.";
function App() {
const [album, setAlbum] = useState<AlbumType | null>(null);
const [buffer, setBuffer] = useState<string[]>([]);
const [nowPlaying, play] = useState<number>(0);
const [searching, setSearching] = useState(false);
const [displayText, setDisplayText] = useState(WELCOME_TEXT);
const [paused, setPaused] = useState(true);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [lastClicked, setLastClicked] = useState(new Date);
const click = () => setLastClicked(new Date);
useEffect(() => {
const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
window.setTimeout(() => {
setDisplayText("Asleep. Tap screen to scan.");
fetch('/api/reader/auto', { method: 'POST' });
}, lastClicked.getTime() + IDLE_TIMEOUT_MS - new Date().getTime());
return () => {
setDisplayText(WELCOME_TEXT);
fetch('/api/reader/on', { method: 'POST' });
}
}, [lastClicked]);
const handleEnded = () => {
if ((album?.tracks?.length ?? 0) > nowPlaying + 1) { // if there's a next track
play(nowPlaying + 1);
setPaused(false);
} else {
setAlbum(null);
setDisplayText(WELCOME_TEXT); // Either I could always be diligent about resetting it elsewhere…or I can just do it here!
}
};
@ -129,7 +149,7 @@ function App() {
console.log("empty barcode; ignoring");
return;
}
setSearching(true);
setDisplayText("Searching…");
console.log("Scanned barcode:", barcode);
var mbRelease: IRelease | IReleaseMatch;
if (barcode.startsWith("mbid:")) {
@ -145,8 +165,8 @@ function App() {
inc: ['artist-credits', 'release-groups', 'genres'], // TODO
});
if (searchResponse.count === 0) {
console.log("No album found for barcode:", barcode); // TODO: some kind of toast?
setSearching(false);
setDisplayText("No album found for barcode: " + barcode);
setTimeout(() => setDisplayText(WELCOME_TEXT), 3000);
return;
}
mbRelease = searchResponse.releases[0];
@ -179,7 +199,6 @@ function App() {
// # …and play!
play(0);
setPaused(false);
setSearching(false);
} else { // Not found in Navidrome; populate with MusicBrainz data instead
const fullMbRelease = await mbApi.lookup('release', mbRelease.id, ['recordings', 'artist-credits', 'release-groups']);
const tracks: Track[] = fullMbRelease.media[0].tracks.map((t: ITrack) => ({ // TODO: handle multi-disc albums
@ -196,7 +215,6 @@ function App() {
coverArtLink: await getCoverArtSrcURL(fullMbRelease.id, fullMbRelease['release-group']?.id || '') || '',
})
console.log(album);
setSearching(false);
}
}
@ -214,7 +232,7 @@ function App() {
<p className='welcome' style={{ gridColumn: 'span 2' }}>This album cannot be played, as Chandler doesn't own a copy.</p>
);
return (
<div className="app">
<div className="app" onPointerDown={click}>
<audio
ref={audioRef}
src={album.tracks[nowPlaying].source}
@ -231,10 +249,8 @@ function App() {
{player}
</div>
)
} else if (searching) {
return (<div className="welcome">Searching</div>);
} else {
return (<div className="welcome">Scan an album to begin.</div>);
return (<div className="welcome" onPointerDown={click}>{displayText}</div>);
}
}