Correction de petits bugs d'interface

This commit is contained in:
2026-01-04 18:54:31 +01:00
parent c53ccf2f93
commit 7546072d2b
18 changed files with 4776 additions and 1895 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,173 +1,260 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue";
import type { ContainerEntry } from '@/services/pmocontrol/types' import type { ContainerEntry } from "@/services/pmocontrol/types";
import { Folder, Music } from 'lucide-vue-next' import { Folder, Music } from "lucide-vue-next";
import ActionMenu from './ActionMenu.vue' import ActionMenu from "./ActionMenu.vue";
const props = defineProps<{ const props = defineProps<{
entry: ContainerEntry entry: ContainerEntry;
serverId: string serverId: string;
showActions?: boolean showActions?: boolean;
}>() }>();
const emit = defineEmits<{ const emit = defineEmits<{
browse: [containerId: string] browse: [containerId: string];
playNow: [containerId: string, rendererId: string] playNow: [containerId: string, rendererId: string];
addToQueue: [containerId: string, rendererId: string] addToQueue: [containerId: string, rendererId: string];
}>() }>();
const iconComponent = computed(() => { const iconComponent = computed(() => {
const cls = props.entry.class.toLowerCase() const cls = props.entry.class.toLowerCase();
if (cls.includes('playlist')) return Music if (cls.includes("playlist")) return Music;
if (cls.includes('album')) return Music if (cls.includes("album")) return Music;
return Folder return Folder;
}) });
const containerType = computed(() => { const containerType = computed(() => {
const cls = props.entry.class.toLowerCase() const cls = props.entry.class.toLowerCase();
if (cls.includes('playlist')) return 'Playlist' if (cls.includes("playlist")) return "Playlist";
if (cls.includes('album')) return 'Album' if (cls.includes("album")) return "Album";
if (cls.includes('artist')) return 'Artiste' if (cls.includes("artist")) return "Artiste";
if (cls.includes('genre')) return 'Genre' if (cls.includes("genre")) return "Genre";
return 'Dossier' return "Dossier";
}) });
const isPlayable = computed(() => {
const cls = props.entry.class.toLowerCase();
return cls.includes("playlist") || cls.includes("album");
});
function handleBrowse() { function handleBrowse() {
emit('browse', props.entry.id) emit("browse", props.entry.id);
} }
function handlePlayNow(rendererId: string) { function handlePlayNow(rendererId: string) {
emit('playNow', props.entry.id, rendererId) emit("playNow", props.entry.id, rendererId);
} }
function handleAddToQueue(rendererId: string) { function handleAddToQueue(rendererId: string) {
emit('addToQueue', props.entry.id, rendererId) emit("addToQueue", props.entry.id, rendererId);
}
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
img.style.display = "none";
const placeholder = img.nextElementSibling;
if (placeholder && placeholder instanceof HTMLElement) {
placeholder.style.display = "flex";
}
} }
</script> </script>
<template> <template>
<div class="container-item"> <div class="container-item">
<!-- Main content (clickable) --> <!-- Main content (clickable) -->
<button class="container-content" @click="handleBrowse"> <button class="container-content" @click="handleBrowse">
<div class="container-icon"> <!-- Cover avec icône de type en overlay -->
<component :is="iconComponent" :size="24" /> <div class="container-cover">
</div> <img
<div class="container-metadata"> v-if="entry.album_art_uri"
<div class="container-title">{{ entry.title }}</div> :src="entry.album_art_uri"
<div class="container-details"> :alt="entry.title"
<span class="container-type">{{ containerType }}</span> class="cover-image"
<span v-if="entry.child_count !== null" class="container-count"> loading="lazy"
{{ entry.child_count }} élément{{ entry.child_count > 1 ? 's' : '' }} @error="handleImageError"
</span> />
</div> <div
</div> class="cover-placeholder"
</button> :style="{
display: entry.album_art_uri ? 'none' : 'flex',
}"
>
<component :is="iconComponent" :size="28" />
</div>
<!-- Petite icône de type dans le coin inférieur droit -->
<div v-if="isPlayable" class="type-badge">
<Folder :size="14" />
</div>
</div>
<!-- Actions menu --> <!-- Métadonnées -->
<div class="container-actions"> <div class="container-metadata">
<ActionMenu <div class="container-title">{{ entry.title }}</div>
type="container" <div class="container-details">
:entry-id="entry.id" <span v-if="entry.artist" class="container-artist">{{
:server-id="serverId" entry.artist
@play-now="handlePlayNow" }}</span>
@add-to-queue="handleAddToQueue" <span class="container-type">{{ containerType }}</span>
/> <span
v-if="entry.child_count !== null"
class="container-count"
>
{{ entry.child_count }} élément{{
entry.child_count > 1 ? "s" : ""
}}
</span>
</div>
</div>
</button>
<!-- Actions menu -->
<div class="container-actions">
<ActionMenu
type="container"
:entry-id="entry.id"
:server-id="serverId"
@play-now="handlePlayNow"
@add-to-queue="handleAddToQueue"
/>
</div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
.container-item { .container-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-sm); gap: var(--spacing-sm);
padding: var(--spacing-sm); padding: var(--spacing-sm);
border-radius: var(--radius-md); border-radius: var(--radius-md);
transition: background-color var(--transition-fast); transition: background-color var(--transition-fast);
border: 1px solid transparent; border: 1px solid transparent;
} }
.container-item:hover { .container-item:hover {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border-color: var(--color-border); border-color: var(--color-border);
} }
.container-content { .container-content {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-md); gap: var(--spacing-md);
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
min-width: 0; min-width: 0;
} }
.container-icon { /* Cover avec image et icône de type */
flex-shrink: 0; .container-cover {
width: 48px; position: relative;
height: 48px; flex-shrink: 0;
display: flex; width: 64px;
align-items: center; height: 64px;
justify-content: center; border-radius: var(--radius-md);
background-color: var(--color-bg-tertiary); overflow: hidden;
border-radius: var(--radius-sm); background-color: var(--color-bg-tertiary);
color: var(--color-primary);
} }
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-primary);
}
.type-badge {
position: absolute;
bottom: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
/* Métadonnées */
.container-metadata { .container-metadata {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
} }
.container-title { .container-title {
font-size: var(--text-base); font-size: var(--text-base);
font-weight: 600; font-weight: 600;
color: var(--color-text); color: var(--color-text);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-bottom: var(--spacing-xs); line-height: 1.3;
} }
.container-details { .container-details {
display: flex; display: flex;
gap: var(--spacing-sm); flex-wrap: wrap;
font-size: var(--text-sm); gap: var(--spacing-xs) var(--spacing-sm);
color: var(--color-text-secondary); font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: 1.4;
}
.container-artist {
font-weight: 500;
color: var(--color-text);
} }
.container-type { .container-type {
font-weight: 500; font-weight: 500;
} }
.container-count::before { .container-count::before {
content: '•'; content: "•";
margin-right: var(--spacing-sm); margin-right: var(--spacing-sm);
} }
.container-actions { .container-actions {
flex-shrink: 0; flex-shrink: 0;
} }
.btn-icon { .btn-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 36px; width: 36px;
height: 36px; height: 36px;
background: none; background: none;
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
} }
.btn-icon:hover { .btn-icon:hover {
background-color: var(--color-bg-tertiary); background-color: var(--color-bg-tertiary);
color: var(--color-text); color: var(--color-text);
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ export interface PlaylistSummary {
persistent: boolean; persistent: boolean;
cover_pk?: string | null; cover_pk?: string | null;
cover_url?: string | null; cover_url?: string | null;
artist?: string | null;
track_count: number; track_count: number;
max_size?: number | null; max_size?: number | null;
default_ttl_secs?: number | null; default_ttl_secs?: number | null;
@@ -49,6 +50,7 @@ export interface UpdatePlaylistPayload {
max_size?: number | null; max_size?: number | null;
default_ttl_secs?: number | null; default_ttl_secs?: number | null;
cover_pk?: string | null; cover_pk?: string | null;
artist?: string | null;
} }
export interface AddTracksPayload { export interface AddTracksPayload {
@@ -99,7 +101,9 @@ export async function getPlaylistDetail(id: string): Promise<PlaylistDetail> {
return parseJsonOrThrow(response); return parseJsonOrThrow(response);
} }
export async function createPlaylist(body: CreatePlaylistPayload): Promise<PlaylistDetail> { export async function createPlaylist(
body: CreatePlaylistPayload,
): Promise<PlaylistDetail> {
const response = await fetch("/api/playlists", { const response = await fetch("/api/playlists", {
method: "POST", method: "POST",
headers: { headers: {
@@ -112,7 +116,7 @@ export async function createPlaylist(body: CreatePlaylistPayload): Promise<Playl
export async function updatePlaylist( export async function updatePlaylist(
id: string, id: string,
body: UpdatePlaylistPayload body: UpdatePlaylistPayload,
): Promise<PlaylistDetail> { ): Promise<PlaylistDetail> {
const response = await fetch(`/api/playlists/${encodeURIComponent(id)}`, { const response = await fetch(`/api/playlists/${encodeURIComponent(id)}`, {
method: "PATCH", method: "PATCH",
@@ -133,31 +137,40 @@ export async function deletePlaylist(id: string): Promise<void> {
export async function addTracksToPlaylist( export async function addTracksToPlaylist(
id: string, id: string,
payload: AddTracksPayload payload: AddTracksPayload,
): Promise<PlaylistDetail> { ): Promise<PlaylistDetail> {
const response = await fetch(`/api/playlists/${encodeURIComponent(id)}/tracks`, { const response = await fetch(
method: "POST", `/api/playlists/${encodeURIComponent(id)}/tracks`,
headers: { {
"Content-Type": "application/json", method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}, },
body: JSON.stringify(payload), );
});
return parseJsonOrThrow(response); return parseJsonOrThrow(response);
} }
export async function flushPlaylist(id: string): Promise<PlaylistDetail> { export async function flushPlaylist(id: string): Promise<PlaylistDetail> {
const response = await fetch(`/api/playlists/${encodeURIComponent(id)}/tracks`, { const response = await fetch(
method: "DELETE", `/api/playlists/${encodeURIComponent(id)}/tracks`,
}); {
method: "DELETE",
},
);
return parseJsonOrThrow(response); return parseJsonOrThrow(response);
} }
export async function removeTrackFromPlaylist(id: string, cachePk: string): Promise<PlaylistDetail> { export async function removeTrackFromPlaylist(
id: string,
cachePk: string,
): Promise<PlaylistDetail> {
const response = await fetch( const response = await fetch(
`/api/playlists/${encodeURIComponent(id)}/tracks/${encodeURIComponent(cachePk)}`, `/api/playlists/${encodeURIComponent(id)}/tracks/${encodeURIComponent(cachePk)}`,
{ {
method: "DELETE", method: "DELETE",
} },
); );
return parseJsonOrThrow(response); return parseJsonOrThrow(response);
} }

View File

@@ -141,6 +141,20 @@ pub struct Container {
#[serde(rename = "upnp:class", alias = "class", default)] #[serde(rename = "upnp:class", alias = "class", default)]
pub class: String, pub class: String,
#[serde(
rename = "upnp:artist",
alias = "artist",
skip_serializing_if = "Option::is_none"
)]
pub artist: Option<String>,
#[serde(
rename = "upnp:albumArtURI",
alias = "albumArtURI",
skip_serializing_if = "Option::is_none"
)]
pub album_art: Option<String>,
#[serde(rename = "container", default)] #[serde(rename = "container", default)]
pub containers: Vec<Container>, pub containers: Vec<Container>,

View File

@@ -474,6 +474,8 @@ impl ContentHandler {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "PMOMusic".to_string(), title: "PMOMusic".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }

View File

@@ -252,6 +252,8 @@ mod tests {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: self.name.clone(), title: self.name.clone(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })

View File

@@ -364,6 +364,8 @@ impl RadioParadiseSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: descriptor.display_name.to_string(), title: descriptor.display_name.to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -379,6 +381,8 @@ impl RadioParadiseSource {
searchable: Some("0".to_string()), searchable: Some("0".to_string()),
title: format!("{} - Live Playlist", descriptor.display_name), title: format!("{} - Live Playlist", descriptor.display_name),
class: "object.container.playlistContainer".to_string(), class: "object.container.playlistContainer".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -435,6 +439,8 @@ impl RadioParadiseSource {
title: format!("{} - History", descriptor.display_name), title: format!("{} - History", descriptor.display_name),
// Expose l'historique comme une playlist jouable // Expose l'historique comme une playlist jouable
class: "object.container.playlistContainer".to_string(), class: "object.container.playlistContainer".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -650,6 +656,8 @@ impl MusicSource for RadioParadiseSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Radio Paradise".to_string(), title: "Radio Paradise".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })

View File

@@ -47,6 +47,8 @@ pub struct PlaylistSummaryResponse {
pub cover_pk: Option<String>, pub cover_pk: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub cover_url: Option<String>, pub cover_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artist: Option<String>,
pub track_count: usize, pub track_count: usize,
pub max_size: Option<usize>, pub max_size: Option<usize>,
pub default_ttl_secs: Option<u64>, pub default_ttl_secs: Option<u64>,
@@ -103,6 +105,8 @@ pub struct UpdatePlaylistRequest {
pub default_ttl_secs: Option<Option<u64>>, pub default_ttl_secs: Option<Option<u64>>,
/// Utiliser `null` explicite pour supprimer la cover, ou omettre pour ne pas modifier. /// Utiliser `null` explicite pour supprimer la cover, ou omettre pour ne pas modifier.
pub cover_pk: Option<Option<String>>, pub cover_pk: Option<Option<String>>,
/// Utiliser `null` explicite pour supprimer l'artiste, ou omettre pour ne pas modifier.
pub artist: Option<Option<String>>,
} }
/// Requête pour ajouter des morceaux dans une playlist. /// Requête pour ajouter des morceaux dans une playlist.
@@ -262,6 +266,7 @@ pub async fn update_playlist(
max_size, max_size,
default_ttl_secs, default_ttl_secs,
cover_pk, cover_pk,
artist,
} = req; } = req;
let manager = crate::manager::PlaylistManager(); let manager = crate::manager::PlaylistManager();
@@ -289,6 +294,9 @@ pub async fn update_playlist(
}; };
writer.set_cover_pk(normalized).await?; writer.set_cover_pk(normalized).await?;
} }
if let Some(artist) = artist {
writer.set_artist(artist).await?;
}
manager.playlist_snapshot(&playlist_id).await manager.playlist_snapshot(&playlist_id).await
} }
@@ -545,6 +553,7 @@ impl From<PlaylistOverview> for PlaylistSummaryResponse {
persistent: value.persistent, persistent: value.persistent,
cover_pk: cover_pk.clone(), cover_pk: cover_pk.clone(),
cover_url: cover_pk.as_deref().map(cover_url_from_pk), cover_url: cover_pk.as_deref().map(cover_url_from_pk),
artist: value.artist,
track_count: value.track_count, track_count: value.track_count,
max_size: value.max_size, max_size: value.max_size,
default_ttl_secs: value.default_ttl.map(|ttl| ttl.as_secs()), default_ttl_secs: value.default_ttl.map(|ttl| ttl.as_secs()),

View File

@@ -69,6 +69,7 @@ impl ReadHandle {
let title = self.playlist.title().await; let title = self.playlist.title().await;
let role = self.playlist.role().await; let role = self.playlist.role().await;
let cover_pk = self.playlist.cover_pk().await; let cover_pk = self.playlist.cover_pk().await;
let artist = self.playlist.artist().await;
let core = self.playlist.core.read().await; let core = self.playlist.core.read().await;
let _ = persistence let _ = persistence
.save_playlist( .save_playlist(
@@ -76,6 +77,7 @@ impl ReadHandle {
&title, &title,
&role, &role,
cover_pk.as_deref(), cover_pk.as_deref(),
artist.as_deref(),
&core.config, &core.config,
&core.tracks, &core.tracks,
) )
@@ -175,8 +177,13 @@ impl ReadHandle {
} }
let title = self.playlist.title().await; let title = self.playlist.title().await;
let artist = self.playlist.artist().await;
let cover_pk = self.playlist.cover_pk().await;
let _remaining = self.remaining().await?; let _remaining = self.remaining().await?;
// Convertir cover_pk en URL si présent
let album_art = cover_pk.map(|pk| format!("/cover/{}", pk));
Ok(Container { Ok(Container {
id: self.playlist.id.clone(), id: self.playlist.id.clone(),
parent_id: "0".to_string(), parent_id: "0".to_string(),
@@ -185,6 +192,8 @@ impl ReadHandle {
searchable: Some("0".to_string()), searchable: Some("0".to_string()),
title, title,
class: "object.container.playlistContainer".to_string(), class: "object.container.playlistContainer".to_string(),
artist,
album_art,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })

View File

@@ -309,6 +309,23 @@ impl WriteHandle {
Ok(()) Ok(())
} }
/// Met à jour l'artiste associé à la playlist.
pub async fn set_artist(&self, artist: Option<String>) -> Result<()> {
if !self.playlist.is_alive() {
return Err(crate::Error::PlaylistDeleted(self.playlist.id.clone()));
}
self.playlist.set_artist(artist).await;
if self.playlist.persistent {
self.save_to_db().await?;
}
crate::manager::PlaylistManager().notify_playlist_changed(&self.playlist.id);
Ok(())
}
/// Vérifie si la playlist contient déjà un pk /// Vérifie si la playlist contient déjà un pk
pub async fn contains_pk(&self, cache_pk: &str) -> Result<bool> { pub async fn contains_pk(&self, cache_pk: &str) -> Result<bool> {
if !self.playlist.is_alive() { if !self.playlist.is_alive() {
@@ -549,6 +566,7 @@ impl WriteHandle {
let tracks = &core.tracks; let tracks = &core.tracks;
let cover_pk = self.playlist.cover_pk().await; let cover_pk = self.playlist.cover_pk().await;
let artist = self.playlist.artist().await;
persistence persistence
.save_playlist( .save_playlist(
@@ -556,6 +574,7 @@ impl WriteHandle {
&title, &title,
&role, &role,
cover_pk.as_deref(), cover_pk.as_deref(),
artist.as_deref(),
config, config,
tracks, tracks,
) )

View File

@@ -71,6 +71,7 @@ pub struct PlaylistOverview {
pub role: PlaylistRole, pub role: PlaylistRole,
pub persistent: bool, pub persistent: bool,
pub cover_pk: Option<String>, pub cover_pk: Option<String>,
pub artist: Option<String>,
pub track_count: usize, pub track_count: usize,
pub max_size: Option<usize>, pub max_size: Option<usize>,
pub default_ttl: Option<Duration>, pub default_ttl: Option<Duration>,
@@ -205,6 +206,7 @@ impl PlaylistManager {
let title = playlist.title().await; let title = playlist.title().await;
let role = playlist.role().await; let role = playlist.role().await;
let cover_pk = playlist.cover_pk().await; let cover_pk = playlist.cover_pk().await;
let artist = playlist.artist().await;
let core = playlist.core.read().await; let core = playlist.core.read().await;
persistence persistence
.save_playlist( .save_playlist(
@@ -212,6 +214,7 @@ impl PlaylistManager {
&title, &title,
&role, &role,
cover_pk.as_deref(), cover_pk.as_deref(),
artist.as_deref(),
&core.config, &core.config,
&core.tracks, &core.tracks,
) )
@@ -519,7 +522,7 @@ impl PlaylistManager {
// Pas en mémoire, essayer de charger depuis la DB // Pas en mémoire, essayer de charger depuis la DB
if let Some(persistence) = &self.inner.persistence { if let Some(persistence) = &self.inner.persistence {
if let Some((title, role, config, cover_pk, tracks)) = if let Some((title, role, config, cover_pk, artist, tracks)) =
persistence.load_playlist(&id).await? persistence.load_playlist(&id).await?
{ {
// Reconstruire la playlist // Reconstruire la playlist
@@ -534,6 +537,11 @@ impl PlaylistManager {
cover_pk, cover_pk,
)); ));
// Restaurer l'artiste si présent
if let Some(artist_name) = artist {
playlist.set_artist(Some(artist_name)).await;
}
// Restaurer les tracks // Restaurer les tracks
{ {
let mut core = playlist.core.write().await; let mut core = playlist.core.write().await;
@@ -575,7 +583,7 @@ impl PlaylistManager {
// Pas en m<>moire, essayer de ressusciter depuis la DB // Pas en m<>moire, essayer de ressusciter depuis la DB
if let Some(persistence) = &self.inner.persistence { if let Some(persistence) = &self.inner.persistence {
if let Some((title, role, config, cover_pk, tracks)) = if let Some((title, role, config, cover_pk, artist, tracks)) =
persistence.load_playlist(id).await? persistence.load_playlist(id).await?
{ {
// Reconstruire la playlist // Reconstruire la playlist
@@ -590,6 +598,11 @@ impl PlaylistManager {
cover_pk, cover_pk,
)); ));
// Restaurer l'artiste si présent
if let Some(artist_name) = artist {
playlist.set_artist(Some(artist_name)).await;
}
// Restaurer les tracks // Restaurer les tracks
{ {
let mut core = playlist.core.write().await; let mut core = playlist.core.write().await;
@@ -653,12 +666,15 @@ impl PlaylistManager {
let track_count = core.len(); let track_count = core.len();
let config = core.config.clone(); let config = core.config.clone();
let artist = playlist.artist().await;
Ok(PlaylistOverview { Ok(PlaylistOverview {
id: playlist.id.clone(), id: playlist.id.clone(),
title, title,
role, role,
persistent, persistent,
cover_pk, cover_pk,
artist,
track_count, track_count,
max_size: config.max_size, max_size: config.max_size,
default_ttl: config.default_ttl, default_ttl: config.default_ttl,
@@ -978,6 +994,7 @@ impl PlaylistManager {
let title = playlist.title().await; let title = playlist.title().await;
let role = playlist.role().await; let role = playlist.role().await;
let cover_pk = playlist.cover_pk().await; let cover_pk = playlist.cover_pk().await;
let artist = playlist.artist().await;
let core = playlist.core.read().await; let core = playlist.core.read().await;
let _ = persistence let _ = persistence
.save_playlist( .save_playlist(
@@ -985,6 +1002,7 @@ impl PlaylistManager {
&title, &title,
&role, &role,
cover_pk.as_deref(), cover_pk.as_deref(),
artist.as_deref(),
&core.config, &core.config,
&core.tracks, &core.tracks,
) )

View File

@@ -37,6 +37,7 @@ impl PersistenceManager {
title TEXT NOT NULL, title TEXT NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
cover_pk TEXT, cover_pk TEXT,
artist TEXT,
max_size INTEGER, max_size INTEGER,
default_ttl_secs INTEGER, default_ttl_secs INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
@@ -86,6 +87,7 @@ impl PersistenceManager {
title: &str, title: &str,
role: &PlaylistRole, role: &PlaylistRole,
cover_pk: Option<&str>, cover_pk: Option<&str>,
artist: Option<&str>,
config: &PlaylistConfig, config: &PlaylistConfig,
tracks: &VecDeque<Arc<Record>>, tracks: &VecDeque<Arc<Record>>,
) -> Result<()> { ) -> Result<()> {
@@ -98,15 +100,16 @@ impl PersistenceManager {
// Upsert playlist metadata // Upsert playlist metadata
conn.execute( conn.execute(
"INSERT OR REPLACE INTO playlists (id, title, role, cover_pk, max_size, default_ttl_secs, created_at, last_modified) "INSERT OR REPLACE INTO playlists (id, title, role, cover_pk, artist, max_size, default_ttl_secs, created_at, last_modified)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7,
COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?7), COALESCE((SELECT created_at FROM playlists WHERE id = ?1), ?8),
?7)", ?8)",
params![ params![
id, id,
title, title,
role.as_str(), role.as_str(),
cover_pk, cover_pk,
artist,
config.max_size.map(|s| s as i64), config.max_size.map(|s| s as i64),
config.default_ttl.map(|d| d.as_secs() as i64), config.default_ttl.map(|d| d.as_secs() as i64),
now_nanos, now_nanos,
@@ -150,6 +153,7 @@ impl PersistenceManager {
PlaylistRole, PlaylistRole,
PlaylistConfig, PlaylistConfig,
Option<String>, Option<String>,
Option<String>,
VecDeque<Arc<Record>>, VecDeque<Arc<Record>>,
)>, )>,
> { > {
@@ -157,7 +161,7 @@ impl PersistenceManager {
// Charger les métadonnées // Charger les métadonnées
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT title, role, cover_pk, max_size, default_ttl_secs FROM playlists WHERE id = ?1", "SELECT title, role, cover_pk, artist, max_size, default_ttl_secs FROM playlists WHERE id = ?1",
) )
.map_err(|e| { .map_err(|e| {
crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e)) crate::Error::PersistenceError(format!("Failed to prepare statement: {}", e))
@@ -167,8 +171,9 @@ impl PersistenceManager {
let title: String = row.get(0)?; let title: String = row.get(0)?;
let role_raw: String = row.get(1)?; let role_raw: String = row.get(1)?;
let cover_pk: Option<String> = row.get(2)?; let cover_pk: Option<String> = row.get(2)?;
let max_size: Option<i64> = row.get(3)?; let artist: Option<String> = row.get(3)?;
let default_ttl_secs: Option<i64> = row.get(4)?; let max_size: Option<i64> = row.get(4)?;
let default_ttl_secs: Option<i64> = row.get(5)?;
Ok(( Ok((
title, title,
@@ -179,10 +184,11 @@ impl PersistenceManager {
default_ttl: default_ttl_secs.map(|s| Duration::from_secs(s as u64)), default_ttl: default_ttl_secs.map(|s| Duration::from_secs(s as u64)),
}, },
cover_pk, cover_pk,
artist,
)) ))
}); });
let (title, role, config, cover_pk) = match result { let (title, role, config, cover_pk, artist) = match result {
Ok(data) => data, Ok(data) => data,
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => { Err(e) => {
@@ -225,7 +231,7 @@ impl PersistenceManager {
tracks.push_back(Arc::new(record)); tracks.push_back(Arc::new(record));
} }
Ok(Some((title, role, config, cover_pk, tracks))) Ok(Some((title, role, config, cover_pk, artist, tracks)))
} }
/// Supprime une playlist /// Supprime une playlist

View File

@@ -35,6 +35,7 @@ pub struct Playlist {
title: RwLock<String>, title: RwLock<String>,
role: RwLock<PlaylistRole>, role: RwLock<PlaylistRole>,
cover_pk: RwLock<Option<String>>, cover_pk: RwLock<Option<String>>,
artist: RwLock<Option<String>>,
state: Arc<AtomicU8>, state: Arc<AtomicU8>,
pub core: Arc<RwLock<PlaylistCore>>, pub core: Arc<RwLock<PlaylistCore>>,
pub persistent: bool, pub persistent: bool,
@@ -57,6 +58,7 @@ impl Playlist {
title: RwLock::new(title), title: RwLock::new(title),
role: RwLock::new(role), role: RwLock::new(role),
cover_pk: RwLock::new(cover_pk), cover_pk: RwLock::new(cover_pk),
artist: RwLock::new(None),
state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)), state: Arc::new(AtomicU8::new(PlaylistState::Active as u8)),
core: Arc::new(RwLock::new(PlaylistCore::new(config))), core: Arc::new(RwLock::new(PlaylistCore::new(config))),
persistent, persistent,
@@ -114,6 +116,17 @@ impl Playlist {
self.touch().await; self.touch().await;
} }
/// Retourne l'artiste associé à la playlist.
pub async fn artist(&self) -> Option<String> {
self.artist.read().await.clone()
}
/// Modifie l'artiste de la playlist.
pub async fn set_artist(&self, value: Option<String>) {
*self.artist.write().await = value;
self.touch().await;
}
/// Timestamp du dernier changement /// Timestamp du dernier changement
pub async fn last_change(&self) -> SystemTime { pub async fn last_change(&self) -> SystemTime {
*self.last_change.read().await *self.last_change.read().await

View File

@@ -40,6 +40,8 @@ impl ToDIDL for Album {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: self.formatted_title(), title: self.formatted_title(),
class: "object.container.album.musicAlbum".to_string(), class: "object.container.album.musicAlbum".to_string(),
artist: Some(self.artist.name.clone()),
album_art: self.image_cached.clone().or_else(|| self.image.clone()),
containers: Vec::new(), containers: Vec::new(),
items: Vec::new(), items: Vec::new(),
}) })
@@ -138,6 +140,8 @@ impl ToDIDL for Playlist {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: self.name.clone(), title: self.name.clone(),
class: "object.container.playlistContainer".to_string(), class: "object.container.playlistContainer".to_string(),
album_art: self.image_cached.clone().or_else(|| self.image.clone()),
artist: self.owner.as_ref().map(|o| o.name.clone()),
containers: Vec::new(), containers: Vec::new(),
items: Vec::new(), items: Vec::new(),
}) })

View File

@@ -568,7 +568,8 @@ impl QobuzSource {
for mut item in items { for mut item in items {
// Extraire cache_pk depuis l'URL du resource // Extraire cache_pk depuis l'URL du resource
let cache_pk = if let Some(resource) = item.resources.first() { let cache_pk = if let Some(resource) = item.resources.first() {
resource.url resource
.url
.strip_prefix("/audio/flac/") .strip_prefix("/audio/flac/")
.map(|s| s.to_string()) .map(|s| s.to_string())
} else { } else {
@@ -577,7 +578,11 @@ impl QobuzSource {
if let Some(pk) = cache_pk { if let Some(pk) = cache_pk {
// Récupérer track_id depuis metadata // Récupérer track_id depuis metadata
if let Ok(Some(track_id_value)) = self.inner.cache_manager.get_audio_metadata(&pk, "qobuz_track_id") { if let Ok(Some(track_id_value)) = self
.inner
.cache_manager
.get_audio_metadata(&pk, "qobuz_track_id")
{
if let Some(track_id) = track_id_value.as_str() { if let Some(track_id) = track_id_value.as_str() {
item.id = format!("qobuz:track:{}", track_id); item.id = format!("qobuz:track:{}", track_id);
} else { } else {
@@ -647,11 +652,7 @@ impl QobuzSource {
// 2. Cache cover // 2. Cache cover
let cover_pk = if let Some(ref image_url) = album.image { let cover_pk = if let Some(ref image_url) = album.image {
self.inner self.inner.cache_manager.cache_cover(image_url).await.ok()
.cache_manager
.cache_cover(image_url)
.await
.ok()
} else { } else {
None None
}; };
@@ -685,6 +686,11 @@ impl QobuzSource {
.await .await
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
writer
.set_artist(Some(album.artist.name.clone()))
.await
.map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?;
if let Some(pk) = cover_pk { if let Some(pk) = cover_pk {
writer writer
.set_cover_pk(Some(pk)) .set_cover_pk(Some(pk))
@@ -731,6 +737,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Discover Catalog".to_string(), title: "Discover Catalog".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -746,6 +754,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Discover Genres".to_string(), title: "Discover Genres".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -761,6 +771,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "My Music".to_string(), title: "My Music".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -776,6 +788,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Albums".to_string(), title: "Albums".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -791,6 +805,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Tracks".to_string(), title: "Tracks".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -806,6 +822,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Artists".to_string(), title: "Artists".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -821,6 +839,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Playlists".to_string(), title: "Playlists".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -892,6 +912,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: artist.name.clone(), title: artist.name.clone(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: Some(artist.name.clone()),
album_art: artist.image_cached.clone().or_else(|| artist.image.clone()),
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })
@@ -942,6 +964,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: tag.display_name().to_string(), title: tag.display_name().to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}); });
@@ -959,6 +983,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Playlists".to_string(), title: "Playlists".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -973,6 +999,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Albums (Ideal Discography)".to_string(), title: "Albums (Ideal Discography)".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -987,6 +1015,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Albums (Qobuzissime)".to_string(), title: "Albums (Qobuzissime)".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1001,6 +1031,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Albums (New Releases)".to_string(), title: "Albums (New Releases)".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1015,6 +1047,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Artists".to_string(), title: "Artists".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1065,7 +1099,11 @@ impl QobuzSource {
let containers: Vec<Container> = albums let containers: Vec<Container> = albums
.into_iter() .into_iter()
.filter_map(|album| album.to_didl_container("qobuz:discover:albums:qobuzissime").ok()) .filter_map(|album| {
album
.to_didl_container("qobuz:discover:albums:qobuzissime")
.ok()
})
.collect(); .collect();
Ok(BrowseResult::Containers(containers)) Ok(BrowseResult::Containers(containers))
@@ -1107,6 +1145,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: artist.name.clone(), title: artist.name.clone(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: Some(artist.name.clone()),
album_art: artist.image_cached.clone().or_else(|| artist.image.clone()),
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })
@@ -1156,6 +1196,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: genre.name.clone(), title: genre.name.clone(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })
@@ -1188,6 +1230,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "New Releases".to_string(), title: "New Releases".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1202,6 +1246,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Ideal Discography".to_string(), title: "Ideal Discography".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1216,6 +1262,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Qobuzissime".to_string(), title: "Qobuzissime".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1230,6 +1278,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Editor Picks".to_string(), title: "Editor Picks".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1244,6 +1294,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Press Awards".to_string(), title: "Press Awards".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1258,6 +1310,8 @@ impl QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Qobuz Playlists".to_string(), title: "Qobuz Playlists".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
} }
@@ -1392,19 +1446,29 @@ impl QobuzSource {
["qobuz", "discover"] => ObjectIdType::DiscoverCatalog, ["qobuz", "discover"] => ObjectIdType::DiscoverCatalog,
["qobuz", "discover", "playlists"] => ObjectIdType::DiscoverPlaylists, ["qobuz", "discover", "playlists"] => ObjectIdType::DiscoverPlaylists,
["qobuz", "discover", "albums", "ideal"] => ObjectIdType::DiscoverAlbumsIdeal, ["qobuz", "discover", "albums", "ideal"] => ObjectIdType::DiscoverAlbumsIdeal,
["qobuz", "discover", "albums", "qobuzissime"] => ObjectIdType::DiscoverAlbumsQobuzissime, ["qobuz", "discover", "albums", "qobuzissime"] => {
ObjectIdType::DiscoverAlbumsQobuzissime
}
["qobuz", "discover", "albums", "new"] => ObjectIdType::DiscoverAlbumsNew, ["qobuz", "discover", "albums", "new"] => ObjectIdType::DiscoverAlbumsNew,
["qobuz", "discover", "artists"] => ObjectIdType::DiscoverArtists, ["qobuz", "discover", "artists"] => ObjectIdType::DiscoverArtists,
["qobuz", "discover", "playlists", tag] => ObjectIdType::DiscoverPlaylistsByTag(tag.to_string()), ["qobuz", "discover", "playlists", tag] => {
ObjectIdType::DiscoverPlaylistsByTag(tag.to_string())
}
// Discover Genres // Discover Genres
["qobuz", "genres"] => ObjectIdType::DiscoverGenres, ["qobuz", "genres"] => ObjectIdType::DiscoverGenres,
["qobuz", "genre", id] => ObjectIdType::GenreRoot(id.to_string()), ["qobuz", "genre", id] => ObjectIdType::GenreRoot(id.to_string()),
["qobuz", "genre", id, "new-releases"] => ObjectIdType::GenreNewReleases(id.to_string()), ["qobuz", "genre", id, "new-releases"] => {
ObjectIdType::GenreNewReleases(id.to_string())
}
["qobuz", "genre", id, "ideal"] => ObjectIdType::GenreIdealDiscography(id.to_string()), ["qobuz", "genre", id, "ideal"] => ObjectIdType::GenreIdealDiscography(id.to_string()),
["qobuz", "genre", id, "qobuzissime"] => ObjectIdType::GenreQobuzissime(id.to_string()), ["qobuz", "genre", id, "qobuzissime"] => ObjectIdType::GenreQobuzissime(id.to_string()),
["qobuz", "genre", id, "editor-picks"] => ObjectIdType::GenreEditorPicks(id.to_string()), ["qobuz", "genre", id, "editor-picks"] => {
["qobuz", "genre", id, "press-awards"] => ObjectIdType::GenrePressAwards(id.to_string()), ObjectIdType::GenreEditorPicks(id.to_string())
}
["qobuz", "genre", id, "press-awards"] => {
ObjectIdType::GenrePressAwards(id.to_string())
}
["qobuz", "genre", id, "playlists"] => ObjectIdType::GenrePlaylists(id.to_string()), ["qobuz", "genre", id, "playlists"] => ObjectIdType::GenrePlaylists(id.to_string()),
// Favourites // Favourites
@@ -1440,13 +1504,13 @@ enum ObjectIdType {
// Discover Genres // Discover Genres
DiscoverGenres, DiscoverGenres,
GenreRoot(String), // genre_id GenreRoot(String), // genre_id
GenreNewReleases(String), // genre_id GenreNewReleases(String), // genre_id
GenreIdealDiscography(String), // genre_id GenreIdealDiscography(String), // genre_id
GenreQobuzissime(String), // genre_id GenreQobuzissime(String), // genre_id
GenreEditorPicks(String), // genre_id GenreEditorPicks(String), // genre_id
GenrePressAwards(String), // genre_id GenrePressAwards(String), // genre_id
GenrePlaylists(String), // genre_id GenrePlaylists(String), // genre_id
// Favourites // Favourites
Favourites, Favourites,
@@ -1488,6 +1552,8 @@ impl MusicSource for QobuzSource {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Qobuz".to_string(), title: "Qobuz".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![ containers: vec![
self.build_discover_catalog_container(), self.build_discover_catalog_container(),
self.build_discover_genres_container(), self.build_discover_genres_container(),
@@ -1509,16 +1575,22 @@ impl MusicSource for QobuzSource {
ObjectIdType::DiscoverCatalog => self.browse_discover_catalog().await, ObjectIdType::DiscoverCatalog => self.browse_discover_catalog().await,
ObjectIdType::DiscoverPlaylists => self.browse_discover_playlists().await, ObjectIdType::DiscoverPlaylists => self.browse_discover_playlists().await,
ObjectIdType::DiscoverAlbumsIdeal => self.browse_discover_albums_ideal().await, ObjectIdType::DiscoverAlbumsIdeal => self.browse_discover_albums_ideal().await,
ObjectIdType::DiscoverAlbumsQobuzissime => self.browse_discover_albums_qobuzissime().await, ObjectIdType::DiscoverAlbumsQobuzissime => {
self.browse_discover_albums_qobuzissime().await
}
ObjectIdType::DiscoverAlbumsNew => self.browse_discover_albums_new().await, ObjectIdType::DiscoverAlbumsNew => self.browse_discover_albums_new().await,
ObjectIdType::DiscoverArtists => self.browse_discover_artists().await, ObjectIdType::DiscoverArtists => self.browse_discover_artists().await,
ObjectIdType::DiscoverPlaylistsByTag(tag) => self.browse_discover_playlists_tag(&tag).await, ObjectIdType::DiscoverPlaylistsByTag(tag) => {
self.browse_discover_playlists_tag(&tag).await
}
// Discover Genres // Discover Genres
ObjectIdType::DiscoverGenres => self.browse_discover_genres().await, ObjectIdType::DiscoverGenres => self.browse_discover_genres().await,
ObjectIdType::GenreRoot(id) => self.browse_genre(&id).await, ObjectIdType::GenreRoot(id) => self.browse_genre(&id).await,
ObjectIdType::GenreNewReleases(id) => self.browse_genre_new_releases(&id).await, ObjectIdType::GenreNewReleases(id) => self.browse_genre_new_releases(&id).await,
ObjectIdType::GenreIdealDiscography(id) => self.browse_genre_ideal_discography(&id).await, ObjectIdType::GenreIdealDiscography(id) => {
self.browse_genre_ideal_discography(&id).await
}
ObjectIdType::GenreQobuzissime(id) => self.browse_genre_qobuzissime(&id).await, ObjectIdType::GenreQobuzissime(id) => self.browse_genre_qobuzissime(&id).await,
ObjectIdType::GenreEditorPicks(id) => self.browse_genre_editor_picks(&id).await, ObjectIdType::GenreEditorPicks(id) => self.browse_genre_editor_picks(&id).await,
ObjectIdType::GenrePressAwards(id) => self.browse_genre_press_awards(&id).await, ObjectIdType::GenrePressAwards(id) => self.browse_genre_press_awards(&id).await,
@@ -1952,11 +2024,7 @@ impl MusicSource for QobuzSource {
.get_or_create_album_playlist_items(&album_id, usize::MAX) .get_or_create_album_playlist_items(&album_id, usize::MAX)
.await?; .await?;
let items: Vec<Item> = all_items let items: Vec<Item> = all_items.into_iter().skip(offset).take(limit).collect();
.into_iter()
.skip(offset)
.take(limit)
.collect();
Ok(BrowseResult::Items(items)) Ok(BrowseResult::Items(items))
} }

2027
pmoqobuz/src/source.rs.bak Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -997,6 +997,8 @@ mod tests {
searchable: Some("1".to_string()), searchable: Some("1".to_string()),
title: "Test Source".to_string(), title: "Test Source".to_string(),
class: "object.container".to_string(), class: "object.container".to_string(),
artist: None,
album_art: None,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })