Compare commits
5 commits
538f7dc5a7
...
52ad6cc804
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ad6cc804 | |||
| 9bb7f8d147 | |||
| 31d0bddc38 | |||
| 0614fa1460 | |||
| ceebc2edb4 |
3 changed files with 49 additions and 16 deletions
16
Makefile
16
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: deploy clean start default frontend
|
.PHONY: deploy clean start debug frontend install-service
|
||||||
|
|
||||||
server/server: frontend
|
server/server: frontend
|
||||||
rm -rf server/dist
|
rm -rf server/dist
|
||||||
|
|
@ -8,19 +8,23 @@ server/server: frontend
|
||||||
frontend:
|
frontend:
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
deploy: server/server
|
deploy: server/server install-service
|
||||||
scp server/server kiosk:
|
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:
|
clean:
|
||||||
rm -rf dist server/dist server/server
|
rm -rf dist server/dist server/server
|
||||||
|
|
||||||
start: deploy
|
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 "pkill -f 'weston --shell=kiosk-shell.so'" || true
|
||||||
ssh kiosk weston --shell=kiosk-shell.so -- firefox --kiosk localhost:8000 --remote-debugging-port=9222
|
ssh kiosk weston --shell=kiosk-shell.so -- firefox --kiosk localhost:8000 --remote-debugging-port=9222
|
||||||
|
|
||||||
debug: deploy
|
debug: deploy
|
||||||
ssh kiosk "pgrep -f './server' || ./server &"
|
|
||||||
ssh kiosk "pkill -f 'weston --shell=kiosk-shell.so'" || true
|
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
|
ssh -L 6000:localhost:6000 kiosk weston --shell=kiosk-shell.so -- firefox --kiosk localhost:8000 --start-debugger-server 6000
|
||||||
|
|
|
||||||
13
server/digital-turntable.service
Normal file
13
server/digital-turntable.service
Normal 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
|
||||||
36
src/App.tsx
36
src/App.tsx
|
|
@ -52,20 +52,40 @@ function navidromeURL(path: string, params: Record<string, string>): string {
|
||||||
}).toString();
|
}).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WELCOME_TEXT = "Scan an album to begin.";
|
||||||
function App() {
|
function App() {
|
||||||
const [album, setAlbum] = useState<AlbumType | null>(null);
|
const [album, setAlbum] = useState<AlbumType | null>(null);
|
||||||
const [buffer, setBuffer] = useState<string[]>([]);
|
const [buffer, setBuffer] = useState<string[]>([]);
|
||||||
const [nowPlaying, play] = useState<number>(0);
|
const [nowPlaying, play] = useState<number>(0);
|
||||||
const [searching, setSearching] = useState(false);
|
const [displayText, setDisplayText] = useState(WELCOME_TEXT);
|
||||||
const [paused, setPaused] = useState(true);
|
const [paused, setPaused] = useState(true);
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
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 = () => {
|
const handleEnded = () => {
|
||||||
if ((album?.tracks?.length ?? 0) > nowPlaying + 1) { // if there's a next track
|
if ((album?.tracks?.length ?? 0) > nowPlaying + 1) { // if there's a next track
|
||||||
play(nowPlaying + 1);
|
play(nowPlaying + 1);
|
||||||
setPaused(false);
|
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");
|
console.log("empty barcode; ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearching(true);
|
setDisplayText("Searching…");
|
||||||
console.log("Scanned barcode:", barcode);
|
console.log("Scanned barcode:", barcode);
|
||||||
var mbRelease: IRelease | IReleaseMatch;
|
var mbRelease: IRelease | IReleaseMatch;
|
||||||
if (barcode.startsWith("mbid:")) {
|
if (barcode.startsWith("mbid:")) {
|
||||||
|
|
@ -145,8 +165,8 @@ function App() {
|
||||||
inc: ['artist-credits', 'release-groups', 'genres'], // TODO
|
inc: ['artist-credits', 'release-groups', 'genres'], // TODO
|
||||||
});
|
});
|
||||||
if (searchResponse.count === 0) {
|
if (searchResponse.count === 0) {
|
||||||
console.log("No album found for barcode:", barcode); // TODO: some kind of toast?
|
setDisplayText("No album found for barcode: " + barcode);
|
||||||
setSearching(false);
|
setTimeout(() => setDisplayText(WELCOME_TEXT), 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mbRelease = searchResponse.releases[0];
|
mbRelease = searchResponse.releases[0];
|
||||||
|
|
@ -179,7 +199,6 @@ function App() {
|
||||||
// # …and play!
|
// # …and play!
|
||||||
play(0);
|
play(0);
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
setSearching(false);
|
|
||||||
} else { // Not found in Navidrome; populate with MusicBrainz data instead
|
} else { // Not found in Navidrome; populate with MusicBrainz data instead
|
||||||
const fullMbRelease = await mbApi.lookup('release', mbRelease.id, ['recordings', 'artist-credits', 'release-groups']);
|
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
|
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 || '') || '',
|
coverArtLink: await getCoverArtSrcURL(fullMbRelease.id, fullMbRelease['release-group']?.id || '') || '',
|
||||||
})
|
})
|
||||||
console.log(album);
|
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>
|
<p className='welcome' style={{ gridColumn: 'span 2' }}>This album cannot be played, as Chandler doesn't own a copy.</p>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app" onPointerDown={click}>
|
||||||
<audio
|
<audio
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
src={album.tracks[nowPlaying].source}
|
src={album.tracks[nowPlaying].source}
|
||||||
|
|
@ -231,10 +249,8 @@ function App() {
|
||||||
{player}
|
{player}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else if (searching) {
|
|
||||||
return (<div className="welcome">Searching…</div>);
|
|
||||||
} else {
|
} else {
|
||||||
return (<div className="welcome">Scan an album to begin.</div>);
|
return (<div className="welcome" onPointerDown={click}>{displayText}</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue