Initial implementation: Bordanlage boat onboard system
Complete multiroom audio + navigation dashboard: - Docker stack: SignalK, Snapcast (4 zones), librespot, shairport-sync, Mopidy, Jellyfin, Portainer - React 18 + Vite dashboard with nautical dark theme - Full mock system (SignalK NMEA simulation, Snapcast zones, Mopidy player) - Real API clients for all services with reconnect logic - SVG instruments: Compass, WindRose, Gauge, DepthSounder, SpeedLog - Pages: Overview, Navigation, Audio (zones/radio/library), Systems - Dev mode runs fully without hardware (make dev) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
149
bordanlage/dashboard/src/mock/mopidy.mock.js
Normal file
149
bordanlage/dashboard/src/mock/mopidy.mock.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// Simulates the Mopidy JSON-RPC WebSocket API.
|
||||
|
||||
const TRACKS = [
|
||||
{
|
||||
uri: 'mock:track:1',
|
||||
name: 'Ocean Drive',
|
||||
artists: [{ name: 'Duke Dumont' }],
|
||||
album: { name: 'Ocean Drive', images: [] },
|
||||
length: 232000,
|
||||
},
|
||||
{
|
||||
uri: 'mock:track:2',
|
||||
name: 'Feel It Still',
|
||||
artists: [{ name: 'Portugal. The Man' }],
|
||||
album: { name: 'Woodstock', images: [] },
|
||||
length: 178000,
|
||||
},
|
||||
{
|
||||
uri: 'mock:track:3',
|
||||
name: 'Sailing',
|
||||
artists: [{ name: 'Christopher Cross' }],
|
||||
album: { name: 'Christopher Cross', images: [] },
|
||||
length: 261000,
|
||||
},
|
||||
{
|
||||
uri: 'mock:track:4',
|
||||
name: 'Beyond the Sea',
|
||||
artists: [{ name: 'Bobby Darin' }],
|
||||
album: { name: 'That\'s All', images: [] },
|
||||
length: 185000,
|
||||
},
|
||||
{
|
||||
uri: 'mock:track:5',
|
||||
name: 'Into the Mystic',
|
||||
artists: [{ name: 'Van Morrison' }],
|
||||
album: { name: 'Moondance', images: [] },
|
||||
length: 215000,
|
||||
},
|
||||
]
|
||||
|
||||
const RADIO_STATIONS = [
|
||||
{ uri: 'http://stream.swr3.de/swr3/mp3-128/stream.mp3', name: 'SWR3' },
|
||||
{ uri: 'http://ndr.de/ndr1welle-nord-128.mp3', name: 'NDR 1 Welle Nord' },
|
||||
{ uri: 'http://live-bauhaus.radiobt.de/bauhaus/mp3-128', name: 'Radio Bauhaus' },
|
||||
]
|
||||
|
||||
export function createMopidyMock() {
|
||||
let state = 'playing'
|
||||
let currentIndex = 0
|
||||
let position = 0
|
||||
let positionTimer = null
|
||||
|
||||
const listeners = {}
|
||||
|
||||
function emit(event, data) {
|
||||
if (listeners[event]) listeners[event].forEach(fn => fn(data))
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (positionTimer) return
|
||||
positionTimer = setInterval(() => {
|
||||
if (state === 'playing') {
|
||||
position += 1000
|
||||
const track = TRACKS[currentIndex]
|
||||
if (position >= track.length) {
|
||||
currentIndex = (currentIndex + 1) % TRACKS.length
|
||||
position = 0
|
||||
emit('event:trackPlaybackStarted', { tl_track: { track: TRACKS[currentIndex] } })
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
startTimer()
|
||||
|
||||
async function call(method, params = {}) {
|
||||
await new Promise(r => setTimeout(r, 15))
|
||||
|
||||
switch (method) {
|
||||
case 'playback.get_current_track':
|
||||
return TRACKS[currentIndex]
|
||||
|
||||
case 'playback.get_state':
|
||||
return state
|
||||
|
||||
case 'playback.get_time_position':
|
||||
return position
|
||||
|
||||
case 'playback.play':
|
||||
state = 'playing'
|
||||
emit('event:playbackStateChanged', { new_state: 'playing' })
|
||||
return null
|
||||
|
||||
case 'playback.pause':
|
||||
state = 'paused'
|
||||
emit('event:playbackStateChanged', { new_state: 'paused' })
|
||||
return null
|
||||
|
||||
case 'playback.stop':
|
||||
state = 'stopped'
|
||||
position = 0
|
||||
emit('event:playbackStateChanged', { new_state: 'stopped' })
|
||||
return null
|
||||
|
||||
case 'playback.next':
|
||||
currentIndex = (currentIndex + 1) % TRACKS.length
|
||||
position = 0
|
||||
state = 'playing'
|
||||
emit('event:trackPlaybackStarted', { tl_track: { track: TRACKS[currentIndex] } })
|
||||
return null
|
||||
|
||||
case 'playback.previous':
|
||||
currentIndex = (currentIndex - 1 + TRACKS.length) % TRACKS.length
|
||||
position = 0
|
||||
state = 'playing'
|
||||
emit('event:trackPlaybackStarted', { tl_track: { track: TRACKS[currentIndex] } })
|
||||
return null
|
||||
|
||||
case 'playback.seek':
|
||||
position = params.time_position || 0
|
||||
return null
|
||||
|
||||
case 'tracklist.get_tracks':
|
||||
return TRACKS
|
||||
|
||||
case 'library.browse':
|
||||
return TRACKS.map(t => ({ uri: t.uri, name: t.name, type: 'track' }))
|
||||
|
||||
case 'library.search':
|
||||
return [{ tracks: TRACKS }]
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
call,
|
||||
on(event, fn) {
|
||||
if (!listeners[event]) listeners[event] = []
|
||||
listeners[event].push(fn)
|
||||
},
|
||||
off(event, fn) {
|
||||
if (listeners[event]) listeners[event] = listeners[event].filter(l => l !== fn)
|
||||
},
|
||||
getTracks: () => TRACKS,
|
||||
getRadioStations: () => RADIO_STATIONS,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user