push-pqqsxyupswry #21
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRenderersStore } from '@/stores/renderers'
|
||||
import { usePlaybackStore } from '@/stores/playback'
|
||||
import { Music } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -9,10 +8,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const renderersStore = useRenderersStore()
|
||||
const playbackStore = usePlaybackStore()
|
||||
|
||||
const state = computed(() => renderersStore.getStateById(props.rendererId))
|
||||
const metadata = computed(() => playbackStore.getTrackMetadata(props.rendererId))
|
||||
const metadata = computed(() => state.value?.current_track)
|
||||
|
||||
// Calcul du pourcentage de progression
|
||||
const progressPercent = computed(() => {
|
||||
|
||||
@@ -18,9 +18,17 @@ defineProps<{
|
||||
<!-- Index (1-based pour l'affichage) -->
|
||||
<span class="item-index">{{ item.index + 1 }}</span>
|
||||
|
||||
<!-- Cover miniature (placeholder pour l'instant, on ajoutera la cover plus tard) -->
|
||||
<!-- Cover miniature -->
|
||||
<div class="item-cover">
|
||||
<Music :size="20" />
|
||||
<img
|
||||
v-if="item.album_art_uri"
|
||||
:src="item.album_art_uri"
|
||||
:alt="item.album || 'Album cover'"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@error="(e: Event) => (e.target as HTMLImageElement).style.display = 'none'"
|
||||
/>
|
||||
<Music v-else :size="20" />
|
||||
</div>
|
||||
|
||||
<!-- Métadonnées -->
|
||||
@@ -85,6 +93,13 @@ defineProps<{
|
||||
justify-content: center;
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.item-metadata {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePlaybackStore } from '@/stores/playback'
|
||||
import type {
|
||||
RendererCapabilitiesSummary,
|
||||
RendererSummary,
|
||||
@@ -16,9 +15,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const playbackStore = usePlaybackStore()
|
||||
|
||||
const metadata = computed(() => playbackStore.getTrackMetadata(props.renderer.id))
|
||||
// Métadonnées proviennent directement de l'état du renderer (API + SSE)
|
||||
const metadata = computed(() => props.state?.current_track)
|
||||
|
||||
const protocolLabel = computed(() => {
|
||||
switch (props.renderer.protocol) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import router from "./router";
|
||||
// Stores
|
||||
import { useRenderersStore } from "./stores/renderers";
|
||||
import { useMediaServersStore } from "./stores/mediaServers";
|
||||
import { usePlaybackStore } from "./stores/playback";
|
||||
import { useUIStore } from "./stores/ui";
|
||||
|
||||
// Service SSE
|
||||
@@ -31,13 +30,13 @@ app.mount("#app");
|
||||
// Après montage, initialiser SSE et connecter aux stores
|
||||
const renderersStore = useRenderersStore();
|
||||
const mediaServersStore = useMediaServersStore();
|
||||
const playbackStore = usePlaybackStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
// Connecter SSE aux stores
|
||||
// Note: Les métadonnées proviennent de l'API (current_track dans RendererState)
|
||||
// Les événements SSE ne servent qu'à notifier les changements
|
||||
sse.onRendererEvent((event) => {
|
||||
renderersStore.updateFromSSE(event);
|
||||
playbackStore.updateFromSSE(event);
|
||||
});
|
||||
|
||||
sse.onMediaServerEvent((event) => {
|
||||
|
||||
@@ -40,6 +40,14 @@ export interface RendererState {
|
||||
mute: boolean | null
|
||||
queue_len: number
|
||||
attached_playlist: AttachedPlaylistInfo | null
|
||||
current_track: CurrentTrackMetadata | null
|
||||
}
|
||||
|
||||
export interface CurrentTrackMetadata {
|
||||
title: string | null
|
||||
artist: string | null
|
||||
album: string | null
|
||||
album_art_uri: string | null
|
||||
}
|
||||
|
||||
export interface AttachedPlaylistInfo {
|
||||
@@ -58,6 +66,7 @@ export interface QueueItem {
|
||||
title: string | null
|
||||
artist: string | null
|
||||
album: string | null
|
||||
album_art_uri: string | null
|
||||
server_id: string | null
|
||||
object_id: string | null
|
||||
}
|
||||
|
||||
@@ -298,6 +298,10 @@ export const useRenderersStore = defineStore('renderers', () => {
|
||||
const state = states.value.get(rendererId)
|
||||
if (state) {
|
||||
state.transport_state = event.state as any
|
||||
// Effacer les métadonnées si stopped/no_media
|
||||
if (event.state === 'STOPPED' || event.state === 'NO_MEDIA') {
|
||||
state.current_track = null
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -339,7 +343,15 @@ export const useRenderersStore = defineStore('renderers', () => {
|
||||
}
|
||||
|
||||
case 'metadata_changed': {
|
||||
// Les métadonnées sont gérées par le store playback
|
||||
const state = states.value.get(rendererId)
|
||||
if (state) {
|
||||
state.current_track = {
|
||||
title: event.title,
|
||||
artist: event.artist,
|
||||
album: event.album,
|
||||
album_art_uri: event.album_art_uri
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -697,6 +697,14 @@ impl ControlPoint {
|
||||
.ok_or_else(|| Self::runtime_entry_missing(renderer_id))
|
||||
}
|
||||
|
||||
/// Retourne les métadonnées courantes depuis le snapshot en mémoire
|
||||
pub fn get_current_track_metadata(
|
||||
&self,
|
||||
renderer_id: &RendererId,
|
||||
) -> Option<TrackMetadata> {
|
||||
self.runtime.current_track_metadata(renderer_id)
|
||||
}
|
||||
|
||||
/// Force a resynchronization of the OpenHome playlist cache for a renderer.
|
||||
///
|
||||
/// This is used by external APIs after mutating the native playlist so that
|
||||
@@ -713,6 +721,11 @@ impl ControlPoint {
|
||||
renderer.openhome_playlist_snapshot()
|
||||
}
|
||||
|
||||
pub fn get_openhome_playlist_len(&self, renderer_id: &RendererId) -> anyhow::Result<usize> {
|
||||
let renderer = self.openhome_renderer(renderer_id)?;
|
||||
renderer.openhome_playlist_len()
|
||||
}
|
||||
|
||||
pub fn clear_openhome_playlist(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
|
||||
let renderer = self.openhome_renderer(renderer_id)?;
|
||||
renderer.openhome_playlist_clear()?;
|
||||
@@ -1211,10 +1224,13 @@ impl ControlPoint {
|
||||
}
|
||||
|
||||
let renderer = self.openhome_renderer(renderer_id)?;
|
||||
let mut after_id = self
|
||||
.runtime
|
||||
.queue_full_snapshot(renderer_id)
|
||||
.and_then(|(queue, _)| queue.last().and_then(openhome_track_id_from_item));
|
||||
|
||||
// Get the last track ID from the OpenHome native playlist
|
||||
// This ensures we append to the end of the actual playlist, not just our local queue
|
||||
let mut after_id = renderer
|
||||
.openhome_playlist_ids()
|
||||
.ok()
|
||||
.and_then(|ids| ids.last().copied());
|
||||
|
||||
for item in items.iter() {
|
||||
let metadata = item.to_didl_metadata();
|
||||
@@ -1227,10 +1243,33 @@ impl ControlPoint {
|
||||
}
|
||||
|
||||
fn play_current_openhome(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
|
||||
let renderer = self.openhome_renderer(renderer_id)?;
|
||||
|
||||
// Pour les renderers OpenHome, vérifier d'abord si la playlist native a des pistes
|
||||
// Cela couvre le cas où l'utilisateur a ajouté des morceaux directement via l'interface OpenHome
|
||||
let native_playlist_len = renderer.openhome_playlist_len().unwrap_or(0);
|
||||
|
||||
if native_playlist_len > 0 {
|
||||
// La playlist native a des pistes, on peut simplement appeler play()
|
||||
// Cela reprendra la lecture à partir du morceau actuel (ou du premier si rien n'est en cours)
|
||||
renderer.play()?;
|
||||
self.runtime
|
||||
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
|
||||
self.sync_openhome_playlist_for(renderer_id)?;
|
||||
info!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
playlist_len = native_playlist_len,
|
||||
"Started OpenHome native playlist playback"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fallback: utiliser la PlaybackQueue locale si la playlist native est vide
|
||||
// (ce cas se produit quand on a enqueue des items via le control point)
|
||||
let Some((item, _)) = self.runtime.peek_current(renderer_id) else {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
"OpenHome playlist is empty or no current item"
|
||||
"OpenHome playlist is empty or no current item (both native and queue)"
|
||||
);
|
||||
self.runtime
|
||||
.set_playback_source(renderer_id, PlaybackSource::None);
|
||||
@@ -1239,14 +1278,13 @@ impl ControlPoint {
|
||||
|
||||
let track_id = openhome_track_id_from_item(&item)
|
||||
.ok_or_else(|| anyhow!("Current OpenHome item has no track id"))?;
|
||||
let renderer = self.openhome_renderer(renderer_id)?;
|
||||
renderer.openhome_playlist_play_id(track_id)?;
|
||||
self.runtime
|
||||
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
|
||||
self.sync_openhome_playlist_for(renderer_id)?;
|
||||
info!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
track_id, "Started OpenHome playlist playback (current item)"
|
||||
track_id, "Started OpenHome playlist playback (current item from queue)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1387,6 +1425,13 @@ impl RuntimeState {
|
||||
entries.get(id).map(|entry| entry.queue.full_snapshot())
|
||||
}
|
||||
|
||||
fn current_track_metadata(&self, id: &RendererId) -> Option<TrackMetadata> {
|
||||
let entries = self.entries.lock().unwrap();
|
||||
entries
|
||||
.get(id)
|
||||
.and_then(|entry| entry.snapshot.last_metadata.clone())
|
||||
}
|
||||
|
||||
fn dequeue_next(&self, id: &RendererId) -> Option<(PlaybackItem, usize)> {
|
||||
let mut entries = self.entries.lock().unwrap();
|
||||
let entry = entries.get_mut(id)?;
|
||||
@@ -1747,6 +1792,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
||||
item.date = entry.date.clone();
|
||||
item.track_number = entry.track_number.clone();
|
||||
item.creator = entry.creator.clone();
|
||||
item.protocol_info = Some(resource.protocol_info.clone());
|
||||
|
||||
Some(item)
|
||||
}
|
||||
@@ -2788,6 +2834,13 @@ fn extract_track_metadata(position: &PlaybackPositionInfo) -> Option<TrackMetada
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
title = item.title.as_str(),
|
||||
has_album_art = item.album_art.is_some(),
|
||||
album_art_uri = item.album_art.as_deref(),
|
||||
"Extracted metadata from position info"
|
||||
);
|
||||
|
||||
Some(TrackMetadata {
|
||||
title: Some(item.title.clone()),
|
||||
artist: item.artist.clone(),
|
||||
|
||||
@@ -171,6 +171,26 @@ impl MusicRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openhome_playlist_len(&self) -> Result<usize> {
|
||||
match self {
|
||||
MusicRenderer::OpenHome(renderer) => renderer.openhome_playlist_len(),
|
||||
_ => Err(op_not_supported(
|
||||
"openhome_playlist_len",
|
||||
self.unsupported_backend_name(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openhome_playlist_ids(&self) -> Result<Vec<u32>> {
|
||||
match self {
|
||||
MusicRenderer::OpenHome(renderer) => renderer.openhome_playlist_ids(),
|
||||
_ => Err(op_not_supported(
|
||||
"openhome_playlist_ids",
|
||||
self.unsupported_backend_name(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openhome_playlist_clear(&self) -> Result<()> {
|
||||
match self {
|
||||
MusicRenderer::OpenHome(renderer) => renderer.clear_openhome_playlist(),
|
||||
|
||||
@@ -79,6 +79,22 @@ pub struct RendererState {
|
||||
pub queue_len: usize,
|
||||
/// Playlist attachée (si applicable)
|
||||
pub attached_playlist: Option<AttachedPlaylistInfo>,
|
||||
/// Métadonnées du morceau courant (si en lecture)
|
||||
pub current_track: Option<CurrentTrackMetadata>,
|
||||
}
|
||||
|
||||
/// Métadonnées du morceau en cours de lecture
|
||||
#[cfg(feature = "pmoserver")]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct CurrentTrackMetadata {
|
||||
/// Titre du morceau
|
||||
pub title: Option<String>,
|
||||
/// Artiste
|
||||
pub artist: Option<String>,
|
||||
/// Album
|
||||
pub album: Option<String>,
|
||||
/// URI de la pochette d'album
|
||||
pub album_art_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// Information sur la playlist attachée
|
||||
@@ -111,6 +127,8 @@ pub struct QueueItem {
|
||||
pub artist: Option<String>,
|
||||
/// Album
|
||||
pub album: Option<String>,
|
||||
/// URI de la pochette d'album
|
||||
pub album_art_uri: Option<String>,
|
||||
/// ID du serveur source
|
||||
pub server_id: Option<String>,
|
||||
/// ID de l'objet DIDL-Lite
|
||||
@@ -358,6 +376,7 @@ GET /control/servers/{server_id}/containers/{container_id}
|
||||
RendererProtocolSummary,
|
||||
RendererCapabilitiesSummary,
|
||||
RendererState,
|
||||
CurrentTrackMetadata,
|
||||
AttachedPlaylistInfo,
|
||||
QueueItem,
|
||||
QueueSnapshot,
|
||||
|
||||
@@ -184,7 +184,20 @@ impl OhPlaylistClient {
|
||||
let response = find_child_with_suffix(&envelope.body.content, "IdArrayResponse")
|
||||
.ok_or_else(|| anyhow!("Missing IdArrayResponse element in SOAP body"))?;
|
||||
|
||||
let array_text = extract_child_text_any(response, &["aArray", "aIdArray"])?;
|
||||
// Try to extract the array element. If missing, assume empty playlist.
|
||||
let array_text = match extract_child_text_any(response, &["aArray", "aIdArray"]) {
|
||||
Ok(text) => text,
|
||||
Err(_) => {
|
||||
// Element not found - playlist is likely empty
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Handle empty string (another way renderers indicate empty playlist)
|
||||
if array_text.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let bytes = decode_base64(&array_text)?;
|
||||
if bytes.len() % 4 != 0 {
|
||||
return Err(anyhow!(
|
||||
@@ -231,6 +244,8 @@ impl OhInfoClient {
|
||||
}
|
||||
|
||||
pub fn track(&self) -> Result<OhInfoTrack> {
|
||||
use tracing::debug;
|
||||
|
||||
let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Track", &[])?;
|
||||
|
||||
let envelope = ensure_success("Track", &call_result)?;
|
||||
@@ -242,6 +257,20 @@ impl OhInfoClient {
|
||||
.unwrap_or(None)
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
debug!(
|
||||
uri = uri.as_str(),
|
||||
has_metadata = metadata_xml.is_some(),
|
||||
metadata_length = metadata_xml.as_ref().map(|s| s.len()),
|
||||
"OpenHome Info.Track() returned"
|
||||
);
|
||||
|
||||
if let Some(ref xml) = metadata_xml {
|
||||
debug!(
|
||||
metadata_xml = xml.as_str(),
|
||||
"OpenHome metadata XML content"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(OhInfoTrack { uri, metadata_xml })
|
||||
}
|
||||
|
||||
@@ -423,6 +452,8 @@ impl OhRadioClient {
|
||||
}
|
||||
|
||||
pub fn parse_track_metadata_from_didl(xml: &str) -> Option<TrackMetadata> {
|
||||
use tracing::debug;
|
||||
|
||||
if xml.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -430,6 +461,13 @@ pub fn parse_track_metadata_from_didl(xml: &str) -> Option<TrackMetadata> {
|
||||
let parsed = pmodidl::parse_metadata::<pmodidl::DIDLLite>(xml).ok()?;
|
||||
let item = parsed.data.items.first()?;
|
||||
|
||||
debug!(
|
||||
title = item.title.as_str(),
|
||||
has_album_art = item.album_art.is_some(),
|
||||
album_art_uri = item.album_art.as_deref(),
|
||||
"Parsed DIDL metadata for track"
|
||||
);
|
||||
|
||||
Some(TrackMetadata {
|
||||
title: Some(item.title.clone()),
|
||||
artist: item.artist.clone(),
|
||||
|
||||
@@ -108,6 +108,21 @@ impl OpenHomeRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Retourne la longueur de la playlist OpenHome sans récupérer toutes les métadonnées.
|
||||
/// Plus rapide que snapshot_openhome_playlist() pour juste connaître le nombre de pistes.
|
||||
pub(crate) fn openhome_playlist_len(&self) -> Result<usize> {
|
||||
let playlist = self.playlist_client_for("openhome_playlist_len")?;
|
||||
let ids = playlist.id_array()?;
|
||||
Ok(ids.len())
|
||||
}
|
||||
|
||||
/// Retourne les IDs des pistes de la playlist OpenHome.
|
||||
/// Plus rapide que snapshot_openhome_playlist() car ne récupère pas les métadonnées.
|
||||
pub(crate) fn openhome_playlist_ids(&self) -> Result<Vec<u32>> {
|
||||
let playlist = self.playlist_client_for("openhome_playlist_ids")?;
|
||||
playlist.id_array()
|
||||
}
|
||||
|
||||
pub(crate) fn clear_openhome_playlist(&self) -> Result<()> {
|
||||
let playlist = self.playlist_client_for("clear_openhome_playlist")?;
|
||||
playlist.delete_all()
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct PlaybackItem {
|
||||
pub date: Option<String>,
|
||||
pub track_number: Option<String>,
|
||||
pub creator: Option<String>,
|
||||
pub protocol_info: Option<String>,
|
||||
}
|
||||
|
||||
impl PlaybackItem {
|
||||
@@ -29,12 +30,14 @@ impl PlaybackItem {
|
||||
date: None,
|
||||
track_number: None,
|
||||
creator: None,
|
||||
protocol_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert PlaybackItem to DIDL-Lite XML metadata for SetAVTransportURI
|
||||
pub fn to_didl_metadata(&self) -> String {
|
||||
use quick_xml::escape::escape;
|
||||
use tracing::debug;
|
||||
|
||||
let title = self.title.as_deref().unwrap_or("Unknown");
|
||||
let escaped_uri = escape(&self.uri);
|
||||
@@ -43,7 +46,13 @@ impl PlaybackItem {
|
||||
let mut didl = String::from(
|
||||
r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">"#,
|
||||
);
|
||||
didl.push_str(r#"<item id="0" parentID="-1" restricted="1">"#);
|
||||
// Use proper ID from object_id if available, otherwise use "0"
|
||||
let item_id = self.object_id.as_deref().unwrap_or("0");
|
||||
let escaped_item_id = escape(item_id);
|
||||
didl.push_str(&format!(
|
||||
r#"<item id="{}" parentID="-1" restricted="1">"#,
|
||||
escaped_item_id
|
||||
));
|
||||
didl.push_str(&format!("<dc:title>{}</dc:title>", escaped_title));
|
||||
|
||||
if let Some(artist) = &self.artist {
|
||||
@@ -64,10 +73,20 @@ impl PlaybackItem {
|
||||
|
||||
if let Some(album_art) = &self.album_art_uri {
|
||||
let escaped_art = escape(album_art);
|
||||
debug!(
|
||||
title = title,
|
||||
album_art_uri = album_art.as_str(),
|
||||
"Including albumArtURI in DIDL metadata"
|
||||
);
|
||||
didl.push_str(&format!(
|
||||
"<upnp:albumArtURI>{}</upnp:albumArtURI>",
|
||||
escaped_art
|
||||
));
|
||||
} else {
|
||||
debug!(
|
||||
title = title,
|
||||
"No album_art_uri in PlaybackItem - skipping albumArtURI in DIDL"
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(date) = &self.date {
|
||||
@@ -84,9 +103,23 @@ impl PlaybackItem {
|
||||
}
|
||||
|
||||
// Add resource with URI
|
||||
// Use the original protocolInfo if available, otherwise use a generic one
|
||||
let protocol_info = self
|
||||
.protocol_info
|
||||
.as_deref()
|
||||
.unwrap_or("http-get:*:audio/*:*");
|
||||
|
||||
// For protocolInfo, we only need to escape XML special chars, not ':'
|
||||
// We manually escape only the necessary characters to preserve the protocolInfo format
|
||||
let safe_protocol_info = protocol_info
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """);
|
||||
|
||||
didl.push_str(&format!(
|
||||
r#"<res protocolInfo="http-get:*:audio/*:*">{}</res>"#,
|
||||
escaped_uri
|
||||
r#"<res protocolInfo="{}">{}</res>"#,
|
||||
safe_protocol_info, escaped_uri
|
||||
));
|
||||
|
||||
didl.push_str(r#"<upnp:class>object.item.audioItem.musicTrack</upnp:class>"#);
|
||||
|
||||
@@ -11,10 +11,11 @@ use crate::media_server::{MediaBrowser, MediaEntry, MediaResource, MusicServer,
|
||||
use crate::model::{RendererCapabilities, RendererId, RendererProtocol};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::openapi::{
|
||||
AttachPlaylistRequest, AttachedPlaylistInfo, BrowseResponse, ContainerEntry, ErrorResponse,
|
||||
MediaServerSummary, OpenHomePlaylistAddRequest, OpenHomePlaylistSnapshot, PlayContentRequest,
|
||||
QueueItem, QueueSnapshot, RendererCapabilitiesSummary, RendererProtocolSummary, RendererState,
|
||||
RendererSummary, SuccessResponse, VolumeSetRequest,
|
||||
AttachPlaylistRequest, AttachedPlaylistInfo, BrowseResponse, ContainerEntry,
|
||||
CurrentTrackMetadata, ErrorResponse, MediaServerSummary, OpenHomePlaylistAddRequest,
|
||||
OpenHomePlaylistSnapshot, PlayContentRequest, QueueItem, QueueSnapshot,
|
||||
RendererCapabilitiesSummary, RendererProtocolSummary, RendererState, RendererSummary,
|
||||
SuccessResponse, VolumeSetRequest,
|
||||
};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::playback_queue::PlaybackItem;
|
||||
@@ -150,19 +151,19 @@ async fn get_renderer_state(
|
||||
})?;
|
||||
|
||||
let info = renderer.info();
|
||||
let renderer_clone = renderer.clone();
|
||||
let renderer = renderer.clone();
|
||||
|
||||
// Spawn blocking task for all SOAP calls to avoid blocking Tokio runtime
|
||||
let state_task = tokio::task::spawn_blocking(move || {
|
||||
// État de transport
|
||||
let transport_state = renderer_clone
|
||||
let transport_state = renderer
|
||||
.playback_state()
|
||||
.ok()
|
||||
.map(state_to_string)
|
||||
.unwrap_or_else(|| "UNKNOWN".to_string());
|
||||
|
||||
// Position et durée
|
||||
let (position_ms, duration_ms) = renderer_clone
|
||||
let (position_ms, duration_ms) = renderer
|
||||
.playback_position()
|
||||
.ok()
|
||||
.and_then(|pos| {
|
||||
@@ -173,11 +174,11 @@ async fn get_renderer_state(
|
||||
.unwrap_or((None, None));
|
||||
|
||||
// Volume et mute
|
||||
let volume = renderer_clone
|
||||
let volume = renderer
|
||||
.volume()
|
||||
.ok()
|
||||
.and_then(|v| u8::try_from(v).ok());
|
||||
let mute = renderer_clone.mute().ok();
|
||||
let mute = renderer.mute().ok();
|
||||
|
||||
(transport_state, position_ms, duration_ms, volume, mute)
|
||||
});
|
||||
@@ -211,11 +212,18 @@ async fn get_renderer_state(
|
||||
})?;
|
||||
|
||||
// Queue (non-blocking, local data)
|
||||
// Pour les renderers OpenHome, on utilise la playlist native au lieu de la PlaybackQueue
|
||||
let queue_len = state
|
||||
.control_point
|
||||
.get_queue_snapshot(&rid)
|
||||
.get_openhome_playlist_len(&rid)
|
||||
.ok()
|
||||
.map(|q| q.len())
|
||||
.or_else(|| {
|
||||
state
|
||||
.control_point
|
||||
.get_queue_snapshot(&rid)
|
||||
.ok()
|
||||
.map(|q| q.len())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
// Playlist binding (non-blocking, local data)
|
||||
@@ -230,6 +238,17 @@ async fn get_renderer_state(
|
||||
},
|
||||
);
|
||||
|
||||
// Current track metadata from in-memory snapshot (non-blocking, fast)
|
||||
let current_track = state
|
||||
.control_point
|
||||
.get_current_track_metadata(&rid)
|
||||
.map(|metadata| CurrentTrackMetadata {
|
||||
title: metadata.title,
|
||||
artist: metadata.artist,
|
||||
album: metadata.album,
|
||||
album_art_uri: metadata.album_art_uri,
|
||||
});
|
||||
|
||||
Ok(Json(RendererState {
|
||||
id: info.id.0.clone(),
|
||||
friendly_name: info.friendly_name.clone(),
|
||||
@@ -240,6 +259,7 @@ async fn get_renderer_state(
|
||||
mute,
|
||||
queue_len,
|
||||
attached_playlist,
|
||||
current_track,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -302,6 +322,7 @@ async fn get_renderer_queue(
|
||||
title: item.title,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
album_art_uri: item.album_art_uri,
|
||||
server_id: item.server_id.map(|s| s.0),
|
||||
object_id: item.object_id,
|
||||
})
|
||||
@@ -1512,7 +1533,9 @@ async fn play_content(
|
||||
control_point.enqueue_items(&rid, items.clone())?;
|
||||
|
||||
// Start playback
|
||||
renderer.play()?;
|
||||
// Pour les renderers OpenHome, play_current_from_queue() va gérer automatiquement
|
||||
// la lecture depuis la playlist native si elle existe
|
||||
control_point.play_current_from_queue(&rid)?;
|
||||
|
||||
// Auto-bind if playing a container (playlist/album)
|
||||
// Rule: if multiple items, it's a container that should be bound
|
||||
@@ -1943,6 +1966,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
||||
item.date = entry.date.clone();
|
||||
item.track_number = entry.track_number.clone();
|
||||
item.creator = entry.creator.clone();
|
||||
item.protocol_info = Some(resource.protocol_info.clone());
|
||||
|
||||
Some(item)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user