diff --git a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue index 691a77aa..41b94ee0 100644 --- a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue +++ b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue @@ -2,7 +2,11 @@ import { computed } from 'vue' import { useRouter } from 'vue-router' import { usePlaybackStore } from '@/stores/playback' -import type { RendererSummary, RendererState } from '@/services/pmocontrol/types' +import type { + RendererCapabilitiesSummary, + RendererSummary, + RendererState, +} from '@/services/pmocontrol/types' import StatusBadge from './StatusBadge.vue' import { Music, Volume2, VolumeX } from 'lucide-vue-next' @@ -18,30 +22,44 @@ const metadata = computed(() => playbackStore.getTrackMetadata(props.renderer.id const protocolLabel = computed(() => { switch (props.renderer.protocol) { - case 'UpnpAvOnly': + case 'upnp': return 'UPnP AV' - case 'OpenHomeOnly': + case 'openhome': return 'OpenHome' - case 'Hybrid': - return 'Hybrid' + case 'hybrid': + return 'Hybrid (UPnP + OpenHome)' default: - return 'Unknown' + return 'Inconnu' } }) const protocolClass = computed(() => { switch (props.renderer.protocol) { - case 'UpnpAvOnly': + case 'upnp': return 'protocol-upnp' - case 'OpenHomeOnly': + case 'openhome': return 'protocol-openhome' - case 'Hybrid': + case 'hybrid': return 'protocol-hybrid' default: return 'protocol-unknown' } }) +const capabilityBadges = computed(() => { + const caps = props.renderer.capabilities + if (!caps) return [] + const mapping: Array<{ key: keyof RendererCapabilitiesSummary; label: string }> = [ + { key: 'has_avtransport', label: 'AVTransport' }, + { key: 'has_oh_playlist', label: 'OpenHome' }, + { key: 'has_linkplay_http', label: 'Hybrid' }, + { key: 'has_oh_volume', label: 'Vol' }, + { key: 'has_oh_time', label: 'Time' }, + { key: 'has_oh_info', label: 'Info' }, + ] + return mapping.filter(({ key }) => caps[key]).map(({ key, label }) => ({ key, label })) +}) + const hasCover = computed(() => !!metadata.value?.album_art_uri) function goToRenderer() { @@ -66,6 +84,12 @@ function goToRenderer() { +
+ + {{ badge.label }} + +
+
{ + const resp = await fetch( + `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist`, + ) + if (!resp.ok) { + throw new Error(`Failed to fetch OpenHome playlist: ${resp.status} ${resp.statusText}`) + } + return resp.json() +} + +export async function clearOpenHomePlaylist(rendererId: string): Promise { + const resp = await fetch( + `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist/clear`, + { method: 'POST' }, + ) + if (!resp.ok) { + throw new Error(`Failed to clear OpenHome playlist: ${resp.status} ${resp.statusText}`) + } +} + +export async function addOpenHomeTrack( + rendererId: string, + payload: OpenHomePlaylistAddRequest, +): Promise { + const resp = await fetch( + `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist/add`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }, + ) + if (!resp.ok) { + throw new Error(`Failed to add track to OpenHome playlist: ${resp.status} ${resp.statusText}`) + } +} + +export async function playOpenHomeTrack(rendererId: string, trackId: number): Promise { + const resp = await fetch( + `${API_BASE}/renderers/${encodeURIComponent( + rendererId, + )}/oh/playlist/play/${encodeURIComponent(trackId.toString())}`, + { method: 'POST' }, + ) + if (!resp.ok) { + throw new Error(`Failed to play OpenHome track ${trackId}: ${resp.status} ${resp.statusText}`) + } +} diff --git a/pmoapp/webapp/src/services/pmocontrol/types.ts b/pmoapp/webapp/src/services/pmocontrol/types.ts index 0becbf5d..57b3e097 100644 --- a/pmoapp/webapp/src/services/pmocontrol/types.ts +++ b/pmoapp/webapp/src/services/pmocontrol/types.ts @@ -5,11 +5,28 @@ // RENDERERS // ============================================================================ +export type RendererProtocolSummary = 'upnp' | 'openhome' | 'hybrid' + +export interface RendererCapabilitiesSummary { + has_avtransport: boolean + has_avtransport_set_next: boolean + has_rendering_control: boolean + has_connection_manager: boolean + has_linkplay_http: boolean + has_arylic_tcp: boolean + has_oh_playlist: boolean + has_oh_volume: boolean + has_oh_info: boolean + has_oh_time: boolean + has_oh_radio: boolean +} + export interface RendererSummary { id: string friendly_name: string model_name: string - protocol: 'UpnpAvOnly' | 'OpenHomeOnly' | 'Hybrid' + protocol: RendererProtocolSummary + capabilities: RendererCapabilitiesSummary online: boolean } @@ -51,6 +68,32 @@ export interface QueueSnapshot { current_index: number | null // Index de la piste en cours (null si rien en lecture) } +// ============================================================================ +// OPENHOME PLAYLIST +// ============================================================================ + +export interface OpenHomePlaylistTrack { + id: number + uri: string + title: string | null + artist: string | null + album: string | null + album_art_uri: string | null +} + +export interface OpenHomePlaylistSnapshot { + renderer_id: string + current_id: number | null + tracks: OpenHomePlaylistTrack[] +} + +export interface OpenHomePlaylistAddRequest { + uri: string + metadata: string + after_id?: number | null + play?: boolean +} + // ============================================================================ // MEDIA SERVERS // ============================================================================ diff --git a/pmoapp/webapp/src/views/RendererView.vue b/pmoapp/webapp/src/views/RendererView.vue index 6f7ba4c7..dc13fa80 100644 --- a/pmoapp/webapp/src/views/RendererView.vue +++ b/pmoapp/webapp/src/views/RendererView.vue @@ -1,5 +1,5 @@