Correction and completion of the blackboard
This commit is contained in:
934
Blackboard_HTML/Architecture_music_source.html
Normal file
934
Blackboard_HTML/Architecture_music_source.html
Normal file
@@ -0,0 +1,934 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>music_source</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="guide-dimplémentation-dune-nouvelle-musicsource">Guide
|
||||
d’implémentation d’une nouvelle MusicSource</h1>
|
||||
<p>Ce document décrit comment implémenter une nouvelle source musicale
|
||||
dans l’écosystème PMOMusic en suivant le trait <code>MusicSource</code>
|
||||
défini dans le crate <code>pmosource</code>.</p>
|
||||
<h2 id="table-des-matières">Table des matières</h2>
|
||||
<ol type="1">
|
||||
<li><a href="#vue-densemble">Vue d’ensemble</a></li>
|
||||
<li><a href="#structure-dune-musicsource">Structure d’une
|
||||
MusicSource</a></li>
|
||||
<li><a href="#implémentation-du-trait-musicsource">Implémentation du
|
||||
trait MusicSource</a></li>
|
||||
<li><a href="#patterns-dimplémentation">Patterns
|
||||
d’implémentation</a></li>
|
||||
<li><a href="#intégration-avec-lécosystème-pmomusic">Intégration avec
|
||||
l’écosystème PMOMusic</a></li>
|
||||
<li><a href="#checklist-de-mise-en-œuvre">Checklist de mise en
|
||||
œuvre</a></li>
|
||||
<li><a href="#exemples-de-référence">Exemples de référence</a></li>
|
||||
</ol>
|
||||
<h2 id="vue-densemble">Vue d’ensemble</h2>
|
||||
<p>Une <code>MusicSource</code> est une abstraction qui représente une
|
||||
source de contenu musical dans PMOMusic. Elle peut être :</p>
|
||||
<ul>
|
||||
<li><strong>Dynamique (FIFO)</strong> : Radio Paradise, streaming radio,
|
||||
playlists live</li>
|
||||
<li><strong>Statique</strong> : Albums Qobuz, bibliothèque locale,
|
||||
playlists fixes</li>
|
||||
</ul>
|
||||
<p>Le trait <code>MusicSource</code> définit une interface unifiée pour
|
||||
: - La navigation UPnP ContentDirectory (browse) - La résolution d’URI
|
||||
audio (avec cache) - La gestion de playlists FIFO (pour les sources
|
||||
dynamiques) - Le suivi des changements (update_id, last_change)</p>
|
||||
<h2 id="structure-dune-musicsource">Structure d’une MusicSource</h2>
|
||||
<h3 id="organisation-du-code">Organisation du code</h3>
|
||||
<pre><code>pmo<votre-source>/
|
||||
├── src/
|
||||
│ ├── lib.rs # Exports publics
|
||||
│ ├── source.rs # Implémentation MusicSource
|
||||
│ ├── client.rs # Client API (optionnel)
|
||||
│ ├── models.rs # Structures de données
|
||||
│ ├── config.rs # Configuration
|
||||
│ └── didl.rs # Conversion DIDL-Lite (optionnel)
|
||||
├── assets/
|
||||
│ └── default.webp # Logo 300x300px
|
||||
├── Cargo.toml
|
||||
└── README.md</pre>
|
||||
<h3 id="dépendances-principales">Dépendances principales</h3>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode toml"><code class="sourceCode toml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[dependencies]</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="dt">pmosource</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmosource"</span><span class="op"> }</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="dt">pmodidl</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmodidl"</span><span class="op"> }</span></span>
|
||||
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="dt">pmoplaylist</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmoplaylist"</span><span class="op">, </span><span class="dt">optional</span><span class="op"> =</span> <span class="cn">true</span><span class="op"> }</span> <span class="co"># Si FIFO</span></span>
|
||||
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="dt">pmoaudiocache</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmoaudiocache"</span><span class="op">, </span><span class="dt">optional</span><span class="op"> =</span> <span class="cn">true</span><span class="op"> }</span> <span class="co"># Si cache</span></span>
|
||||
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="dt">pmocovers</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmocovers"</span><span class="op">, </span><span class="dt">optional</span><span class="op"> =</span> <span class="cn">true</span><span class="op"> }</span> <span class="co"># Si cache</span></span>
|
||||
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="dt">async-trait</span> <span class="op">=</span> <span class="st">"0.1"</span></span>
|
||||
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="dt">tokio</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"1"</span><span class="op">, </span><span class="dt">features</span><span class="op"> =</span> <span class="op">[</span><span class="st">"sync"</span><span class="op">] }</span></span>
|
||||
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="dt">serde</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"1"</span><span class="op">, </span><span class="dt">features</span><span class="op"> =</span> <span class="op">[</span><span class="st">"derive"</span><span class="op">] }</span></span>
|
||||
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a><span class="kw">[features]</span></span>
|
||||
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a><span class="dt">default</span> <span class="op">=</span> <span class="op">[</span><span class="st">"cache"</span><span class="op">]</span></span>
|
||||
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a><span class="dt">cache</span> <span class="op">=</span> <span class="op">[</span><span class="st">"pmoaudiocache"</span><span class="op">,</span> <span class="st">"pmocovers"</span><span class="op">]</span></span>
|
||||
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a><span class="dt">playlist</span> <span class="op">=</span> <span class="op">[</span><span class="st">"pmoplaylist"</span><span class="op">]</span></span></pre></div>
|
||||
<h2 id="implémentation-du-trait-musicsource">Implémentation du trait
|
||||
MusicSource</h2>
|
||||
<h3 id="informations-de-base">1. Informations de base</h3>
|
||||
<p>Chaque source doit fournir :</p>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmosource::</span><span class="op">{</span>async_trait<span class="op">,</span> MusicSource<span class="op">};</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="op">,</span> <span class="bu">Debug</span><span class="at">)]</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> MyMusicSource <span class="op">{</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// Champs internes</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> MusicSource <span class="cf">for</span> MyMusicSource <span class="op">{</span></span>
|
||||
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> name(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="op">&</span><span class="dt">str</span> <span class="op">{</span></span>
|
||||
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"Ma Source Musicale"</span> <span class="co">// Nom affiché dans l'UI</span></span>
|
||||
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> id(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="op">&</span><span class="dt">str</span> <span class="op">{</span></span>
|
||||
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> <span class="st">"my-music-source"</span> <span class="co">// ID unique (format: lowercase-kebab-case)</span></span>
|
||||
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> default_image(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="op">&</span>[<span class="dt">u8</span>] <span class="op">{</span></span>
|
||||
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="co">// Logo WebP 300x300px inclus dans le binaire</span></span>
|
||||
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> <span class="pp">include_bytes!</span>(<span class="st">"../assets/default.webp"</span>)</span>
|
||||
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> default_image_mime_type(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="op">&</span><span class="dt">str</span> <span class="op">{</span></span>
|
||||
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="st">"image/webp"</span> <span class="co">// Toujours WebP</span></span>
|
||||
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Règles :</strong> - <code>id()</code> doit être unique parmi
|
||||
toutes les sources - <code>id()</code> doit être en lowercase-kebab-case
|
||||
- <code>default_image()</code> doit être un WebP 300x300px</p>
|
||||
<h3 id="navigation-contentdirectory">2. Navigation ContentDirectory</h3>
|
||||
<h4 id="container-racine">2.1 Container racine</h4>
|
||||
<div class="sourceCode" id="cb4"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> root_container(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Container<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Container <span class="op">{</span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> id<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>id()<span class="op">.</span>to_string()<span class="op">,</span> <span class="co">// "my-music-source"</span></span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a> parent_id<span class="op">:</span> <span class="st">"0"</span><span class="op">.</span>to_string()<span class="op">,</span> <span class="co">// Toujours "0" pour la racine</span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a> restricted<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"1"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> child_count<span class="op">:</span> <span class="cn">None</span><span class="op">,</span> <span class="co">// Optionnel</span></span>
|
||||
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a> searchable<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"1"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a> title<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>name()<span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> class<span class="op">:</span> <span class="st">"object.container"</span><span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> artist<span class="op">:</span> <span class="cn">None</span><span class="op">,</span></span>
|
||||
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> album_art<span class="op">:</span> <span class="cn">None</span><span class="op">,</span></span>
|
||||
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> containers<span class="op">:</span> <span class="pp">vec!</span>[]<span class="op">,</span></span>
|
||||
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> items<span class="op">:</span> <span class="pp">vec!</span>[]<span class="op">,</span></span>
|
||||
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="browse">2.2 Browse</h4>
|
||||
<p>La méthode <code>browse()</code> est le cœur de la navigation :</p>
|
||||
<div class="sourceCode" id="cb5"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> browse(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> object_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>BrowseResult<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a> <span class="cf">match</span> <span class="kw">self</span><span class="op">.</span>parse_object_id(object_id) <span class="op">{</span></span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a> <span class="pp">ObjectIdType::</span>Root <span class="op">=></span> <span class="op">{</span></span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a> <span class="co">// Retourner les sous-containers principaux</span></span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> containers <span class="op">=</span> <span class="pp">vec!</span>[</span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>build_albums_container()<span class="op">,</span></span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>build_playlists_container()<span class="op">,</span></span>
|
||||
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>build_favorites_container()<span class="op">,</span></span>
|
||||
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a> ]<span class="op">;</span></span>
|
||||
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="pp">BrowseResult::</span>Containers(containers))</span>
|
||||
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> <span class="pp">ObjectIdType::</span>Album <span class="op">{</span> album_id <span class="op">}</span> <span class="op">=></span> <span class="op">{</span></span>
|
||||
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> <span class="co">// Retourner le container + ses tracks</span></span>
|
||||
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> album_container <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>build_album_container(<span class="op">&</span>album_id)<span class="op">;</span></span>
|
||||
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> tracks <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>get_album_tracks(<span class="op">&</span>album_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="pp">BrowseResult::</span>Mixed <span class="op">{</span></span>
|
||||
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> containers<span class="op">:</span> <span class="pp">vec!</span>[album_container]<span class="op">,</span></span>
|
||||
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> items<span class="op">:</span> tracks<span class="op">,</span></span>
|
||||
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> <span class="pp">ObjectIdType::</span>Track <span class="op">{</span> track_id <span class="op">}</span> <span class="op">=></span> <span class="op">{</span></span>
|
||||
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> <span class="co">// Retourner les détails d'un track</span></span>
|
||||
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> track <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>get_track_item(<span class="op">&</span>track_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="pp">BrowseResult::</span>Items(<span class="pp">vec!</span>[track]))</span>
|
||||
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> _ <span class="op">=></span> <span class="cn">Err</span>(<span class="pp">MusicSourceError::</span>ObjectNotFound(</span>
|
||||
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> <span class="pp">format!</span>(<span class="st">"Unknown object: {}"</span><span class="op">,</span> object_id)</span>
|
||||
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> ))</span>
|
||||
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Schema d’Object ID recommandé :</strong></p>
|
||||
<pre><code><source-id> # Racine
|
||||
<source-id>:albums # Container albums
|
||||
<source-id>:album:<album_id> # Album spécifique
|
||||
<source-id>:track:<track_id> # Track spécifique
|
||||
<source-id>:playlist:<playlist_id> # Playlist spécifique</pre>
|
||||
<p><strong>Types de BrowseResult :</strong> -
|
||||
<code>Containers(Vec<Container>)</code> : Liste de containers
|
||||
(navigation) - <code>Items(Vec<Item>)</code> : Liste de tracks
|
||||
(lecture) - <code>Mixed { containers, items }</code> : Les deux (album
|
||||
avec tracks)</p>
|
||||
<h4 id="résolution-duri">2.3 Résolution d’URI</h4>
|
||||
<div class="sourceCode" id="cb7"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> resolve_uri(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> object_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">String</span><span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a> <span class="co">// Étape 1 : Vérifier le cache audio</span></span>
|
||||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(cached_pk) <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>get_cached_audio_pk(object_id)<span class="op">.</span><span class="kw">await</span> <span class="op">{</span></span>
|
||||
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cn">Ok</span>(<span class="pp">format!</span>(<span class="st">"{}/audio/flac/{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>base_url<span class="op">,</span> cached_pk))<span class="op">;</span></span>
|
||||
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a> <span class="co">// Étape 2 : Retourner l'URI originale</span></span>
|
||||
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">match</span> <span class="kw">self</span><span class="op">.</span>parse_object_id(object_id) <span class="op">{</span></span>
|
||||
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a> <span class="pp">ObjectIdType::</span>Track <span class="op">{</span> track_id <span class="op">}</span> <span class="op">=></span> <span class="op">{</span></span>
|
||||
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> stream_url <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>get_stream_url(<span class="op">&</span>track_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(stream_url)</span>
|
||||
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a> _ <span class="op">=></span> <span class="cn">Err</span>(<span class="pp">MusicSourceError::</span>UriResolutionError(</span>
|
||||
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a> <span class="pp">format!</span>(<span class="st">"Cannot resolve URI for: {}"</span><span class="op">,</span> object_id)</span>
|
||||
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a> ))</span>
|
||||
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Ordre de résolution :</strong> 1. Cache audio local (si
|
||||
disponible) 2. URI originale (API streaming, fichier local, etc.)</p>
|
||||
<h3 id="support-fifo-sources-dynamiques">3. Support FIFO (sources
|
||||
dynamiques)</h3>
|
||||
<p>Si votre source est dynamique (radio, streaming live) :</p>
|
||||
<div class="sourceCode" id="cb8"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoplaylist::</span>PlaylistManager<span class="op">;</span></span>
|
||||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">std::sync::</span>Arc<span class="op">;</span></span>
|
||||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">tokio::sync::</span>RwLock<span class="op">;</span></span>
|
||||
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="at">)]</span></span>
|
||||
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> RadioSource <span class="op">{</span></span>
|
||||
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a> playlist_id<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a> update_counter<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span><span class="dt">u32</span><span class="op">>>,</span></span>
|
||||
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a> last_change<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>SystemTime<span class="op">>>,</span></span>
|
||||
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> MusicSource <span class="cf">for</span> RadioSource <span class="op">{</span></span>
|
||||
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> supports_fifo(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">bool</span> <span class="op">{</span></span>
|
||||
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a> <span class="cn">true</span> <span class="co">// Cette source utilise une FIFO</span></span>
|
||||
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> append_track(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> track<span class="op">:</span> Item) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer le gestionnaire de playlist</span></span>
|
||||
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> writer <span class="op">=</span> manager</span>
|
||||
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>get_persistent_write_handle(<span class="kw">self</span><span class="op">.</span>playlist_id<span class="op">.</span>clone())</span>
|
||||
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a> <span class="co">// Extraire le PK depuis l'URI du track</span></span>
|
||||
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> pk <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>extract_pk_from_item(<span class="op">&</span>track)<span class="op">?;</span></span>
|
||||
<span id="cb8-28"><a href="#cb8-28" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-29"><a href="#cb8-29" aria-hidden="true" tabindex="-1"></a> <span class="co">// Ajouter à la playlist</span></span>
|
||||
<span id="cb8-30"><a href="#cb8-30" aria-hidden="true" tabindex="-1"></a> writer</span>
|
||||
<span id="cb8-31"><a href="#cb8-31" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>push_lazy(pk)</span>
|
||||
<span id="cb8-32"><a href="#cb8-32" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-33"><a href="#cb8-33" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-34"><a href="#cb8-34" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-35"><a href="#cb8-35" aria-hidden="true" tabindex="-1"></a> <span class="co">// Incrémenter update_id</span></span>
|
||||
<span id="cb8-36"><a href="#cb8-36" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>bump_update_counter()<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb8-37"><a href="#cb8-37" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-38"><a href="#cb8-38" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(())</span>
|
||||
<span id="cb8-39"><a href="#cb8-39" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-40"><a href="#cb8-40" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-41"><a href="#cb8-41" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> remove_oldest(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Option</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb8-42"><a href="#cb8-42" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb8-43"><a href="#cb8-43" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> reader <span class="op">=</span> manager</span>
|
||||
<span id="cb8-44"><a href="#cb8-44" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>get_read_handle(<span class="op">&</span><span class="kw">self</span><span class="op">.</span>playlist_id)</span>
|
||||
<span id="cb8-45"><a href="#cb8-45" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-46"><a href="#cb8-46" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-47"><a href="#cb8-47" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-48"><a href="#cb8-48" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer le plus ancien</span></span>
|
||||
<span id="cb8-49"><a href="#cb8-49" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> reader<span class="op">.</span>to_items(<span class="dv">1</span>)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-50"><a href="#cb8-50" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-51"><a href="#cb8-51" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-52"><a href="#cb8-52" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(item) <span class="op">=</span> items<span class="op">.</span>first() <span class="op">{</span></span>
|
||||
<span id="cb8-53"><a href="#cb8-53" aria-hidden="true" tabindex="-1"></a> <span class="co">// Adapter l'item au schéma de la source</span></span>
|
||||
<span id="cb8-54"><a href="#cb8-54" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> adapted <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>adapt_item_to_schema(item<span class="op">.</span>clone())<span class="op">;</span></span>
|
||||
<span id="cb8-55"><a href="#cb8-55" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>bump_update_counter()<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb8-56"><a href="#cb8-56" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="cn">Some</span>(adapted))</span>
|
||||
<span id="cb8-57"><a href="#cb8-57" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
|
||||
<span id="cb8-58"><a href="#cb8-58" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="cn">None</span>)</span>
|
||||
<span id="cb8-59"><a href="#cb8-59" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-60"><a href="#cb8-60" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-61"><a href="#cb8-61" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-62"><a href="#cb8-62" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> update_id(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">u32</span> <span class="op">{</span></span>
|
||||
<span id="cb8-63"><a href="#cb8-63" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span><span class="kw">self</span><span class="op">.</span>update_counter<span class="op">.</span>read()<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-64"><a href="#cb8-64" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-65"><a href="#cb8-65" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-66"><a href="#cb8-66" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> last_change(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Option</span><span class="op"><</span>SystemTime<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb8-67"><a href="#cb8-67" aria-hidden="true" tabindex="-1"></a> <span class="cn">Some</span>(<span class="op">*</span><span class="kw">self</span><span class="op">.</span>last_change<span class="op">.</span>read()<span class="op">.</span><span class="kw">await</span>)</span>
|
||||
<span id="cb8-68"><a href="#cb8-68" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-69"><a href="#cb8-69" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-70"><a href="#cb8-70" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> get_items(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> offset<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span> count<span class="op">:</span> <span class="dt">usize</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb8-71"><a href="#cb8-71" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb8-72"><a href="#cb8-72" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> reader <span class="op">=</span> manager</span>
|
||||
<span id="cb8-73"><a href="#cb8-73" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>get_read_handle(<span class="op">&</span><span class="kw">self</span><span class="op">.</span>playlist_id)</span>
|
||||
<span id="cb8-74"><a href="#cb8-74" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-75"><a href="#cb8-75" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-76"><a href="#cb8-76" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-77"><a href="#cb8-77" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer les items</span></span>
|
||||
<span id="cb8-78"><a href="#cb8-78" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> reader</span>
|
||||
<span id="cb8-79"><a href="#cb8-79" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>to_items(count)</span>
|
||||
<span id="cb8-80"><a href="#cb8-80" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb8-81"><a href="#cb8-81" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">MusicSourceError::</span>PlaylistError(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb8-82"><a href="#cb8-82" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-83"><a href="#cb8-83" aria-hidden="true" tabindex="-1"></a> <span class="co">// Adapter au schéma de la source</span></span>
|
||||
<span id="cb8-84"><a href="#cb8-84" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> adapted <span class="op">=</span> items<span class="op">.</span>into_iter()</span>
|
||||
<span id="cb8-85"><a href="#cb8-85" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map(<span class="op">|</span>item<span class="op">|</span> <span class="kw">self</span><span class="op">.</span>adapt_item_to_schema(item))</span>
|
||||
<span id="cb8-86"><a href="#cb8-86" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>collect()<span class="op">;</span></span>
|
||||
<span id="cb8-87"><a href="#cb8-87" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-88"><a href="#cb8-88" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(adapted)</span>
|
||||
<span id="cb8-89"><a href="#cb8-89" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-90"><a href="#cb8-90" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb8-91"><a href="#cb8-91" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-92"><a href="#cb8-92" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> RadioSource <span class="op">{</span></span>
|
||||
<span id="cb8-93"><a href="#cb8-93" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> bump_update_counter(<span class="op">&</span><span class="kw">self</span>) <span class="op">{</span></span>
|
||||
<span id="cb8-94"><a href="#cb8-94" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> <span class="kw">mut</span> counter <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>update_counter<span class="op">.</span>write()<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb8-95"><a href="#cb8-95" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span>counter <span class="op">=</span> counter<span class="op">.</span>wrapping_add(<span class="dv">1</span>)<span class="op">.</span>max(<span class="dv">1</span>)<span class="op">;</span></span>
|
||||
<span id="cb8-96"><a href="#cb8-96" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> <span class="kw">mut</span> last <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>last_change<span class="op">.</span>write()<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb8-97"><a href="#cb8-97" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span>last <span class="op">=</span> <span class="pp">SystemTime::</span>now()<span class="op">;</span></span>
|
||||
<span id="cb8-98"><a href="#cb8-98" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-99"><a href="#cb8-99" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Points clés :</strong> - Utiliser
|
||||
<code>pmoplaylist::PlaylistManager</code> singleton - Incrémenter
|
||||
<code>update_id</code> à chaque modification - Mettre à jour
|
||||
<code>last_change</code> à chaque modification - Adapter les IDs des
|
||||
items au schéma de la source</p>
|
||||
<h3 id="support-statique-albums-bibliothèques">4. Support statique
|
||||
(albums, bibliothèques)</h3>
|
||||
<p>Si votre source est statique (catalogue, albums) :</p>
|
||||
<div class="sourceCode" id="cb9"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> MusicSource <span class="cf">for</span> CatalogSource <span class="op">{</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> supports_fifo(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">bool</span> <span class="op">{</span></span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="cn">false</span> <span class="co">// Pas de FIFO</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> append_track(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> _track<span class="op">:</span> Item) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a> <span class="cn">Err</span>(<span class="pp">MusicSourceError::</span>NotSupported(</span>
|
||||
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a> <span class="st">"This source is read-only"</span><span class="op">.</span>to_string()</span>
|
||||
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a> ))</span>
|
||||
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> remove_oldest(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Option</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="cn">None</span>) <span class="co">// Pas de suppression</span></span>
|
||||
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> update_id(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">u32</span> <span class="op">{</span></span>
|
||||
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a> <span class="dv">0</span> <span class="co">// Jamais de changement</span></span>
|
||||
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> last_change(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Option</span><span class="op"><</span>SystemTime<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a> <span class="cn">None</span> <span class="co">// Pas de suivi des changements</span></span>
|
||||
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> get_items(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> offset<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span> count<span class="op">:</span> <span class="dt">usize</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a> <span class="co">// Retourner une liste paginée depuis le catalogue</span></span>
|
||||
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>get_catalog_items(offset<span class="op">,</span> count)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="patterns-dimplémentation">Patterns d’implémentation</h2>
|
||||
<h3 id="pattern-1-source-dynamique-avec-fifo-radio-paradise">Pattern 1 :
|
||||
Source dynamique avec FIFO (Radio Paradise)</h3>
|
||||
<p><strong>Caractéristiques :</strong> - Flux continu de tracks -
|
||||
Capacité limitée (50-100 tracks) - Suppression automatique des plus
|
||||
anciens - <code>supports_fifo() = true</code></p>
|
||||
<p><strong>Structure :</strong></p>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="at">)]</span></span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> RadioParadiseSource <span class="op">{</span></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a> base_url<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a> update_counter<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span><span class="dt">u32</span><span class="op">>>,</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> last_change<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>SystemTime<span class="op">>>,</span></span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> callback_tokens<span class="op">:</span> Arc<span class="op"><</span><span class="pp">std::sync::</span>Mutex<span class="op"><</span><span class="dt">Vec</span><span class="op"><</span><span class="dt">u64</span><span class="op">>>>,</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> container_notifier<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span>Arc<span class="op"><</span><span class="kw">dyn</span> <span class="bu">Fn</span>(<span class="op">&</span>[<span class="dt">String</span>]) <span class="op">+</span> <span class="bu">Send</span> <span class="op">+</span> <span class="bu">Sync</span><span class="op">>>,</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> RadioParadiseSource <span class="op">{</span></span>
|
||||
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> <span class="co">// Enregistrer des callbacks sur les playlists pour notifier les changements</span></span>
|
||||
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> attach_playlist_callbacks(<span class="kw">self</span><span class="op">:</span> <span class="op">&</span>Arc<span class="op"><</span><span class="dt">Self</span><span class="op">></span>) <span class="op">{</span></span>
|
||||
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> playlist_ids <span class="op">=</span> <span class="pp">vec!</span>[</span>
|
||||
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>live_playlist_id()<span class="op">,</span></span>
|
||||
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>history_playlist_id()<span class="op">,</span></span>
|
||||
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a> ]<span class="op">;</span></span>
|
||||
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> <span class="kw">mut</span> tokens <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>callback_tokens<span class="op">.</span>lock()<span class="op">.</span>unwrap()<span class="op">;</span></span>
|
||||
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> pid <span class="kw">in</span> playlist_ids <span class="op">{</span></span>
|
||||
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> weak <span class="op">=</span> <span class="pp">Arc::</span>downgrade(<span class="kw">self</span>)<span class="op">;</span></span>
|
||||
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> pid_clone <span class="op">=</span> pid<span class="op">.</span>clone()<span class="op">;</span></span>
|
||||
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> token <span class="op">=</span> manager<span class="op">.</span>register_callback(<span class="kw">move</span> <span class="op">|</span>event<span class="op">|</span> <span class="op">{</span></span>
|
||||
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> event<span class="op">.</span>playlist_id <span class="op">==</span> pid_clone <span class="op">{</span></span>
|
||||
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(strong) <span class="op">=</span> weak<span class="op">.</span>upgrade() <span class="op">{</span></span>
|
||||
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a> <span class="pp">tokio::</span>spawn(<span class="kw">async</span> <span class="kw">move</span> <span class="op">{</span></span>
|
||||
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a> strong<span class="op">.</span>bump_update_counter()<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a> <span class="co">// Notifier ContentDirectory</span></span>
|
||||
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(notifier) <span class="op">=</span> strong<span class="op">.</span>container_notifier<span class="op">.</span>as_ref() <span class="op">{</span></span>
|
||||
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a> notifier(<span class="op">&</span>[<span class="pp">format!</span>(<span class="st">"radio-paradise:history"</span>)])<span class="op">;</span></span>
|
||||
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)<span class="op">;</span></span>
|
||||
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)<span class="op">;</span></span>
|
||||
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a> tokens<span class="op">.</span>push(token)<span class="op">;</span></span>
|
||||
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Points clés :</strong> - Callbacks sur
|
||||
<code>pmoplaylist</code> pour détecter les changements - Notification du
|
||||
ContentDirectory via un notifier injecté - <code>update_counter</code>
|
||||
partagé via <code>Arc<RwLock<u32>></code></p>
|
||||
<h3 id="pattern-2-source-catalogue-avec-playlists-lazy-qobuz">Pattern 2
|
||||
: Source catalogue avec playlists lazy (Qobuz)</h3>
|
||||
<p><strong>Caractéristiques :</strong> - Catalogue vaste (millions de
|
||||
tracks) - Playlists créées à la demande - Cache lazy (cover eager, audio
|
||||
lazy) - <code>supports_fifo() = false</code></p>
|
||||
<p><strong>Structure :</strong></p>
|
||||
<div class="sourceCode" id="cb11"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="at">)]</span></span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> QobuzSource <span class="op">{</span></span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a> inner<span class="op">:</span> Arc<span class="op"><</span>QobuzSourceInner<span class="op">>,</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="kw">struct</span> QobuzSourceInner <span class="op">{</span></span>
|
||||
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a> client<span class="op">:</span> Arc<span class="op"><</span>QobuzClient<span class="op">>,</span></span>
|
||||
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a> cache_manager<span class="op">:</span> SourceCacheManager<span class="op">,</span></span>
|
||||
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a> base_url<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a> update_counter<span class="op">:</span> <span class="pp">tokio::sync::</span>RwLock<span class="op"><</span><span class="dt">u32</span><span class="op">>,</span></span>
|
||||
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a> last_change<span class="op">:</span> <span class="pp">tokio::sync::</span>RwLock<span class="op"><</span>SystemTime<span class="op">>,</span></span>
|
||||
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> QobuzSource <span class="op">{</span></span>
|
||||
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a> <span class="co">// Ajouter un track avec cache lazy</span></span>
|
||||
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> add_track_lazy(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> track<span class="op">:</span> <span class="op">&</span>Track) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>(<span class="dt">String</span><span class="op">,</span> <span class="dt">String</span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb11-17"><a href="#cb11-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> track_id <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"qobuz://track/{}"</span><span class="op">,</span> track<span class="op">.</span>id)<span class="op">;</span></span>
|
||||
<span id="cb11-18"><a href="#cb11-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> lazy_pk <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"QOBUZ:{}"</span><span class="op">,</span> track<span class="op">.</span>id)<span class="op">;</span></span>
|
||||
<span id="cb11-19"><a href="#cb11-19" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-20"><a href="#cb11-20" aria-hidden="true" tabindex="-1"></a> <span class="co">// 1. Cache cover EAGERLY (petit, UI en a besoin)</span></span>
|
||||
<span id="cb11-21"><a href="#cb11-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> cached_cover_pk <span class="op">=</span> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(<span class="kw">ref</span> image_url) <span class="op">=</span> track<span class="op">.</span>album<span class="op">.</span>as_ref()</span>
|
||||
<span id="cb11-22"><a href="#cb11-22" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>and_then(<span class="op">|</span>a<span class="op">|</span> a<span class="op">.</span>image<span class="op">.</span>as_ref()) <span class="op">{</span></span>
|
||||
<span id="cb11-23"><a href="#cb11-23" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>inner<span class="op">.</span>cache_manager<span class="op">.</span>cache_cover(image_url)<span class="op">.</span><span class="kw">await</span><span class="op">.</span>ok()</span>
|
||||
<span id="cb11-24"><a href="#cb11-24" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
|
||||
<span id="cb11-25"><a href="#cb11-25" aria-hidden="true" tabindex="-1"></a> <span class="cn">None</span></span>
|
||||
<span id="cb11-26"><a href="#cb11-26" aria-hidden="true" tabindex="-1"></a> <span class="op">};</span></span>
|
||||
<span id="cb11-27"><a href="#cb11-27" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-28"><a href="#cb11-28" aria-hidden="true" tabindex="-1"></a> <span class="co">// 2. Préparer metadata</span></span>
|
||||
<span id="cb11-29"><a href="#cb11-29" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> metadata <span class="op">=</span> AudioMetadata <span class="op">{</span></span>
|
||||
<span id="cb11-30"><a href="#cb11-30" aria-hidden="true" tabindex="-1"></a> title<span class="op">:</span> <span class="cn">Some</span>(track<span class="op">.</span>title<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb11-31"><a href="#cb11-31" aria-hidden="true" tabindex="-1"></a> artist<span class="op">:</span> track<span class="op">.</span>performer<span class="op">.</span>as_ref()<span class="op">.</span>map(<span class="op">|</span>p<span class="op">|</span> p<span class="op">.</span>name<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb11-32"><a href="#cb11-32" aria-hidden="true" tabindex="-1"></a> album<span class="op">:</span> track<span class="op">.</span>album<span class="op">.</span>as_ref()<span class="op">.</span>map(<span class="op">|</span>a<span class="op">|</span> a<span class="op">.</span>title<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb11-33"><a href="#cb11-33" aria-hidden="true" tabindex="-1"></a> duration_secs<span class="op">:</span> <span class="cn">Some</span>(track<span class="op">.</span>duration <span class="kw">as</span> <span class="dt">u64</span>)<span class="op">,</span></span>
|
||||
<span id="cb11-34"><a href="#cb11-34" aria-hidden="true" tabindex="-1"></a> <span class="co">// ... autres champs</span></span>
|
||||
<span id="cb11-35"><a href="#cb11-35" aria-hidden="true" tabindex="-1"></a> <span class="op">};</span></span>
|
||||
<span id="cb11-36"><a href="#cb11-36" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-37"><a href="#cb11-37" aria-hidden="true" tabindex="-1"></a> <span class="co">// 3. Cache audio LAZILY (grand, téléchargé à la demande)</span></span>
|
||||
<span id="cb11-38"><a href="#cb11-38" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> cached_audio_pk <span class="op">=</span> <span class="kw">self</span></span>
|
||||
<span id="cb11-39"><a href="#cb11-39" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>inner</span>
|
||||
<span id="cb11-40"><a href="#cb11-40" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>cache_manager</span>
|
||||
<span id="cb11-41"><a href="#cb11-41" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>cache_audio_lazy_with_provider(</span>
|
||||
<span id="cb11-42"><a href="#cb11-42" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span>lazy_pk<span class="op">,</span></span>
|
||||
<span id="cb11-43"><a href="#cb11-43" aria-hidden="true" tabindex="-1"></a> <span class="cn">Some</span>(metadata<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb11-44"><a href="#cb11-44" aria-hidden="true" tabindex="-1"></a> cached_cover_pk<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb11-45"><a href="#cb11-45" aria-hidden="true" tabindex="-1"></a> )</span>
|
||||
<span id="cb11-46"><a href="#cb11-46" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-47"><a href="#cb11-47" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-48"><a href="#cb11-48" aria-hidden="true" tabindex="-1"></a> <span class="co">// 4. Stocker metadata</span></span>
|
||||
<span id="cb11-49"><a href="#cb11-49" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>inner<span class="op">.</span>cache_manager<span class="op">.</span>update_metadata(</span>
|
||||
<span id="cb11-50"><a href="#cb11-50" aria-hidden="true" tabindex="-1"></a> track_id<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb11-51"><a href="#cb11-51" aria-hidden="true" tabindex="-1"></a> <span class="pp">pmosource::</span>TrackMetadata <span class="op">{</span></span>
|
||||
<span id="cb11-52"><a href="#cb11-52" aria-hidden="true" tabindex="-1"></a> original_uri<span class="op">:</span> stream_url<span class="op">,</span></span>
|
||||
<span id="cb11-53"><a href="#cb11-53" aria-hidden="true" tabindex="-1"></a> cached_audio_pk<span class="op">:</span> <span class="cn">Some</span>(cached_audio_pk<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb11-54"><a href="#cb11-54" aria-hidden="true" tabindex="-1"></a> cached_cover_pk<span class="op">,</span></span>
|
||||
<span id="cb11-55"><a href="#cb11-55" aria-hidden="true" tabindex="-1"></a> <span class="op">},</span></span>
|
||||
<span id="cb11-56"><a href="#cb11-56" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb11-57"><a href="#cb11-57" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-58"><a href="#cb11-58" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>((track_id<span class="op">,</span> cached_audio_pk))</span>
|
||||
<span id="cb11-59"><a href="#cb11-59" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb11-60"><a href="#cb11-60" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-61"><a href="#cb11-61" aria-hidden="true" tabindex="-1"></a> <span class="co">// Créer une playlist d'album avec TTL</span></span>
|
||||
<span id="cb11-62"><a href="#cb11-62" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> get_or_create_album_playlist_items(</span>
|
||||
<span id="cb11-63"><a href="#cb11-63" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span><span class="kw">self</span><span class="op">,</span></span>
|
||||
<span id="cb11-64"><a href="#cb11-64" aria-hidden="true" tabindex="-1"></a> album_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span><span class="op">,</span></span>
|
||||
<span id="cb11-65"><a href="#cb11-65" aria-hidden="true" tabindex="-1"></a> limit<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span></span>
|
||||
<span id="cb11-66"><a href="#cb11-66" aria-hidden="true" tabindex="-1"></a> ) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb11-67"><a href="#cb11-67" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> ALBUM_PLAYLIST_TTL<span class="op">:</span> Duration <span class="op">=</span> <span class="pp">Duration::</span>from_secs(<span class="dv">7</span> <span class="op">*</span> <span class="dv">24</span> <span class="op">*</span> <span class="dv">3600</span>)<span class="op">;</span></span>
|
||||
<span id="cb11-68"><a href="#cb11-68" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-69"><a href="#cb11-69" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> playlist_id <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"qobuz-album-{}"</span><span class="op">,</span> album_id)<span class="op">;</span></span>
|
||||
<span id="cb11-70"><a href="#cb11-70" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> playlist_manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb11-71"><a href="#cb11-71" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-72"><a href="#cb11-72" aria-hidden="true" tabindex="-1"></a> <span class="co">// Vérifier validité (existe ET non expirée ET non vide)</span></span>
|
||||
<span id="cb11-73"><a href="#cb11-73" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> is_valid <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>is_album_playlist_valid(<span class="op">&</span>playlist_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-74"><a href="#cb11-74" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-75"><a href="#cb11-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> is_valid <span class="op">{</span></span>
|
||||
<span id="cb11-76"><a href="#cb11-76" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer depuis playlist existante</span></span>
|
||||
<span id="cb11-77"><a href="#cb11-77" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> reader <span class="op">=</span> playlist_manager<span class="op">.</span>get_read_handle(<span class="op">&</span>playlist_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-78"><a href="#cb11-78" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> reader<span class="op">.</span>to_items(limit)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-79"><a href="#cb11-79" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="kw">self</span><span class="op">.</span>adapt_playlist_items_to_qobuz(items<span class="op">,</span> album_id)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb11-80"><a href="#cb11-80" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb11-81"><a href="#cb11-81" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-82"><a href="#cb11-82" aria-hidden="true" tabindex="-1"></a> <span class="co">// Créer nouvelle playlist</span></span>
|
||||
<span id="cb11-83"><a href="#cb11-83" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> writer <span class="op">=</span> playlist_manager</span>
|
||||
<span id="cb11-84"><a href="#cb11-84" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>create_persistent_playlist_with_role(</span>
|
||||
<span id="cb11-85"><a href="#cb11-85" aria-hidden="true" tabindex="-1"></a> playlist_id<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb11-86"><a href="#cb11-86" aria-hidden="true" tabindex="-1"></a> <span class="pp">pmoplaylist::PlaylistRole::</span>Album<span class="op">,</span></span>
|
||||
<span id="cb11-87"><a href="#cb11-87" aria-hidden="true" tabindex="-1"></a> )</span>
|
||||
<span id="cb11-88"><a href="#cb11-88" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-89"><a href="#cb11-89" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-90"><a href="#cb11-90" aria-hidden="true" tabindex="-1"></a> <span class="co">// Ajouter tracks avec cache lazy</span></span>
|
||||
<span id="cb11-91"><a href="#cb11-91" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_album_to_playlist(<span class="op">&</span>playlist_id<span class="op">,</span> album_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-92"><a href="#cb11-92" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-93"><a href="#cb11-93" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer items</span></span>
|
||||
<span id="cb11-94"><a href="#cb11-94" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> reader <span class="op">=</span> playlist_manager<span class="op">.</span>get_read_handle(<span class="op">&</span>playlist_id)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-95"><a href="#cb11-95" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> reader<span class="op">.</span>to_items(limit)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb11-96"><a href="#cb11-96" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>adapt_playlist_items_to_qobuz(items<span class="op">,</span> album_id)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb11-97"><a href="#cb11-97" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb11-98"><a href="#cb11-98" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Points clés :</strong> - Cache lazy pour l’audio (téléchargé
|
||||
à la demande) - Cache eager pour les covers (petit, UI en a besoin) -
|
||||
Playlists avec TTL (7 jours) - <code>LazyProvider</code> pour
|
||||
télécharger l’audio lors de la lecture</p>
|
||||
<h3 id="pattern-3-adaptation-des-ids-entre-playlist-et-source">Pattern 3
|
||||
: Adaptation des IDs entre playlist et source</h3>
|
||||
<p>Lorsqu’une source utilise <code>pmoplaylist</code>, les items
|
||||
retournés ont des IDs génériques. Il faut les adapter au schéma de la
|
||||
source :</p>
|
||||
<div class="sourceCode" id="cb12"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> adapt_playlist_items_to_source(</span>
|
||||
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span><span class="kw">self</span><span class="op">,</span></span>
|
||||
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a> items<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>Item<span class="op">>,</span></span>
|
||||
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a> parent_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span><span class="op">,</span></span>
|
||||
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>Item<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> <span class="kw">mut</span> adapted <span class="op">=</span> <span class="dt">Vec</span><span class="pp">::</span>with_capacity(items<span class="op">.</span>len())<span class="op">;</span></span>
|
||||
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> <span class="kw">mut</span> item <span class="kw">in</span> items <span class="op">{</span></span>
|
||||
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a> <span class="co">// Extraire cache_pk depuis l'URL du resource</span></span>
|
||||
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> cache_pk <span class="op">=</span> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(resource) <span class="op">=</span> item<span class="op">.</span>resources<span class="op">.</span>first() <span class="op">{</span></span>
|
||||
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a> resource</span>
|
||||
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>url</span>
|
||||
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>strip_prefix(<span class="st">"/audio/flac/"</span>)</span>
|
||||
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map(<span class="op">|</span>s<span class="op">|</span> s<span class="op">.</span>to_string())</span>
|
||||
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
|
||||
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a> <span class="cn">None</span></span>
|
||||
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a> <span class="op">};</span></span>
|
||||
<span id="cb12-18"><a href="#cb12-18" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-19"><a href="#cb12-19" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(pk) <span class="op">=</span> cache_pk <span class="op">{</span></span>
|
||||
<span id="cb12-20"><a href="#cb12-20" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer source_track_id depuis metadata</span></span>
|
||||
<span id="cb12-21"><a href="#cb12-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Ok</span>(<span class="cn">Some</span>(track_id_value)) <span class="op">=</span> <span class="kw">self</span></span>
|
||||
<span id="cb12-22"><a href="#cb12-22" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>cache_manager</span>
|
||||
<span id="cb12-23"><a href="#cb12-23" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>get_audio_metadata(<span class="op">&</span>pk<span class="op">,</span> <span class="st">"source_track_id"</span>)</span>
|
||||
<span id="cb12-24"><a href="#cb12-24" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span></span>
|
||||
<span id="cb12-25"><a href="#cb12-25" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(track_id) <span class="op">=</span> track_id_value<span class="op">.</span>as_str() <span class="op">{</span></span>
|
||||
<span id="cb12-26"><a href="#cb12-26" aria-hidden="true" tabindex="-1"></a> item<span class="op">.</span>id <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"my-source:track:{}"</span><span class="op">,</span> track_id)<span class="op">;</span></span>
|
||||
<span id="cb12-27"><a href="#cb12-27" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-28"><a href="#cb12-28" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-29"><a href="#cb12-29" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-30"><a href="#cb12-30" aria-hidden="true" tabindex="-1"></a> <span class="co">// Convertir URL relative en absolue</span></span>
|
||||
<span id="cb12-31"><a href="#cb12-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(resource) <span class="op">=</span> item<span class="op">.</span>resources<span class="op">.</span>first_mut() <span class="op">{</span></span>
|
||||
<span id="cb12-32"><a href="#cb12-32" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> resource<span class="op">.</span>url<span class="op">.</span>starts_with(<span class="ch">'/'</span>) <span class="op">{</span></span>
|
||||
<span id="cb12-33"><a href="#cb12-33" aria-hidden="true" tabindex="-1"></a> resource<span class="op">.</span>url <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"{}{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>base_url<span class="op">,</span> resource<span class="op">.</span>url)<span class="op">;</span></span>
|
||||
<span id="cb12-34"><a href="#cb12-34" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-35"><a href="#cb12-35" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-36"><a href="#cb12-36" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-37"><a href="#cb12-37" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-38"><a href="#cb12-38" aria-hidden="true" tabindex="-1"></a> item<span class="op">.</span>parent_id <span class="op">=</span> parent_id<span class="op">.</span>to_string()<span class="op">;</span></span>
|
||||
<span id="cb12-39"><a href="#cb12-39" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-40"><a href="#cb12-40" aria-hidden="true" tabindex="-1"></a> <span class="co">// Normaliser album art</span></span>
|
||||
<span id="cb12-41"><a href="#cb12-41" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">let</span> <span class="cn">Some</span>(art) <span class="op">=</span> item<span class="op">.</span>album_art<span class="op">.</span>as_mut() <span class="op">{</span></span>
|
||||
<span id="cb12-42"><a href="#cb12-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> art<span class="op">.</span>starts_with(<span class="ch">'/'</span>) <span class="op">{</span></span>
|
||||
<span id="cb12-43"><a href="#cb12-43" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span>art <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"{}{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>base_url<span class="op">,</span> art)<span class="op">;</span></span>
|
||||
<span id="cb12-44"><a href="#cb12-44" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-45"><a href="#cb12-45" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
|
||||
<span id="cb12-46"><a href="#cb12-46" aria-hidden="true" tabindex="-1"></a> item<span class="op">.</span>album_art <span class="op">=</span> <span class="cn">Some</span>(<span class="kw">self</span><span class="op">.</span>default_cover_url())<span class="op">;</span></span>
|
||||
<span id="cb12-47"><a href="#cb12-47" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-48"><a href="#cb12-48" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-49"><a href="#cb12-49" aria-hidden="true" tabindex="-1"></a> <span class="co">// Ajouter genre par défaut si absent (requis par certains clients)</span></span>
|
||||
<span id="cb12-50"><a href="#cb12-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> item<span class="op">.</span>genre<span class="op">.</span>is_none() <span class="op">{</span></span>
|
||||
<span id="cb12-51"><a href="#cb12-51" aria-hidden="true" tabindex="-1"></a> item<span class="op">.</span>genre <span class="op">=</span> <span class="cn">Some</span>(<span class="st">"Music"</span><span class="op">.</span>to_string())<span class="op">;</span></span>
|
||||
<span id="cb12-52"><a href="#cb12-52" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-53"><a href="#cb12-53" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-54"><a href="#cb12-54" aria-hidden="true" tabindex="-1"></a> adapted<span class="op">.</span>push(item)<span class="op">;</span></span>
|
||||
<span id="cb12-55"><a href="#cb12-55" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-56"><a href="#cb12-56" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-57"><a href="#cb12-57" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(adapted)</span>
|
||||
<span id="cb12-58"><a href="#cb12-58" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Points clés :</strong> - Stocker <code>source_track_id</code>
|
||||
dans les metadata du cache audio - Reconstituer l’ID correct lors de la
|
||||
récupération depuis playlist - Normaliser URLs (relatives → absolues) -
|
||||
Ajouter champs requis par certains clients UPnP</p>
|
||||
<h2 id="intégration-avec-lécosystème-pmomusic">Intégration avec
|
||||
l’écosystème PMOMusic</h2>
|
||||
<h3 id="avec-pmoplaylist">Avec pmoplaylist</h3>
|
||||
<p>Pour les sources dynamiques et les catalogues :</p>
|
||||
<div class="sourceCode" id="cb13"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoplaylist::</span><span class="op">{</span>PlaylistManager<span class="op">,</span> PlaylistRole<span class="op">};</span></span>
|
||||
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a><span class="co">// Créer une playlist persistante</span></span>
|
||||
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> manager <span class="op">=</span> PlaylistManager()<span class="op">;</span></span>
|
||||
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> writer <span class="op">=</span> manager</span>
|
||||
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>create_persistent_playlist_with_role(</span>
|
||||
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> <span class="st">"my-source-album-123"</span><span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> <span class="pp">PlaylistRole::</span>Album<span class="op">,</span></span>
|
||||
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a> )</span>
|
||||
<span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb13-12"><a href="#cb13-12" aria-hidden="true" tabindex="-1"></a><span class="co">// Configurer metadata</span></span>
|
||||
<span id="cb13-13"><a href="#cb13-13" aria-hidden="true" tabindex="-1"></a>writer<span class="op">.</span>set_title(<span class="st">"Album Title"</span><span class="op">.</span>to_string())<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb13-14"><a href="#cb13-14" aria-hidden="true" tabindex="-1"></a>writer<span class="op">.</span>set_artist(<span class="cn">Some</span>(<span class="st">"Artist Name"</span><span class="op">.</span>to_string()))<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb13-15"><a href="#cb13-15" aria-hidden="true" tabindex="-1"></a>writer<span class="op">.</span>set_cover_pk(<span class="cn">Some</span>(<span class="st">"cover-pk"</span><span class="op">.</span>to_string()))<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb13-16"><a href="#cb13-16" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb13-17"><a href="#cb13-17" aria-hidden="true" tabindex="-1"></a><span class="co">// Ajouter tracks avec cache lazy</span></span>
|
||||
<span id="cb13-18"><a href="#cb13-18" aria-hidden="true" tabindex="-1"></a>writer<span class="op">.</span>push_lazy_batch(<span class="pp">vec!</span>[<span class="st">"pk1"</span><span class="op">,</span> <span class="st">"pk2"</span><span class="op">,</span> <span class="st">"pk3"</span>])<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb13-19"><a href="#cb13-19" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb13-20"><a href="#cb13-20" aria-hidden="true" tabindex="-1"></a><span class="co">// Activer mode lazy (lookahead 2 tracks)</span></span>
|
||||
<span id="cb13-21"><a href="#cb13-21" aria-hidden="true" tabindex="-1"></a>manager<span class="op">.</span>enable_lazy_mode(<span class="st">"my-source-album-123"</span><span class="op">,</span> <span class="dv">2</span>)<span class="op">;</span></span></pre></div>
|
||||
<h3 id="avec-pmoaudiocache-et-pmocovers-via-sourcecachemanager">Avec
|
||||
pmoaudiocache et pmocovers (via SourceCacheManager)</h3>
|
||||
<div class="sourceCode" id="cb14"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmosource::</span>SourceCacheManager<span class="op">;</span></span>
|
||||
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="co">// Créer le manager centralisé</span></span>
|
||||
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> cache_manager <span class="op">=</span> <span class="pp">SourceCacheManager::</span>from_registry(<span class="st">"my-source"</span><span class="op">.</span>to_string())<span class="op">?;</span></span>
|
||||
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a><span class="co">// Enregistrer un LazyProvider</span></span>
|
||||
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a>cache_manager<span class="op">.</span>register_lazy_provider(<span class="pp">Arc::</span>new(<span class="pp">MyLazyProvider::</span>new(client)))<span class="op">;</span></span>
|
||||
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="co">// Cache eager (cover)</span></span>
|
||||
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> cover_pk <span class="op">=</span> cache_manager<span class="op">.</span>cache_cover(<span class="st">"https://example.com/cover.jpg"</span>)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="co">// Cache lazy (audio)</span></span>
|
||||
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> audio_pk <span class="op">=</span> cache_manager</span>
|
||||
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>cache_audio_lazy_with_provider(</span>
|
||||
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a> <span class="st">"MY-SOURCE:123"</span><span class="op">,</span> <span class="co">// Lazy PK</span></span>
|
||||
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a> <span class="cn">Some</span>(metadata)<span class="op">,</span></span>
|
||||
<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a> <span class="cn">Some</span>(cover_pk)<span class="op">,</span></span>
|
||||
<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a> )</span>
|
||||
<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a><span class="co">// Récupérer metadata</span></span>
|
||||
<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> value <span class="op">=</span> cache_manager<span class="op">.</span>get_audio_metadata(<span class="op">&</span>audio_pk<span class="op">,</span> <span class="st">"key"</span>)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span></pre></div>
|
||||
<p><strong>LazyProvider personnalisé :</strong></p>
|
||||
<div class="sourceCode" id="cb15"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoaudiocache::</span><span class="op">{</span>LazyProvider<span class="op">,</span> LazyProviderError<span class="op">};</span></span>
|
||||
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> MyLazyProvider <span class="op">{</span></span>
|
||||
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a> client<span class="op">:</span> Arc<span class="op"><</span>MyClient<span class="op">>,</span></span>
|
||||
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> LazyProvider <span class="cf">for</span> MyLazyProvider <span class="op">{</span></span>
|
||||
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> fetch_audio(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> lazy_pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">Vec</span><span class="op"><</span><span class="dt">u8</span><span class="op">>,</span> LazyProviderError<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a> <span class="co">// Extraire l'ID depuis le lazy_pk</span></span>
|
||||
<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> id <span class="op">=</span> lazy_pk</span>
|
||||
<span id="cb15-12"><a href="#cb15-12" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>strip_prefix(<span class="st">"MY-SOURCE:"</span>)</span>
|
||||
<span id="cb15-13"><a href="#cb15-13" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>ok_or_else(<span class="op">||</span> <span class="pp">LazyProviderError::</span>InvalidKey)<span class="op">?;</span></span>
|
||||
<span id="cb15-14"><a href="#cb15-14" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-15"><a href="#cb15-15" aria-hidden="true" tabindex="-1"></a> <span class="co">// Récupérer l'URL de streaming</span></span>
|
||||
<span id="cb15-16"><a href="#cb15-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> stream_url <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>client<span class="op">.</span>get_stream_url(id)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb15-17"><a href="#cb15-17" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">LazyProviderError::</span>FetchFailed(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb15-18"><a href="#cb15-18" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-19"><a href="#cb15-19" aria-hidden="true" tabindex="-1"></a> <span class="co">// Télécharger l'audio</span></span>
|
||||
<span id="cb15-20"><a href="#cb15-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> response <span class="op">=</span> <span class="pp">reqwest::</span>get(<span class="op">&</span>stream_url)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb15-21"><a href="#cb15-21" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">LazyProviderError::</span>FetchFailed(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb15-22"><a href="#cb15-22" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-23"><a href="#cb15-23" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> bytes <span class="op">=</span> response<span class="op">.</span>bytes()<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb15-24"><a href="#cb15-24" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="pp">LazyProviderError::</span>FetchFailed(e<span class="op">.</span>to_string()))<span class="op">?;</span></span>
|
||||
<span id="cb15-25"><a href="#cb15-25" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-26"><a href="#cb15-26" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(bytes<span class="op">.</span>to_vec())</span>
|
||||
<span id="cb15-27"><a href="#cb15-27" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb15-28"><a href="#cb15-28" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="avec-pmodidl">Avec pmodidl</h3>
|
||||
<p>Conversion de vos structures en DIDL-Lite :</p>
|
||||
<div class="sourceCode" id="cb16"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmodidl::</span><span class="op">{</span>Container<span class="op">,</span> Item<span class="op">,</span> Resource<span class="op">};</span></span>
|
||||
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a><span class="co">// Container</span></span>
|
||||
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> ToDIDLContainer <span class="op">{</span></span>
|
||||
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> to_didl_container(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> parent_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Container<span class="op">>;</span></span>
|
||||
<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb16-8"><a href="#cb16-8" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> ToDIDLContainer <span class="cf">for</span> MyAlbum <span class="op">{</span></span>
|
||||
<span id="cb16-9"><a href="#cb16-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> to_didl_container(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> parent_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Container<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb16-10"><a href="#cb16-10" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Container <span class="op">{</span></span>
|
||||
<span id="cb16-11"><a href="#cb16-11" aria-hidden="true" tabindex="-1"></a> id<span class="op">:</span> <span class="pp">format!</span>(<span class="st">"my-source:album:{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>id)<span class="op">,</span></span>
|
||||
<span id="cb16-12"><a href="#cb16-12" aria-hidden="true" tabindex="-1"></a> parent_id<span class="op">:</span> parent_id<span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb16-13"><a href="#cb16-13" aria-hidden="true" tabindex="-1"></a> restricted<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"1"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-14"><a href="#cb16-14" aria-hidden="true" tabindex="-1"></a> child_count<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>tracks_count<span class="op">.</span>map(<span class="op">|</span>c<span class="op">|</span> c<span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-15"><a href="#cb16-15" aria-hidden="true" tabindex="-1"></a> searchable<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"1"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-16"><a href="#cb16-16" aria-hidden="true" tabindex="-1"></a> title<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>title<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-17"><a href="#cb16-17" aria-hidden="true" tabindex="-1"></a> class<span class="op">:</span> <span class="st">"object.container.album.musicAlbum"</span><span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb16-18"><a href="#cb16-18" aria-hidden="true" tabindex="-1"></a> artist<span class="op">:</span> <span class="cn">Some</span>(<span class="kw">self</span><span class="op">.</span>artist<span class="op">.</span>name<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb16-19"><a href="#cb16-19" aria-hidden="true" tabindex="-1"></a> album_art<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>cover_url<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-20"><a href="#cb16-20" aria-hidden="true" tabindex="-1"></a> containers<span class="op">:</span> <span class="pp">vec!</span>[]<span class="op">,</span></span>
|
||||
<span id="cb16-21"><a href="#cb16-21" aria-hidden="true" tabindex="-1"></a> items<span class="op">:</span> <span class="pp">vec!</span>[]<span class="op">,</span></span>
|
||||
<span id="cb16-22"><a href="#cb16-22" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb16-23"><a href="#cb16-23" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb16-24"><a href="#cb16-24" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb16-25"><a href="#cb16-25" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb16-26"><a href="#cb16-26" aria-hidden="true" tabindex="-1"></a><span class="co">// Item</span></span>
|
||||
<span id="cb16-27"><a href="#cb16-27" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> ToDIDLItem <span class="op">{</span></span>
|
||||
<span id="cb16-28"><a href="#cb16-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> to_didl_item(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> parent_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Item<span class="op">>;</span></span>
|
||||
<span id="cb16-29"><a href="#cb16-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb16-30"><a href="#cb16-30" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb16-31"><a href="#cb16-31" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> ToDIDLItem <span class="cf">for</span> MyTrack <span class="op">{</span></span>
|
||||
<span id="cb16-32"><a href="#cb16-32" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> to_didl_item(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> parent_id<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Item<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb16-33"><a href="#cb16-33" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Item <span class="op">{</span></span>
|
||||
<span id="cb16-34"><a href="#cb16-34" aria-hidden="true" tabindex="-1"></a> id<span class="op">:</span> <span class="pp">format!</span>(<span class="st">"my-source:track:{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>id)<span class="op">,</span></span>
|
||||
<span id="cb16-35"><a href="#cb16-35" aria-hidden="true" tabindex="-1"></a> parent_id<span class="op">:</span> parent_id<span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb16-36"><a href="#cb16-36" aria-hidden="true" tabindex="-1"></a> restricted<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"1"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-37"><a href="#cb16-37" aria-hidden="true" tabindex="-1"></a> title<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>title<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-38"><a href="#cb16-38" aria-hidden="true" tabindex="-1"></a> creator<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>artist<span class="op">.</span>as_ref()<span class="op">.</span>map(<span class="op">|</span>a<span class="op">|</span> a<span class="op">.</span>name<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb16-39"><a href="#cb16-39" aria-hidden="true" tabindex="-1"></a> class<span class="op">:</span> <span class="st">"object.item.audioItem.musicTrack"</span><span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb16-40"><a href="#cb16-40" aria-hidden="true" tabindex="-1"></a> artist<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>artist<span class="op">.</span>as_ref()<span class="op">.</span>map(<span class="op">|</span>a<span class="op">|</span> a<span class="op">.</span>name<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb16-41"><a href="#cb16-41" aria-hidden="true" tabindex="-1"></a> album<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>album<span class="op">.</span>as_ref()<span class="op">.</span>map(<span class="op">|</span>a<span class="op">|</span> a<span class="op">.</span>title<span class="op">.</span>clone())<span class="op">,</span></span>
|
||||
<span id="cb16-42"><a href="#cb16-42" aria-hidden="true" tabindex="-1"></a> genre<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"Music"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-43"><a href="#cb16-43" aria-hidden="true" tabindex="-1"></a> album_art<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>cover_url<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-44"><a href="#cb16-44" aria-hidden="true" tabindex="-1"></a> album_art_pk<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>cover_pk<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-45"><a href="#cb16-45" aria-hidden="true" tabindex="-1"></a> date<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>release_date<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb16-46"><a href="#cb16-46" aria-hidden="true" tabindex="-1"></a> original_track_number<span class="op">:</span> <span class="cn">Some</span>(<span class="kw">self</span><span class="op">.</span>track_number)<span class="op">,</span></span>
|
||||
<span id="cb16-47"><a href="#cb16-47" aria-hidden="true" tabindex="-1"></a> resources<span class="op">:</span> <span class="pp">vec!</span>[Resource <span class="op">{</span></span>
|
||||
<span id="cb16-48"><a href="#cb16-48" aria-hidden="true" tabindex="-1"></a> protocol_info<span class="op">:</span> <span class="st">"http-get:*:audio/flac:*"</span><span class="op">.</span>to_string()<span class="op">,</span></span>
|
||||
<span id="cb16-49"><a href="#cb16-49" aria-hidden="true" tabindex="-1"></a> bits_per_sample<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>bit_depth<span class="op">.</span>map(<span class="op">|</span>b<span class="op">|</span> b<span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-50"><a href="#cb16-50" aria-hidden="true" tabindex="-1"></a> sample_frequency<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>sample_rate<span class="op">.</span>map(<span class="op">|</span>s<span class="op">|</span> s<span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-51"><a href="#cb16-51" aria-hidden="true" tabindex="-1"></a> nr_audio_channels<span class="op">:</span> <span class="cn">Some</span>(<span class="st">"2"</span><span class="op">.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb16-52"><a href="#cb16-52" aria-hidden="true" tabindex="-1"></a> duration<span class="op">:</span> <span class="kw">self</span><span class="op">.</span>duration_as_upnp_format()<span class="op">,</span></span>
|
||||
<span id="cb16-53"><a href="#cb16-53" aria-hidden="true" tabindex="-1"></a> url<span class="op">:</span> <span class="pp">format!</span>(<span class="st">"/audio/flac/{}"</span><span class="op">,</span> <span class="kw">self</span><span class="op">.</span>cache_pk)<span class="op">,</span></span>
|
||||
<span id="cb16-54"><a href="#cb16-54" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>]<span class="op">,</span></span>
|
||||
<span id="cb16-55"><a href="#cb16-55" aria-hidden="true" tabindex="-1"></a> descriptions<span class="op">:</span> <span class="pp">vec!</span>[]<span class="op">,</span></span>
|
||||
<span id="cb16-56"><a href="#cb16-56" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb16-57"><a href="#cb16-57" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb16-58"><a href="#cb16-58" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="checklist-de-mise-en-œuvre">Checklist de mise en œuvre</h2>
|
||||
<h3 id="phase-1-structure-de-base">Phase 1 : Structure de base</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Créer le crate
|
||||
<code>pmo<votre-source></code></label></li>
|
||||
<li><label><input type="checkbox" />Ajouter les dépendances dans
|
||||
<code>Cargo.toml</code></label></li>
|
||||
<li><label><input type="checkbox" />Créer le logo WebP 300x300px dans
|
||||
<code>assets/</code></label></li>
|
||||
<li><label><input type="checkbox" />Définir la structure
|
||||
principale</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>name()</code>,
|
||||
<code>id()</code>, <code>default_image()</code></label></li>
|
||||
</ul>
|
||||
<h3 id="phase-2-navigation-contentdirectory">Phase 2 : Navigation
|
||||
ContentDirectory</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Définir le schéma d’Object
|
||||
ID</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>root_container()</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>browse()</code>
|
||||
pour la racine</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>browse()</code>
|
||||
pour les sous-containers</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>browse()</code>
|
||||
pour les items</label></li>
|
||||
<li><label><input type="checkbox" />Tester la navigation avec un client
|
||||
UPnP</label></li>
|
||||
</ul>
|
||||
<h3 id="phase-3-résolution-duri">Phase 3 : Résolution d’URI</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>resolve_uri()</code> avec fallback</label></li>
|
||||
<li><label><input type="checkbox" />Intégrer avec
|
||||
<code>SourceCacheManager</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>LazyProvider</code> si cache lazy</label></li>
|
||||
<li><label><input type="checkbox" />Tester la lecture audio</label></li>
|
||||
</ul>
|
||||
<h3 id="phase-4-support-fifo-si-dynamique">Phase 4 : Support FIFO (si
|
||||
dynamique)</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Décider de la stratégie
|
||||
FIFO</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>supports_fifo() = true</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>append_track()</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>remove_oldest()</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>update_id()</code>
|
||||
et <code>last_change()</code></label></li>
|
||||
<li><label><input type="checkbox" />Enregistrer callbacks sur
|
||||
playlists</label></li>
|
||||
<li><label><input type="checkbox" />Tester ajout/suppression de
|
||||
tracks</label></li>
|
||||
</ul>
|
||||
<h3 id="phase-5-support-statique-si-catalogue">Phase 5 : Support
|
||||
statique (si catalogue)</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>supports_fifo() = false</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>get_items()</code>
|
||||
avec pagination</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter <code>search()</code> si
|
||||
applicable</label></li>
|
||||
<li><label><input type="checkbox" />Tester browsing du
|
||||
catalogue</label></li>
|
||||
</ul>
|
||||
<h3 id="phase-6-intégration-avancée">Phase 6 : Intégration avancée</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Implémenter <code>get_item()</code>
|
||||
pour metadata</label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>capabilities()</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter
|
||||
<code>get_available_formats()</code></label></li>
|
||||
<li><label><input type="checkbox" />Ajouter gestion d’erreurs
|
||||
robuste</label></li>
|
||||
<li><label><input type="checkbox" />Documenter le code</label></li>
|
||||
</ul>
|
||||
<h3 id="phase-7-tests-et-validation">Phase 7 : Tests et validation</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Écrire tests unitaires</label></li>
|
||||
<li><label><input type="checkbox" />Écrire tests
|
||||
d’intégration</label></li>
|
||||
<li><label><input type="checkbox" />Tester avec différents clients
|
||||
UPnP</label></li>
|
||||
<li><label><input type="checkbox" />Valider les
|
||||
performances</label></li>
|
||||
<li><label><input type="checkbox" />Documenter les
|
||||
limitations</label></li>
|
||||
</ul>
|
||||
<h2 id="exemples-de-référence">Exemples de référence</h2>
|
||||
<h3 id="radio-paradise-source-dynamique-fifo">Radio Paradise (source
|
||||
dynamique FIFO)</h3>
|
||||
<p><strong>Fichier :</strong> <code>pmoparadise/src/source.rs</code></p>
|
||||
<p><strong>Points d’intérêt :</strong> - Structure avec
|
||||
<code>Arc<RwLock<>></code> pour l’état partagé - Callbacks
|
||||
sur playlists pour détecter les changements - Notifier injecté pour
|
||||
ContentDirectory - Adaptation des IDs playlist → Radio Paradise -
|
||||
Support de 4 canaux avec sous-containers</p>
|
||||
<p><strong>Schema d’Object ID :</strong></p>
|
||||
<pre><code>radio-paradise # Racine
|
||||
radio-paradise:channel:{slug} # Canal (main, mellow, rock, eclectic)
|
||||
radio-paradise:channel:{slug}:live # Stream live
|
||||
radio-paradise:channel:{slug}:liveplaylist # Playlist live (queue)
|
||||
radio-paradise:channel:{slug}:liveplaylist:track:{pk} # Track dans queue
|
||||
radio-paradise:channel:{slug}:history # Historique
|
||||
radio-paradise:channel:{slug}:history:track:{pk} # Track dans historique</pre>
|
||||
<h3 id="qobuz-source-catalogue-avec-playlists-lazy">Qobuz (source
|
||||
catalogue avec playlists lazy)</h3>
|
||||
<p><strong>Fichier :</strong> <code>pmoqobuz/src/source.rs</code></p>
|
||||
<p><strong>Points d’intérêt :</strong> - <code>SourceCacheManager</code>
|
||||
centralisé - Cache lazy pour audio, eager pour covers -
|
||||
<code>LazyProvider</code> personnalisé - Playlists d’albums avec TTL (7
|
||||
jours) - Adaptation IDs playlist → Qobuz - Navigation hiérarchique
|
||||
complexe (Discover, Genres, Favorites)</p>
|
||||
<p><strong>Schema d’Object ID :</strong></p>
|
||||
<pre><code>qobuz # Racine
|
||||
qobuz:discover # Discover Catalog
|
||||
qobuz:discover:albums:ideal # Albums (Ideal Discography)
|
||||
qobuz:discover:artists # Artistes Featured
|
||||
qobuz:genres # Discover Genres
|
||||
qobuz:genre:{id} # Genre spécifique
|
||||
qobuz:genre:{id}:new-releases # Nouveautés du genre
|
||||
qobuz:favorites # My Music
|
||||
qobuz:favorites:albums # Albums favoris
|
||||
qobuz:album:{id} # Album spécifique
|
||||
qobuz:track:{id} # Track spécifique
|
||||
qobuz:playlist:{id} # Playlist spécifique
|
||||
qobuz:artist:{id} # Artiste spécifique</pre>
|
||||
<h2 id="conseils-dimplémentation">Conseils d’implémentation</h2>
|
||||
<h3 id="performance">Performance</h3>
|
||||
<ol type="1">
|
||||
<li><strong>Cache agressif</strong> : Utilisez
|
||||
<code>SourceCacheManager</code> pour tout</li>
|
||||
<li><strong>Pagination</strong> : Limitez le nombre d’items retournés
|
||||
(max 100)</li>
|
||||
<li><strong>Lazy loading</strong> : Ne chargez que ce qui est
|
||||
demandé</li>
|
||||
<li><strong>Rate limiting</strong> : Respectez les limites API de la
|
||||
source</li>
|
||||
<li><strong>Arc<></strong> : Partagez les données coûteuses</li>
|
||||
</ol>
|
||||
<h3 id="compatibilité-upnp">Compatibilité UPnP</h3>
|
||||
<ol type="1">
|
||||
<li><strong>Genre obligatoire</strong> : Certains clients (gupnp-av-cp)
|
||||
requièrent <code><upnp:genre></code></li>
|
||||
<li><strong>URLs absolues</strong> : Toujours retourner des URLs
|
||||
complètes (pas de chemins relatifs)</li>
|
||||
<li><strong>Protocol Info</strong> : Utilisez
|
||||
<code>http-get:*:audio/flac:*</code> pour FLAC</li>
|
||||
<li><strong>Duration</strong> : Format <code>H:MM:SS</code> (ex:
|
||||
<code>0:03:45</code>)</li>
|
||||
<li><strong>childCount</strong> : Optionnel mais recommandé pour
|
||||
l’UI</li>
|
||||
</ol>
|
||||
<h3 id="gestion-derreurs">Gestion d’erreurs</h3>
|
||||
<ol type="1">
|
||||
<li><strong>ObjectNotFound</strong> : ID invalide</li>
|
||||
<li><strong>BrowseError</strong> : Erreur générique de navigation</li>
|
||||
<li><strong>UriResolutionError</strong> : Impossible de résoudre
|
||||
l’URI</li>
|
||||
<li><strong>PlaylistError</strong> : Erreur d’interaction avec
|
||||
pmoplaylist</li>
|
||||
<li><strong>CacheError</strong> : Erreur de cache</li>
|
||||
</ol>
|
||||
<h3 id="thread-safety">Thread Safety</h3>
|
||||
<ol type="1">
|
||||
<li><strong>Arc<RwLock<>></strong> : Pour l’état mutable
|
||||
partagé</li>
|
||||
<li><strong>tokio::sync::RwLock</strong> : Pour l’async</li>
|
||||
<li><strong>Éviter Rc<></strong> : Pas thread-safe</li>
|
||||
<li><strong>Clone</strong> : Implémentez <code>Clone</code> pour
|
||||
<code>Arc<></code></li>
|
||||
</ol>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>L’implémentation d’une nouvelle <code>MusicSource</code> suit ces
|
||||
étapes :</p>
|
||||
<ol type="1">
|
||||
<li><strong>Définir le schéma d’Object ID</strong> : Hiérarchie claire
|
||||
et cohérente</li>
|
||||
<li><strong>Implémenter la navigation</strong> : <code>browse()</code>
|
||||
pour tous les niveaux</li>
|
||||
<li><strong>Résoudre les URIs</strong> : Cache local d’abord, puis
|
||||
original</li>
|
||||
<li><strong>Gérer le cache</strong> : <code>SourceCacheManager</code> +
|
||||
<code>LazyProvider</code></li>
|
||||
<li><strong>Adapter les IDs</strong> : Playlist → Schema de la
|
||||
source</li>
|
||||
<li><strong>Notifier les changements</strong> : <code>update_id</code> +
|
||||
callbacks</li>
|
||||
</ol>
|
||||
<p>Les exemples Radio Paradise et Qobuz couvrent les deux patterns
|
||||
principaux : - <strong>Dynamique FIFO</strong> : Radio Paradise -
|
||||
<strong>Catalogue lazy</strong> : Qobuz</p>
|
||||
<p>En suivant ces patterns, vous obtiendrez une source musicale
|
||||
performante, compatible UPnP, et bien intégrée dans l’écosystème
|
||||
PMOMusic.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
1023
Blackboard_HTML/Architecture_pmoconfig_ext.html
Normal file
1023
Blackboard_HTML/Architecture_pmoconfig_ext.html
Normal file
File diff suppressed because it is too large
Load Diff
929
Blackboard_HTML/Architecture_pmoserver_ext.html
Normal file
929
Blackboard_HTML/Architecture_pmoserver_ext.html
Normal file
@@ -0,0 +1,929 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pmoserver_ext</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="pattern-dextension-pmoserver-pmoserver_ext">Pattern d’extension
|
||||
PMOServer (<code>pmoserver_ext</code>)</h1>
|
||||
<h2 id="vue-densemble">Vue d’ensemble</h2>
|
||||
<p>Le pattern <code>pmoserver_ext</code> permet d’étendre les
|
||||
fonctionnalités du serveur HTTP <code>pmoserver</code> de manière
|
||||
modulaire et découplée. Chaque crate spécialisée peut ajouter ses
|
||||
propres routes HTTP sans que <code>pmoserver</code> ne dépende de ces
|
||||
crates.</p>
|
||||
<p><strong>Principe</strong> : Définir un trait d’extension que
|
||||
<code>pmoserver::Server</code> implémente via une feature Cargo.</p>
|
||||
<h2 id="anatomie-dune-extension">Anatomie d’une extension</h2>
|
||||
<h3 id="structure-du-module">1. Structure du module</h3>
|
||||
<p>Créer un module <code>pmoserver_ext.rs</code> dans la crate :</p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">// pmoXXX/src/pmoserver_ext.rs</span></span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="kw">crate</span><span class="pp">::</span><span class="op">{</span><span class="co">/* types internes de la crate */</span><span class="op">};</span></span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">async_trait::</span>async_trait<span class="op">;</span></span>
|
||||
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">axum::</span><span class="op">{</span>Router<span class="op">,</span> <span class="pp">routing::</span>get<span class="op">,</span> Json<span class="op">,</span> <span class="pp">extract::</span><span class="op">{</span>State<span class="op">,</span> <span class="dt">Path</span><span class="op">}};</span></span>
|
||||
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">std::sync::</span>Arc<span class="op">;</span></span></pre></div>
|
||||
<p>Déclarer le module dans <code>lib.rs</code> :</p>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">// pmoXXX/src/lib.rs</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">mod</span> pmoserver_ext<span class="op">;</span></span>
|
||||
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">use</span> <span class="pp">pmoserver_ext::</span>XXXExt<span class="op">;</span></span></pre></div>
|
||||
<p>Ajouter la feature dans <code>Cargo.toml</code> :</p>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode toml"><code class="sourceCode toml"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[features]</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="dt">pmoserver</span> <span class="op">=</span> <span class="op">[</span><span class="st">"dep:axum"</span><span class="op">,</span> <span class="st">"dep:async-trait"</span><span class="op">]</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="kw">[dependencies]</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="dt">axum</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"0.8"</span><span class="op">, </span><span class="dt">optional</span><span class="op"> =</span> <span class="cn">true</span><span class="op"> }</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="dt">async-trait</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"0.1"</span><span class="op">, </span><span class="dt">optional</span><span class="op"> =</span> <span class="cn">true</span><span class="op"> }</span></span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="dt">pmoserver</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">path</span><span class="op"> =</span> <span class="st">"../pmoserver"</span><span class="op"> }</span></span></pre></div>
|
||||
<h3 id="définir-le-trait-dextension">2. Définir le trait
|
||||
d’extension</h3>
|
||||
<p><strong>Convention de nommage</strong> : <code>{Domaine}Ext</code>
|
||||
avec méthodes préfixées <code>init_*</code></p>
|
||||
<div class="sourceCode" id="cb4"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">/// Trait pour étendre pmoserver avec les fonctionnalités XXX</span></span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> XXXExt <span class="op">{</span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Initialise l'extension XXX et enregistre les routes HTTP</span></span>
|
||||
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> <span class="co">///</span></span>
|
||||
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a> <span class="co">/// # Arguments</span></span>
|
||||
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a> <span class="co">/// * `param1` - Description du paramètre</span></span>
|
||||
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> <span class="co">///</span></span>
|
||||
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="co">/// # Returns</span></span>
|
||||
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Instance partagée de la ressource créée</span></span>
|
||||
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="co">///</span></span>
|
||||
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> <span class="co">/// # Exemple</span></span>
|
||||
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a> <span class="co">/// ```ignore</span></span>
|
||||
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a> <span class="co">/// use pmoserver::ServerBuilder;</span></span>
|
||||
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a> <span class="co">/// use pmoXXX::XXXExt;</span></span>
|
||||
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a> <span class="co">///</span></span>
|
||||
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a> <span class="co">/// let mut server = ServerBuilder::new(...).build();</span></span>
|
||||
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a> <span class="co">/// let resource = server.init_xxx(param1).await?;</span></span>
|
||||
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a> <span class="co">/// ```</span></span>
|
||||
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_xxx(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span><span class="op">,</span> param1<span class="op">:</span> <span class="dt">String</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>Resource<span class="op">>>;</span></span>
|
||||
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="implémenter-le-trait">3. Implémenter le trait</h3>
|
||||
<p>Implémenter le trait pour <code>pmoserver::Server</code> :</p>
|
||||
<div class="sourceCode" id="cb5"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> XXXExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_xxx(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span><span class="op">,</span> param1<span class="op">:</span> <span class="dt">String</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>Resource<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// 1. Créer la ressource interne</span></span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> resource <span class="op">=</span> <span class="pp">Arc::</span>new(<span class="pp">Resource::</span>new(param1)<span class="op">?</span>)<span class="op">;</span></span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> <span class="co">// 2. Créer l'état partagé pour les handlers</span></span>
|
||||
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state <span class="op">=</span> <span class="pp">XxxState::</span>new(resource<span class="op">.</span>clone())<span class="op">;</span></span>
|
||||
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a> <span class="co">// 3. Créer le router avec les routes</span></span>
|
||||
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> router <span class="op">=</span> create_xxx_router(state)<span class="op">;</span></span>
|
||||
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> <span class="co">// 4. Enregistrer le router sur le serveur</span></span>
|
||||
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_router(<span class="st">"/api/xxx"</span><span class="op">,</span> router)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="co">// 5. Retourner la ressource pour usage ultérieur</span></span>
|
||||
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(resource)</span>
|
||||
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="état-partagé-state">4. État partagé (State)</h3>
|
||||
<p>Créer une structure d’état cloneable pour les handlers :</p>
|
||||
<div class="sourceCode" id="cb6"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co">/// État partagé pour les handlers XXX</span></span>
|
||||
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="at">)]</span></span>
|
||||
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> XxxState <span class="op">{</span></span>
|
||||
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> resource<span class="op">:</span> Arc<span class="op"><</span>Resource<span class="op">>,</span></span>
|
||||
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> XxxState <span class="op">{</span></span>
|
||||
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> new(resource<span class="op">:</span> Arc<span class="op"><</span>Resource<span class="op">></span>) <span class="op">-></span> <span class="dt">Self</span> <span class="op">{</span></span>
|
||||
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a> <span class="dt">Self</span> <span class="op">{</span> resource <span class="op">}</span></span>
|
||||
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="créer-le-router">5. Créer le router</h3>
|
||||
<p>Définir les routes et handlers :</p>
|
||||
<div class="sourceCode" id="cb7"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="co">/// Crée le router pour l'API XXX</span></span>
|
||||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> create_xxx_router(state<span class="op">:</span> XxxState) <span class="op">-></span> Router <span class="op">{</span></span>
|
||||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a> <span class="pp">Router::</span>new()</span>
|
||||
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/items"</span><span class="op">,</span> get(list_items)<span class="op">.</span>post(create_item))</span>
|
||||
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/items/{id}"</span><span class="op">,</span> get(get_item)<span class="op">.</span>delete(delete_item))</span>
|
||||
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>with_state(state)</span>
|
||||
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">// Handlers</span></span>
|
||||
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> list_items(</span>
|
||||
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">></span></span>
|
||||
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> Json<span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>ItemSummary<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> state<span class="op">.</span>resource<span class="op">.</span>list_items()<span class="op">;</span></span>
|
||||
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a> Json(items)</span>
|
||||
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> get_item(</span>
|
||||
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a> <span class="dt">Path</span>(id)<span class="op">:</span> <span class="dt">Path</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Json<span class="op"><</span>Item<span class="op">>,</span> StatusCode<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>resource<span class="op">.</span>get_item(<span class="op">&</span>id)</span>
|
||||
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>ok_or(<span class="pp">StatusCode::</span>NOT_FOUND)</span>
|
||||
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map(Json)</span>
|
||||
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="méthodes-disponibles-du-serveur">Méthodes disponibles du
|
||||
serveur</h2>
|
||||
<p><code>pmoserver::Server</code> expose ces méthodes pour enregistrer
|
||||
des routes :</p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 56%" />
|
||||
<col style="width: 43%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Méthode</th>
|
||||
<th>Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>add_handler(path, handler)</code></td>
|
||||
<td>Ajoute un handler simple sans état</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add_handler_with_state(path, handler, state)</code></td>
|
||||
<td>Ajoute un handler avec état partagé</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add_router(path, router)</code></td>
|
||||
<td>Monte un sous-router Axum</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add_openapi(router, doc, tag)</code></td>
|
||||
<td>Enregistre une API avec documentation OpenAPI</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add_spa::<W>(path)</code></td>
|
||||
<td>Sert une Single Page Application (RustEmbed)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>base_url()</code></td>
|
||||
<td>Récupère l’URL de base du serveur</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="documentation-openapi-avec-utoipa">Documentation OpenAPI avec
|
||||
utoipa</h2>
|
||||
<p>La documentation OpenAPI est essentielle pour une extension
|
||||
<code>pmoserver</code>. Elle génère automatiquement une interface
|
||||
Swagger UI et documente les endpoints de l’API.</p>
|
||||
<h3 id="configuration-de-base">Configuration de base</h3>
|
||||
<p>Ajouter <code>utoipa</code> dans <code>Cargo.toml</code> :</p>
|
||||
<div class="sourceCode" id="cb8"><pre
|
||||
class="sourceCode toml"><code class="sourceCode toml"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[dependencies]</span></span>
|
||||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="dt">utoipa</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"5"</span><span class="op">, </span><span class="dt">features</span><span class="op"> =</span> <span class="op">[</span><span class="st">"axum_extras"</span><span class="op">] }</span></span>
|
||||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="dt">serde</span> <span class="op">=</span> <span class="op">{ </span><span class="dt">version</span><span class="op"> =</span> <span class="st">"1"</span><span class="op">, </span><span class="dt">features</span><span class="op"> =</span> <span class="op">[</span><span class="st">"derive"</span><span class="op">] }</span></span></pre></div>
|
||||
<h3 id="définir-les-schémas-de-données">1. Définir les schémas de
|
||||
données</h3>
|
||||
<p>Annoter les structures de réponse/requête avec
|
||||
<code>#[derive(ToSchema)]</code> :</p>
|
||||
<div class="sourceCode" id="cb9"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">serde::</span><span class="op">{</span>Serialize<span class="op">,</span> Deserialize<span class="op">};</span></span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">utoipa::</span>ToSchema<span class="op">;</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="co">/// Information sur un item</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ItemInfo <span class="op">{</span></span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> <span class="co">/// ID unique de l'item</span></span>
|
||||
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"item-123"</span><span class="at">)]</span></span>
|
||||
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> id<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Nom de l'item</span></span>
|
||||
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"Mon Item"</span><span class="at">)]</span></span>
|
||||
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> name<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Description optionnelle</span></span>
|
||||
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"Une description détaillée"</span><span class="at">)]</span></span>
|
||||
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> description<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Timestamp de création (millisecondes)</span></span>
|
||||
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="dv">1234567890</span><span class="at">)]</span></span>
|
||||
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> created_at<span class="op">:</span> <span class="dt">u64</span><span class="op">,</span></span>
|
||||
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a><span class="co">/// Liste d'items</span></span>
|
||||
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ItemList <span class="op">{</span></span>
|
||||
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Nombre total d'items</span></span>
|
||||
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> total<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span></span>
|
||||
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Items de la page courante</span></span>
|
||||
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> items<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>ItemInfo<span class="op">>,</span></span>
|
||||
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a><span class="co">/// Requête de création d'item</span></span>
|
||||
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Deserialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> CreateItemRequest <span class="op">{</span></span>
|
||||
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Nom de l'item à créer</span></span>
|
||||
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"Nouvel Item"</span><span class="at">)]</span></span>
|
||||
<span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> name<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Description optionnelle</span></span>
|
||||
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> description<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a><span class="co">/// Réponse d'erreur standard</span></span>
|
||||
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ErrorResponse <span class="op">{</span></span>
|
||||
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Message d'erreur</span></span>
|
||||
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"Item not found"</span><span class="at">)]</span></span>
|
||||
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> error<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Points clés</strong> : -
|
||||
<code>#[schema(example = "...")]</code> : Fournit des exemples pour la
|
||||
doc Swagger - Documenter chaque champ avec <code>///</code> pour
|
||||
apparaître dans l’API - Utiliser <code>Option<T></code> pour les
|
||||
champs optionnels</p>
|
||||
<h3 id="annoter-les-handlers">2. Annoter les handlers</h3>
|
||||
<p>Utiliser <code>#[utoipa::path(...)]</code> pour documenter chaque
|
||||
endpoint :</p>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co">/// GET /items - Liste tous les items</span></span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span><span class="pp">utoipa::</span>path<span class="at">(</span></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a> get<span class="op">,</span></span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> <span class="st">"/items"</span><span class="op">,</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> params<span class="at">(</span></span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"limit"</span> <span class="op">=</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u32</span><span class="op">>,</span> Query<span class="op">,</span> description <span class="op">=</span> <span class="st">"Nombre max d'items à retourner"</span><span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"offset"</span> <span class="op">=</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u32</span><span class="op">>,</span> Query<span class="op">,</span> description <span class="op">=</span> <span class="st">"Offset pour la pagination"</span><span class="at">)</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> responses<span class="at">(</span></span>
|
||||
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">200</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Liste des items"</span><span class="op">,</span> body <span class="op">=</span> ItemList<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">500</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Erreur serveur"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span></span>
|
||||
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a> tag <span class="op">=</span> <span class="st">"items"</span></span>
|
||||
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> list_items(</span>
|
||||
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a> Query(params)<span class="op">:</span> Query<span class="op"><</span>ListParams<span class="op">>,</span></span>
|
||||
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Json<span class="op"><</span>ItemList<span class="op">>,</span> (StatusCode<span class="op">,</span> Json<span class="op"><</span>ErrorResponse<span class="op">></span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> state<span class="op">.</span>resource<span class="op">.</span>list_items(params<span class="op">.</span>limit<span class="op">,</span> params<span class="op">.</span>offset)</span>
|
||||
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> (</span>
|
||||
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>INTERNAL_SERVER_ERROR<span class="op">,</span></span>
|
||||
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a> Json(ErrorResponse <span class="op">{</span> error<span class="op">:</span> e<span class="op">.</span>to_string() <span class="op">}</span>)</span>
|
||||
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a> ))<span class="op">?;</span></span>
|
||||
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Json(ItemList <span class="op">{</span></span>
|
||||
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a> total<span class="op">:</span> items<span class="op">.</span>len()<span class="op">,</span></span>
|
||||
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a> items<span class="op">,</span></span>
|
||||
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>))</span>
|
||||
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a><span class="co">/// GET /items/{id} - Récupère un item spécifique</span></span>
|
||||
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span><span class="pp">utoipa::</span>path<span class="at">(</span></span>
|
||||
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a> get<span class="op">,</span></span>
|
||||
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> <span class="st">"/items/{id}"</span><span class="op">,</span></span>
|
||||
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a> params<span class="at">(</span></span>
|
||||
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"id"</span> <span class="op">=</span> <span class="dt">String</span><span class="op">,</span> <span class="dt">Path</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"ID unique de l'item"</span><span class="at">)</span></span>
|
||||
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a> responses<span class="at">(</span></span>
|
||||
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">200</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Item trouvé"</span><span class="op">,</span> body <span class="op">=</span> ItemInfo<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">404</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Item non trouvé"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">500</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Erreur serveur"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span></span>
|
||||
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a> tag <span class="op">=</span> <span class="st">"items"</span></span>
|
||||
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> get_item(</span>
|
||||
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a> <span class="dt">Path</span>(id)<span class="op">:</span> <span class="dt">Path</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Json<span class="op"><</span>ItemInfo<span class="op">>,</span> (StatusCode<span class="op">,</span> Json<span class="op"><</span>ErrorResponse<span class="op">></span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>resource<span class="op">.</span>get_item(<span class="op">&</span>id)</span>
|
||||
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>ok_or_else(<span class="op">||</span> (</span>
|
||||
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>NOT_FOUND<span class="op">,</span></span>
|
||||
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a> Json(ErrorResponse <span class="op">{</span></span>
|
||||
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a> error<span class="op">:</span> <span class="pp">format!</span>(<span class="st">"Item {} not found"</span><span class="op">,</span> id)</span>
|
||||
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a> ))</span>
|
||||
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map(Json)</span>
|
||||
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a><span class="co">/// POST /items - Crée un nouvel item</span></span>
|
||||
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span><span class="pp">utoipa::</span>path<span class="at">(</span></span>
|
||||
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a> post<span class="op">,</span></span>
|
||||
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> <span class="st">"/items"</span><span class="op">,</span></span>
|
||||
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a> request_body <span class="op">=</span> CreateItemRequest<span class="op">,</span></span>
|
||||
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a> responses<span class="at">(</span></span>
|
||||
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">201</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Item créé"</span><span class="op">,</span> body <span class="op">=</span> ItemInfo<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">400</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Requête invalide"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">500</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Erreur serveur"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span></span>
|
||||
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a> tag <span class="op">=</span> <span class="st">"items"</span></span>
|
||||
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> create_item(</span>
|
||||
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a> Json(req)<span class="op">:</span> Json<span class="op"><</span>CreateItemRequest<span class="op">>,</span></span>
|
||||
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>(StatusCode<span class="op">,</span> Json<span class="op"><</span>ItemInfo<span class="op">></span>)<span class="op">,</span> (StatusCode<span class="op">,</span> Json<span class="op"><</span>ErrorResponse<span class="op">></span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> item <span class="op">=</span> state<span class="op">.</span>resource<span class="op">.</span>create_item(req<span class="op">.</span>name<span class="op">,</span> req<span class="op">.</span>description)</span>
|
||||
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> (</span>
|
||||
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>INTERNAL_SERVER_ERROR<span class="op">,</span></span>
|
||||
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a> Json(ErrorResponse <span class="op">{</span> error<span class="op">:</span> e<span class="op">.</span>to_string() <span class="op">}</span>)</span>
|
||||
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a> ))<span class="op">?;</span></span>
|
||||
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>((<span class="pp">StatusCode::</span>CREATED<span class="op">,</span> Json(item)))</span>
|
||||
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a><span class="co">/// DELETE /items/{id} - Supprime un item</span></span>
|
||||
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span><span class="pp">utoipa::</span>path<span class="at">(</span></span>
|
||||
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a> delete<span class="op">,</span></span>
|
||||
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> <span class="st">"/items/{id}"</span><span class="op">,</span></span>
|
||||
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a> params<span class="at">(</span></span>
|
||||
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"id"</span> <span class="op">=</span> <span class="dt">String</span><span class="op">,</span> <span class="dt">Path</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"ID unique de l'item"</span><span class="at">)</span></span>
|
||||
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a> responses<span class="at">(</span></span>
|
||||
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">204</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Item supprimé"</span><span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">404</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Item non trouvé"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">500</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Erreur serveur"</span><span class="op">,</span> body <span class="op">=</span> ErrorResponse<span class="at">)</span></span>
|
||||
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a> tag <span class="op">=</span> <span class="st">"items"</span></span>
|
||||
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> delete_item(</span>
|
||||
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a> <span class="dt">Path</span>(id)<span class="op">:</span> <span class="dt">Path</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>StatusCode<span class="op">,</span> (StatusCode<span class="op">,</span> Json<span class="op"><</span>ErrorResponse<span class="op">></span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>resource<span class="op">.</span>delete_item(<span class="op">&</span>id)</span>
|
||||
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> (</span>
|
||||
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>INTERNAL_SERVER_ERROR<span class="op">,</span></span>
|
||||
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a> Json(ErrorResponse <span class="op">{</span> error<span class="op">:</span> e<span class="op">.</span>to_string() <span class="op">}</span>)</span>
|
||||
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a> ))<span class="op">?;</span></span>
|
||||
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(<span class="pp">StatusCode::</span>NO_CONTENT)</span>
|
||||
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Structure de <code>#[utoipa::path]</code></strong> : -
|
||||
<strong>Méthode HTTP</strong> : <code>get</code>, <code>post</code>,
|
||||
<code>put</code>, <code>delete</code>, <code>patch</code> -
|
||||
<strong><code>path</code></strong> : Chemin de l’endpoint (doit
|
||||
correspondre au router) - <strong><code>params</code></strong> :
|
||||
Paramètres Path ou Query avec description -
|
||||
<strong><code>request_body</code></strong> : Type du body pour POST/PUT
|
||||
- <strong><code>responses</code></strong> : Liste des réponses possibles
|
||||
avec codes HTTP - <strong><code>tag</code></strong> : Groupe d’endpoints
|
||||
dans Swagger UI</p>
|
||||
<h3 id="créer-la-structure-openapi">3. Créer la structure OpenAPI</h3>
|
||||
<p>Définir une structure avec <code>#[derive(OpenApi)]</code> :</p>
|
||||
<div class="sourceCode" id="cb11"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">utoipa::</span>OpenApi<span class="op">;</span></span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co">/// Documentation OpenAPI pour l'API XXX</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span>OpenApi<span class="at">)]</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>openapi<span class="at">(</span></span>
|
||||
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a> info<span class="at">(</span></span>
|
||||
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a> title <span class="op">=</span> <span class="st">"XXX API"</span><span class="op">,</span></span>
|
||||
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a> version <span class="op">=</span> <span class="st">"1.0.0"</span><span class="op">,</span></span>
|
||||
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a> description <span class="op">=</span> <span class="st">r#"</span></span>
|
||||
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a><span class="st"># API REST pour XXX</span></span>
|
||||
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a><span class="st">Cette API permet de gérer les items XXX avec les fonctionnalités suivantes :</span></span>
|
||||
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a><span class="st">## Fonctionnalités</span></span>
|
||||
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a><span class="st">- **CRUD complet** : Création, lecture, mise à jour et suppression d'items</span></span>
|
||||
<span id="cb11-17"><a href="#cb11-17" aria-hidden="true" tabindex="-1"></a><span class="st">- **Pagination** : Support de limit/offset pour les listes</span></span>
|
||||
<span id="cb11-18"><a href="#cb11-18" aria-hidden="true" tabindex="-1"></a><span class="st">- **Filtrage** : Recherche par critères multiples</span></span>
|
||||
<span id="cb11-19"><a href="#cb11-19" aria-hidden="true" tabindex="-1"></a><span class="st">- **Validation** : Vérification automatique des données</span></span>
|
||||
<span id="cb11-20"><a href="#cb11-20" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-21"><a href="#cb11-21" aria-hidden="true" tabindex="-1"></a><span class="st">## Exemples d'utilisation</span></span>
|
||||
<span id="cb11-22"><a href="#cb11-22" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb11-23"><a href="#cb11-23" aria-hidden="true" tabindex="-1"></a><span class="st">### Lister les items</span></span></pre></div>
|
||||
<p>GET /api/xxx/items?limit=10&offset=0</p>
|
||||
<pre><code>
|
||||
### Créer un item</pre>
|
||||
<p>POST /api/xxx/items Content-Type: application/json</p>
|
||||
<p>{ “name”: “Mon Item”, “description”: “Description détaillée” }</p>
|
||||
<pre><code>
|
||||
### Récupérer un item</pre>
|
||||
<p>GET /api/xxx/items/item-123</p>
|
||||
<pre><code>
|
||||
### Supprimer un item</pre>
|
||||
<p>DELETE /api/xxx/items/item-123</p>
|
||||
<pre><code> "#
|
||||
),
|
||||
paths(
|
||||
list_items,
|
||||
get_item,
|
||||
create_item,
|
||||
delete_item,
|
||||
),
|
||||
components(schemas(
|
||||
ItemInfo,
|
||||
ItemList,
|
||||
CreateItemRequest,
|
||||
ErrorResponse,
|
||||
)),
|
||||
tags(
|
||||
(name = "items", description = "Opérations sur les items")
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;</pre>
|
||||
<p><strong>Sections importantes</strong> : -
|
||||
<strong><code>info</code></strong> : Titre, version et description
|
||||
Markdown de l’API - <strong><code>paths</code></strong> : Liste des
|
||||
fonctions handler annotées -
|
||||
<strong><code>components(schemas(...))</code></strong> : Liste des
|
||||
structures <code>ToSchema</code> - <strong><code>tags</code></strong> :
|
||||
Organisation des endpoints en groupes</p>
|
||||
<h3 id="enregistrer-lapi-avec-openapi">4. Enregistrer l’API avec
|
||||
OpenAPI</h3>
|
||||
<p>Dans l’implémentation du trait d’extension :</p>
|
||||
<div class="sourceCode" id="cb16"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> XxxExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_xxx(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>Resource<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> resource <span class="op">=</span> <span class="pp">Arc::</span>new(<span class="pp">Resource::</span>new()<span class="op">?</span>)<span class="op">;</span></span>
|
||||
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state <span class="op">=</span> XxxState <span class="op">{</span> resource<span class="op">:</span> resource<span class="op">.</span>clone() <span class="op">};</span></span>
|
||||
<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a> <span class="co">// Créer le router avec les routes</span></span>
|
||||
<span id="cb16-8"><a href="#cb16-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> router <span class="op">=</span> <span class="pp">Router::</span>new()</span>
|
||||
<span id="cb16-9"><a href="#cb16-9" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/items"</span><span class="op">,</span> get(list_items)<span class="op">.</span>post(create_item))</span>
|
||||
<span id="cb16-10"><a href="#cb16-10" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/items/{id}"</span><span class="op">,</span> get(get_item)<span class="op">.</span>delete(delete_item))</span>
|
||||
<span id="cb16-11"><a href="#cb16-11" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>with_state(state)<span class="op">;</span></span>
|
||||
<span id="cb16-12"><a href="#cb16-12" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb16-13"><a href="#cb16-13" aria-hidden="true" tabindex="-1"></a> <span class="co">// Enregistrer avec OpenAPI (génère aussi /swagger-ui/xxx)</span></span>
|
||||
<span id="cb16-14"><a href="#cb16-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> openapi <span class="op">=</span> <span class="pp">ApiDoc::</span>openapi()<span class="op">;</span></span>
|
||||
<span id="cb16-15"><a href="#cb16-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_openapi(router<span class="op">,</span> openapi<span class="op">,</span> <span class="st">"xxx"</span>)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb16-16"><a href="#cb16-16" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb16-17"><a href="#cb16-17" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(resource)</span>
|
||||
<span id="cb16-18"><a href="#cb16-18" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb16-19"><a href="#cb16-19" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Ce que fait <code>add_openapi</code></strong> : - Monte le
|
||||
router sur <code>/api/{tag}/</code> - Génère la spec OpenAPI JSON sur
|
||||
<code>/api/{tag}/openapi.json</code> - Crée une UI Swagger sur
|
||||
<code>/swagger-ui/{tag}/</code></p>
|
||||
<h3 id="exemple-complet-radio-paradise">5. Exemple complet : Radio
|
||||
Paradise</h3>
|
||||
<p><strong>Extrait de</strong>
|
||||
<code>pmoparadise/src/pmoserver_ext.rs:93-315</code></p>
|
||||
<div class="sourceCode" id="cb17"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="co">/// Information sur un morceau</span></span>
|
||||
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> SongInfo <span class="op">{</span></span>
|
||||
<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Index dans le block</span></span>
|
||||
<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> index<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span></span>
|
||||
<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Artiste</span></span>
|
||||
<span id="cb17-7"><a href="#cb17-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> artist<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb17-8"><a href="#cb17-8" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Titre</span></span>
|
||||
<span id="cb17-9"><a href="#cb17-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> title<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb17-10"><a href="#cb17-10" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Album</span></span>
|
||||
<span id="cb17-11"><a href="#cb17-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> album<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb17-12"><a href="#cb17-12" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Année</span></span>
|
||||
<span id="cb17-13"><a href="#cb17-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> year<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u32</span><span class="op">>,</span></span>
|
||||
<span id="cb17-14"><a href="#cb17-14" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Temps écoulé depuis le début du block (ms)</span></span>
|
||||
<span id="cb17-15"><a href="#cb17-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> elapsed_ms<span class="op">:</span> <span class="dt">u64</span><span class="op">,</span></span>
|
||||
<span id="cb17-16"><a href="#cb17-16" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Durée du morceau (ms)</span></span>
|
||||
<span id="cb17-17"><a href="#cb17-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> duration_ms<span class="op">:</span> <span class="dt">u64</span><span class="op">,</span></span>
|
||||
<span id="cb17-18"><a href="#cb17-18" aria-hidden="true" tabindex="-1"></a> <span class="co">/// URL de la pochette</span></span>
|
||||
<span id="cb17-19"><a href="#cb17-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> cover_url<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb17-20"><a href="#cb17-20" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb17-21"><a href="#cb17-21" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb17-22"><a href="#cb17-22" aria-hidden="true" tabindex="-1"></a><span class="co">/// Réponse pour l'URL de streaming</span></span>
|
||||
<span id="cb17-23"><a href="#cb17-23" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> ToSchema<span class="at">)]</span></span>
|
||||
<span id="cb17-24"><a href="#cb17-24" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> StreamUrlResponse <span class="op">{</span></span>
|
||||
<span id="cb17-25"><a href="#cb17-25" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Event ID du block</span></span>
|
||||
<span id="cb17-26"><a href="#cb17-26" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="dv">1234567</span><span class="at">)]</span></span>
|
||||
<span id="cb17-27"><a href="#cb17-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> event<span class="op">:</span> <span class="dt">u64</span><span class="op">,</span></span>
|
||||
<span id="cb17-28"><a href="#cb17-28" aria-hidden="true" tabindex="-1"></a> <span class="co">/// URL de streaming FLAC</span></span>
|
||||
<span id="cb17-29"><a href="#cb17-29" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="st">"https://apps.radioparadise.com/blocks/chan/0/4/1234567-1234580.flac"</span><span class="at">)]</span></span>
|
||||
<span id="cb17-30"><a href="#cb17-30" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> stream_url<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb17-31"><a href="#cb17-31" aria-hidden="true" tabindex="-1"></a> <span class="co">/// Durée totale (ms)</span></span>
|
||||
<span id="cb17-32"><a href="#cb17-32" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>schema<span class="at">(</span>example <span class="op">=</span> <span class="dv">900000</span><span class="at">)]</span></span>
|
||||
<span id="cb17-33"><a href="#cb17-33" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> length_ms<span class="op">:</span> <span class="dt">u64</span><span class="op">,</span></span>
|
||||
<span id="cb17-34"><a href="#cb17-34" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb17-35"><a href="#cb17-35" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb17-36"><a href="#cb17-36" aria-hidden="true" tabindex="-1"></a><span class="co">/// GET /stream-url/{event_id} - Récupère l'URL de streaming</span></span>
|
||||
<span id="cb17-37"><a href="#cb17-37" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span><span class="pp">utoipa::</span>path<span class="at">(</span></span>
|
||||
<span id="cb17-38"><a href="#cb17-38" aria-hidden="true" tabindex="-1"></a> get<span class="op">,</span></span>
|
||||
<span id="cb17-39"><a href="#cb17-39" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> <span class="st">"/stream-url/{event_id}"</span><span class="op">,</span></span>
|
||||
<span id="cb17-40"><a href="#cb17-40" aria-hidden="true" tabindex="-1"></a> params<span class="at">(</span></span>
|
||||
<span id="cb17-41"><a href="#cb17-41" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"event_id"</span> <span class="op">=</span> <span class="dt">u64</span><span class="op">,</span> <span class="dt">Path</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Event ID du block"</span><span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-42"><a href="#cb17-42" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span><span class="st">"channel"</span> <span class="op">=</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u8</span><span class="op">>,</span> Query<span class="op">,</span> description <span class="op">=</span> <span class="st">"Channel ID (0-3)"</span><span class="at">)</span></span>
|
||||
<span id="cb17-43"><a href="#cb17-43" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-44"><a href="#cb17-44" aria-hidden="true" tabindex="-1"></a> responses<span class="at">(</span></span>
|
||||
<span id="cb17-45"><a href="#cb17-45" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">200</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"URL de streaming"</span><span class="op">,</span> body <span class="op">=</span> StreamUrlResponse<span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-46"><a href="#cb17-46" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>status <span class="op">=</span> <span class="dv">500</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Erreur serveur"</span><span class="at">)</span></span>
|
||||
<span id="cb17-47"><a href="#cb17-47" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-48"><a href="#cb17-48" aria-hidden="true" tabindex="-1"></a> tag <span class="op">=</span> <span class="st">"Radio Paradise"</span></span>
|
||||
<span id="cb17-49"><a href="#cb17-49" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb17-50"><a href="#cb17-50" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> get_stream_url(</span>
|
||||
<span id="cb17-51"><a href="#cb17-51" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>RadioParadiseState<span class="op">>,</span></span>
|
||||
<span id="cb17-52"><a href="#cb17-52" aria-hidden="true" tabindex="-1"></a> <span class="dt">Path</span>(event_id)<span class="op">:</span> <span class="dt">Path</span><span class="op"><</span><span class="dt">u64</span><span class="op">>,</span></span>
|
||||
<span id="cb17-53"><a href="#cb17-53" aria-hidden="true" tabindex="-1"></a> Query(params)<span class="op">:</span> Query<span class="op"><</span>ParadiseQuery<span class="op">>,</span></span>
|
||||
<span id="cb17-54"><a href="#cb17-54" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Json<span class="op"><</span>StreamUrlResponse<span class="op">>,</span> StatusCode<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb17-55"><a href="#cb17-55" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> client <span class="op">=</span> state<span class="op">.</span>client_for_params(<span class="op">&</span>params)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb17-56"><a href="#cb17-56" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> block <span class="op">=</span> client<span class="op">.</span>get_block(<span class="cn">Some</span>(event_id))<span class="op">.</span><span class="kw">await</span><span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="op">{</span></span>
|
||||
<span id="cb17-57"><a href="#cb17-57" aria-hidden="true" tabindex="-1"></a> <span class="pp">tracing::error!</span>(<span class="st">"Failed to fetch block {}: {}"</span><span class="op">,</span> event_id<span class="op">,</span> e)<span class="op">;</span></span>
|
||||
<span id="cb17-58"><a href="#cb17-58" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>INTERNAL_SERVER_ERROR</span>
|
||||
<span id="cb17-59"><a href="#cb17-59" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)<span class="op">?;</span></span>
|
||||
<span id="cb17-60"><a href="#cb17-60" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb17-61"><a href="#cb17-61" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Json(StreamUrlResponse <span class="op">{</span></span>
|
||||
<span id="cb17-62"><a href="#cb17-62" aria-hidden="true" tabindex="-1"></a> event<span class="op">:</span> block<span class="op">.</span>event<span class="op">,</span></span>
|
||||
<span id="cb17-63"><a href="#cb17-63" aria-hidden="true" tabindex="-1"></a> stream_url<span class="op">:</span> block<span class="op">.</span>url<span class="op">,</span></span>
|
||||
<span id="cb17-64"><a href="#cb17-64" aria-hidden="true" tabindex="-1"></a> length_ms<span class="op">:</span> block<span class="op">.</span>length<span class="op">,</span></span>
|
||||
<span id="cb17-65"><a href="#cb17-65" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>))</span>
|
||||
<span id="cb17-66"><a href="#cb17-66" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb17-67"><a href="#cb17-67" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb17-68"><a href="#cb17-68" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span>OpenApi<span class="at">)]</span></span>
|
||||
<span id="cb17-69"><a href="#cb17-69" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>openapi<span class="at">(</span></span>
|
||||
<span id="cb17-70"><a href="#cb17-70" aria-hidden="true" tabindex="-1"></a> info<span class="at">(</span></span>
|
||||
<span id="cb17-71"><a href="#cb17-71" aria-hidden="true" tabindex="-1"></a> title <span class="op">=</span> <span class="st">"Radio Paradise API"</span><span class="op">,</span></span>
|
||||
<span id="cb17-72"><a href="#cb17-72" aria-hidden="true" tabindex="-1"></a> version <span class="op">=</span> <span class="st">"1.0.0"</span><span class="op">,</span></span>
|
||||
<span id="cb17-73"><a href="#cb17-73" aria-hidden="true" tabindex="-1"></a> description <span class="op">=</span> <span class="st">"API REST pour accéder aux métadonnées Radio Paradise"</span></span>
|
||||
<span id="cb17-74"><a href="#cb17-74" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-75"><a href="#cb17-75" aria-hidden="true" tabindex="-1"></a> paths<span class="at">(</span></span>
|
||||
<span id="cb17-76"><a href="#cb17-76" aria-hidden="true" tabindex="-1"></a> get_now_playing<span class="op">,</span></span>
|
||||
<span id="cb17-77"><a href="#cb17-77" aria-hidden="true" tabindex="-1"></a> get_current_block<span class="op">,</span></span>
|
||||
<span id="cb17-78"><a href="#cb17-78" aria-hidden="true" tabindex="-1"></a> get_stream_url<span class="op">,</span></span>
|
||||
<span id="cb17-79"><a href="#cb17-79" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span><span class="op">,</span></span>
|
||||
<span id="cb17-80"><a href="#cb17-80" aria-hidden="true" tabindex="-1"></a> components<span class="at">(</span>schemas<span class="at">(</span></span>
|
||||
<span id="cb17-81"><a href="#cb17-81" aria-hidden="true" tabindex="-1"></a> SongInfo<span class="op">,</span></span>
|
||||
<span id="cb17-82"><a href="#cb17-82" aria-hidden="true" tabindex="-1"></a> StreamUrlResponse<span class="op">,</span></span>
|
||||
<span id="cb17-83"><a href="#cb17-83" aria-hidden="true" tabindex="-1"></a> <span class="at">))</span><span class="op">,</span></span>
|
||||
<span id="cb17-84"><a href="#cb17-84" aria-hidden="true" tabindex="-1"></a> tags<span class="at">(</span></span>
|
||||
<span id="cb17-85"><a href="#cb17-85" aria-hidden="true" tabindex="-1"></a> <span class="at">(</span>name <span class="op">=</span> <span class="st">"Radio Paradise"</span><span class="op">,</span> description <span class="op">=</span> <span class="st">"Endpoints Radio Paradise"</span><span class="at">)</span></span>
|
||||
<span id="cb17-86"><a href="#cb17-86" aria-hidden="true" tabindex="-1"></a> <span class="at">)</span></span>
|
||||
<span id="cb17-87"><a href="#cb17-87" aria-hidden="true" tabindex="-1"></a><span class="at">)]</span></span>
|
||||
<span id="cb17-88"><a href="#cb17-88" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> RadioParadiseApiDoc<span class="op">;</span></span></pre></div>
|
||||
<h3 id="résultat-interface-swagger">Résultat : Interface Swagger</h3>
|
||||
<p>Après avoir appelé <code>init_xxx()</code>, l’API est accessible
|
||||
:</p>
|
||||
<ul>
|
||||
<li><strong>API JSON</strong> :
|
||||
<code>http://localhost:8080/api/xxx/</code></li>
|
||||
<li><strong>Spec OpenAPI</strong> :
|
||||
<code>http://localhost:8080/api/xxx/openapi.json</code></li>
|
||||
<li><strong>Swagger UI</strong> :
|
||||
<code>http://localhost:8080/swagger-ui/xxx/</code></li>
|
||||
</ul>
|
||||
<p>L’interface Swagger permet : - Parcourir tous les endpoints avec leur
|
||||
documentation - Tester les requêtes directement depuis le navigateur -
|
||||
Voir les schémas de données avec exemples - Consulter les codes de
|
||||
réponse HTTP possibles</p>
|
||||
<h2 id="patterns-courants">Patterns courants</h2>
|
||||
<h3 id="pattern-1-extension-simple-avec-router">Pattern 1 : Extension
|
||||
simple avec router</h3>
|
||||
<p><strong>Exemple</strong> : <code>pmoparadise</code>
|
||||
(pmoparadise/src/pmoserver_ext.rs:367-392)</p>
|
||||
<div class="sourceCode" id="cb18"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> RadioParadiseExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_radioparadise(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>State<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state <span class="op">=</span> <span class="pp">RadioParadiseState::</span>new()<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb18-5"><a href="#cb18-5" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-6"><a href="#cb18-6" aria-hidden="true" tabindex="-1"></a> <span class="co">// Créer le router API</span></span>
|
||||
<span id="cb18-7"><a href="#cb18-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> api_router <span class="op">=</span> create_api_router(state<span class="op">.</span>clone())<span class="op">;</span></span>
|
||||
<span id="cb18-8"><a href="#cb18-8" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-9"><a href="#cb18-9" aria-hidden="true" tabindex="-1"></a> <span class="co">// Enregistrer avec OpenAPI</span></span>
|
||||
<span id="cb18-10"><a href="#cb18-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_openapi(api_router<span class="op">,</span> <span class="pp">ApiDoc::</span>openapi()<span class="op">,</span> <span class="st">"radioparadise"</span>)</span>
|
||||
<span id="cb18-11"><a href="#cb18-11" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb18-12"><a href="#cb18-12" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-13"><a href="#cb18-13" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(state)</span>
|
||||
<span id="cb18-14"><a href="#cb18-14" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-15"><a href="#cb18-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="pattern-2-extension-avec-cache-et-fichiers">Pattern 2 :
|
||||
Extension avec cache et fichiers</h3>
|
||||
<p><strong>Exemple</strong> : <code>pmoaudiocache</code>
|
||||
(pmoaudiocache/src/lib.rs:225-260)</p>
|
||||
<div class="sourceCode" id="cb19"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> AudioCacheExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_audio_cache(</span>
|
||||
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span><span class="op">,</span></span>
|
||||
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a> cache_dir<span class="op">:</span> <span class="op">&</span><span class="dt">str</span><span class="op">,</span></span>
|
||||
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a> limit<span class="op">:</span> <span class="dt">usize</span><span class="op">,</span></span>
|
||||
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a> ) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>Cache<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> cache <span class="op">=</span> <span class="pp">Arc::</span>new(new_cache(cache_dir<span class="op">,</span> limit)<span class="op">?</span>)<span class="op">;</span></span>
|
||||
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb19-10"><a href="#cb19-10" aria-hidden="true" tabindex="-1"></a> <span class="co">// Router pour servir les fichiers FLAC</span></span>
|
||||
<span id="cb19-11"><a href="#cb19-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> file_router <span class="op">=</span> create_file_router(cache<span class="op">.</span>clone()<span class="op">,</span> <span class="st">"audio/flac"</span>)<span class="op">;</span></span>
|
||||
<span id="cb19-12"><a href="#cb19-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_router(<span class="st">"/"</span><span class="op">,</span> file_router)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb19-13"><a href="#cb19-13" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb19-14"><a href="#cb19-14" aria-hidden="true" tabindex="-1"></a> <span class="co">// API REST</span></span>
|
||||
<span id="cb19-15"><a href="#cb19-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> api_router <span class="op">=</span> <span class="pp">Router::</span>new()</span>
|
||||
<span id="cb19-16"><a href="#cb19-16" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/"</span><span class="op">,</span> get(list)<span class="op">.</span>post(add))</span>
|
||||
<span id="cb19-17"><a href="#cb19-17" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/{pk}"</span><span class="op">,</span> get(get_info)<span class="op">.</span>delete(delete))</span>
|
||||
<span id="cb19-18"><a href="#cb19-18" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>with_state(cache<span class="op">.</span>clone())<span class="op">;</span></span>
|
||||
<span id="cb19-19"><a href="#cb19-19" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb19-20"><a href="#cb19-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_openapi(api_router<span class="op">,</span> <span class="pp">ApiDoc::</span>openapi()<span class="op">,</span> <span class="st">"audio"</span>)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb19-21"><a href="#cb19-21" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb19-22"><a href="#cb19-22" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(cache)</span>
|
||||
<span id="cb19-23"><a href="#cb19-23" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb19-24"><a href="#cb19-24" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="pattern-3-extension-avec-routes-dynamiques">Pattern 3 :
|
||||
Extension avec routes dynamiques</h3>
|
||||
<p><strong>Exemple</strong> : <code>pmomediaserver</code>
|
||||
(pmomediaserver/src/paradise_streaming.rs:70-148)</p>
|
||||
<div class="sourceCode" id="cb20"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> ParadiseStreamingExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_paradise_streaming(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>Manager<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb20-4"><a href="#cb20-4" aria-hidden="true" tabindex="-1"></a> <span class="co">// 1. Récupérer/créer les ressources partagées</span></span>
|
||||
<span id="cb20-5"><a href="#cb20-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> audio_cache <span class="op">=</span> get_or_init_audio_cache(<span class="kw">self</span>)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb20-6"><a href="#cb20-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> manager <span class="op">=</span> <span class="pp">Arc::</span>new(<span class="pp">Manager::</span>new(audio_cache)<span class="op">.</span><span class="kw">await</span><span class="op">?</span>)<span class="op">;</span></span>
|
||||
<span id="cb20-7"><a href="#cb20-7" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb20-8"><a href="#cb20-8" aria-hidden="true" tabindex="-1"></a> <span class="co">// 2. Créer l'état partagé</span></span>
|
||||
<span id="cb20-9"><a href="#cb20-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state <span class="op">=</span> <span class="pp">Arc::</span>new(StreamingState <span class="op">{</span> manager<span class="op">:</span> manager<span class="op">.</span>clone() <span class="op">}</span>)<span class="op">;</span></span>
|
||||
<span id="cb20-10"><a href="#cb20-10" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb20-11"><a href="#cb20-11" aria-hidden="true" tabindex="-1"></a> <span class="co">// 3. Enregistrer les routes pour chaque canal</span></span>
|
||||
<span id="cb20-12"><a href="#cb20-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> descriptor <span class="kw">in</span> ALL_CHANNELS<span class="op">.</span>iter() <span class="op">{</span></span>
|
||||
<span id="cb20-13"><a href="#cb20-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> slug <span class="op">=</span> descriptor<span class="op">.</span>slug<span class="op">;</span></span>
|
||||
<span id="cb20-14"><a href="#cb20-14" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb20-15"><a href="#cb20-15" aria-hidden="true" tabindex="-1"></a> <span class="co">// Route streaming FLAC</span></span>
|
||||
<span id="cb20-16"><a href="#cb20-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> path <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"/stream/{}/flac"</span><span class="op">,</span> slug)<span class="op">;</span></span>
|
||||
<span id="cb20-17"><a href="#cb20-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_handler_with_state(</span>
|
||||
<span id="cb20-18"><a href="#cb20-18" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span>path<span class="op">,</span></span>
|
||||
<span id="cb20-19"><a href="#cb20-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">move</span> <span class="op">|</span>State(s)<span class="op">:</span> State<span class="op"><</span>Arc<span class="op"><</span>StreamingState<span class="op">>>|</span> <span class="kw">async</span> <span class="kw">move</span> <span class="op">{</span></span>
|
||||
<span id="cb20-20"><a href="#cb20-20" aria-hidden="true" tabindex="-1"></a> stream_flac(s<span class="op">.</span>manager<span class="op">.</span>clone()<span class="op">,</span> descriptor<span class="op">.</span>id)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb20-21"><a href="#cb20-21" aria-hidden="true" tabindex="-1"></a> <span class="op">},</span></span>
|
||||
<span id="cb20-22"><a href="#cb20-22" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb20-23"><a href="#cb20-23" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb20-24"><a href="#cb20-24" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb20-25"><a href="#cb20-25" aria-hidden="true" tabindex="-1"></a> <span class="co">// Route streaming OGG</span></span>
|
||||
<span id="cb20-26"><a href="#cb20-26" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> path <span class="op">=</span> <span class="pp">format!</span>(<span class="st">"/stream/{}/ogg"</span><span class="op">,</span> slug)<span class="op">;</span></span>
|
||||
<span id="cb20-27"><a href="#cb20-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_handler_with_state(</span>
|
||||
<span id="cb20-28"><a href="#cb20-28" aria-hidden="true" tabindex="-1"></a> <span class="op">&</span>path<span class="op">,</span></span>
|
||||
<span id="cb20-29"><a href="#cb20-29" aria-hidden="true" tabindex="-1"></a> <span class="kw">move</span> <span class="op">|</span>State(s)<span class="op">:</span> State<span class="op"><</span>Arc<span class="op"><</span>StreamingState<span class="op">>>|</span> <span class="kw">async</span> <span class="kw">move</span> <span class="op">{</span></span>
|
||||
<span id="cb20-30"><a href="#cb20-30" aria-hidden="true" tabindex="-1"></a> stream_ogg(s<span class="op">.</span>manager<span class="op">.</span>clone()<span class="op">,</span> descriptor<span class="op">.</span>id)<span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb20-31"><a href="#cb20-31" aria-hidden="true" tabindex="-1"></a> <span class="op">},</span></span>
|
||||
<span id="cb20-32"><a href="#cb20-32" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>clone()<span class="op">,</span></span>
|
||||
<span id="cb20-33"><a href="#cb20-33" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb20-34"><a href="#cb20-34" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb20-35"><a href="#cb20-35" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb20-36"><a href="#cb20-36" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(manager)</span>
|
||||
<span id="cb20-37"><a href="#cb20-37" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb20-38"><a href="#cb20-38" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="gestion-des-opérations-longues">Gestion des opérations
|
||||
longues</h2>
|
||||
<h3 id="utiliser-spawn_blocking-pour-le-code-synchrone">Utiliser
|
||||
<code>spawn_blocking</code> pour le code synchrone</h3>
|
||||
<p>Pour éviter de bloquer le runtime Tokio avec du code synchrone :</p>
|
||||
<div class="sourceCode" id="cb21"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> list_renderers(</span>
|
||||
<span id="cb21-2"><a href="#cb21-2" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>ControlPointState<span class="op">></span></span>
|
||||
<span id="cb21-3"><a href="#cb21-3" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> Json<span class="op"><</span><span class="dt">Vec</span><span class="op"><</span>Summary<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb21-4"><a href="#cb21-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> control_point <span class="op">=</span> state<span class="op">.</span>control_point<span class="op">.</span>clone()<span class="op">;</span></span>
|
||||
<span id="cb21-5"><a href="#cb21-5" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb21-6"><a href="#cb21-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> summaries <span class="op">=</span> <span class="pp">tokio::task::</span>spawn_blocking(<span class="kw">move</span> <span class="op">||</span> <span class="op">{</span></span>
|
||||
<span id="cb21-7"><a href="#cb21-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> renderers <span class="op">=</span> control_point<span class="op">.</span>list_music_renderers()<span class="op">;</span></span>
|
||||
<span id="cb21-8"><a href="#cb21-8" aria-hidden="true" tabindex="-1"></a> renderers<span class="op">.</span>into_iter()</span>
|
||||
<span id="cb21-9"><a href="#cb21-9" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map(<span class="op">|</span>r<span class="op">|</span> <span class="pp">Summary::</span>from(<span class="op">&</span>r))</span>
|
||||
<span id="cb21-10"><a href="#cb21-10" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>collect()</span>
|
||||
<span id="cb21-11"><a href="#cb21-11" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb21-12"><a href="#cb21-12" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb21-13"><a href="#cb21-13" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>unwrap_or_default()<span class="op">;</span></span>
|
||||
<span id="cb21-14"><a href="#cb21-14" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb21-15"><a href="#cb21-15" aria-hidden="true" tabindex="-1"></a> Json(summaries)</span>
|
||||
<span id="cb21-16"><a href="#cb21-16" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="ajouter-des-timeouts-pour-les-opérations-réseau">Ajouter des
|
||||
timeouts pour les opérations réseau</h3>
|
||||
<div class="sourceCode" id="cb22"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> COMMAND_TIMEOUT<span class="op">:</span> Duration <span class="op">=</span> <span class="pp">Duration::</span>from_secs(<span class="dv">5</span>)<span class="op">;</span></span>
|
||||
<span id="cb22-2"><a href="#cb22-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb22-3"><a href="#cb22-3" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> play_renderer(</span>
|
||||
<span id="cb22-4"><a href="#cb22-4" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>ControlPointState<span class="op">>,</span></span>
|
||||
<span id="cb22-5"><a href="#cb22-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">Path</span>(id)<span class="op">:</span> <span class="dt">Path</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb22-6"><a href="#cb22-6" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Json<span class="op"><</span>Response<span class="op">>,</span> (StatusCode<span class="op">,</span> Json<span class="op"><</span><span class="bu">Error</span><span class="op">></span>)<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb22-7"><a href="#cb22-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> renderer <span class="op">=</span> state<span class="op">.</span>get_renderer(<span class="op">&</span>id)</span>
|
||||
<span id="cb22-8"><a href="#cb22-8" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>ok_or((<span class="pp">StatusCode::</span>NOT_FOUND<span class="op">,</span> Json(<span class="bu">Error</span><span class="pp">::</span>not_found())))<span class="op">?;</span></span>
|
||||
<span id="cb22-9"><a href="#cb22-9" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb22-10"><a href="#cb22-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> play_task <span class="op">=</span> <span class="pp">tokio::task::</span>spawn_blocking(<span class="kw">move</span> <span class="op">||</span> renderer<span class="op">.</span>play())<span class="op">;</span></span>
|
||||
<span id="cb22-11"><a href="#cb22-11" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb22-12"><a href="#cb22-12" aria-hidden="true" tabindex="-1"></a> <span class="pp">time::</span>timeout(COMMAND_TIMEOUT<span class="op">,</span> play_task)</span>
|
||||
<span id="cb22-13"><a href="#cb22-13" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="kw">await</span></span>
|
||||
<span id="cb22-14"><a href="#cb22-14" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>_<span class="op">|</span> (</span>
|
||||
<span id="cb22-15"><a href="#cb22-15" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>GATEWAY_TIMEOUT<span class="op">,</span></span>
|
||||
<span id="cb22-16"><a href="#cb22-16" aria-hidden="true" tabindex="-1"></a> Json(<span class="bu">Error</span><span class="pp">::</span>timeout())</span>
|
||||
<span id="cb22-17"><a href="#cb22-17" aria-hidden="true" tabindex="-1"></a> ))<span class="op">?</span></span>
|
||||
<span id="cb22-18"><a href="#cb22-18" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> (</span>
|
||||
<span id="cb22-19"><a href="#cb22-19" aria-hidden="true" tabindex="-1"></a> <span class="pp">StatusCode::</span>INTERNAL_SERVER_ERROR<span class="op">,</span></span>
|
||||
<span id="cb22-20"><a href="#cb22-20" aria-hidden="true" tabindex="-1"></a> Json(<span class="bu">Error</span><span class="pp">::</span>internal(e))</span>
|
||||
<span id="cb22-21"><a href="#cb22-21" aria-hidden="true" tabindex="-1"></a> ))<span class="op">??;</span></span>
|
||||
<span id="cb22-22"><a href="#cb22-22" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb22-23"><a href="#cb22-23" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(Json(<span class="pp">Response::</span>success()))</span>
|
||||
<span id="cb22-24"><a href="#cb22-24" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="utiliser-spawn-pour-les-tâches-en-arrière-plan">Utiliser
|
||||
<code>spawn</code> pour les tâches en arrière-plan</h3>
|
||||
<p>Pour les opérations qui ne nécessitent pas d’attendre le résultat
|
||||
:</p>
|
||||
<div class="sourceCode" id="cb23"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> trigger_action(</span>
|
||||
<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a> State(state)<span class="op">:</span> State<span class="op"><</span>XxxState<span class="op">>,</span></span>
|
||||
<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a> Json(req)<span class="op">:</span> Json<span class="op"><</span>Request<span class="op">>,</span></span>
|
||||
<span id="cb23-4"><a href="#cb23-4" aria-hidden="true" tabindex="-1"></a>) <span class="op">-></span> Json<span class="op"><</span>Response<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb23-5"><a href="#cb23-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// Valider la requête</span></span>
|
||||
<span id="cb23-6"><a href="#cb23-6" aria-hidden="true" tabindex="-1"></a> state<span class="op">.</span>validate(<span class="op">&</span>req)<span class="op">?;</span></span>
|
||||
<span id="cb23-7"><a href="#cb23-7" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb23-8"><a href="#cb23-8" aria-hidden="true" tabindex="-1"></a> <span class="co">// Lancer l'action en arrière-plan</span></span>
|
||||
<span id="cb23-9"><a href="#cb23-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state_clone <span class="op">=</span> state<span class="op">.</span>clone()<span class="op">;</span></span>
|
||||
<span id="cb23-10"><a href="#cb23-10" aria-hidden="true" tabindex="-1"></a> <span class="pp">tokio::task::</span>spawn(<span class="kw">async</span> <span class="kw">move</span> <span class="op">{</span></span>
|
||||
<span id="cb23-11"><a href="#cb23-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">match</span> state_clone<span class="op">.</span>perform_action(req)<span class="op">.</span><span class="kw">await</span> <span class="op">{</span></span>
|
||||
<span id="cb23-12"><a href="#cb23-12" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(_) <span class="op">=></span> <span class="pp">debug!</span>(<span class="st">"Action completed"</span>)<span class="op">,</span></span>
|
||||
<span id="cb23-13"><a href="#cb23-13" aria-hidden="true" tabindex="-1"></a> <span class="cn">Err</span>(e) <span class="op">=></span> <span class="pp">warn!</span>(<span class="st">"Action failed: {}"</span><span class="op">,</span> e)<span class="op">,</span></span>
|
||||
<span id="cb23-14"><a href="#cb23-14" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb23-15"><a href="#cb23-15" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)<span class="op">;</span></span>
|
||||
<span id="cb23-16"><a href="#cb23-16" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb23-17"><a href="#cb23-17" aria-hidden="true" tabindex="-1"></a> <span class="co">// Retourner immédiatement</span></span>
|
||||
<span id="cb23-18"><a href="#cb23-18" aria-hidden="true" tabindex="-1"></a> Json(<span class="pp">Response::</span>accepted())</span>
|
||||
<span id="cb23-19"><a href="#cb23-19" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="checklist-dimplémentation">Checklist d’implémentation</h2>
|
||||
<h3 id="configuration-de-base-1">Configuration de base</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Créer le module
|
||||
<code>pmoserver_ext.rs</code> avec
|
||||
<code>#[cfg(feature = "pmoserver")]</code></label></li>
|
||||
<li><label><input type="checkbox" />Ajouter la feature
|
||||
<code>pmoserver</code> dans <code>Cargo.toml</code> avec dépendances
|
||||
optionnelles</label></li>
|
||||
<li><label><input type="checkbox" />Re-exporter le trait dans
|
||||
<code>lib.rs</code></label></li>
|
||||
</ul>
|
||||
<h3 id="définition-du-trait">Définition du trait</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Définir le trait
|
||||
<code>{Domaine}Ext</code> avec méthode <code>init_*</code></label></li>
|
||||
<li><label><input type="checkbox" />Créer la structure
|
||||
<code>{Domaine}State</code> avec
|
||||
<code>#[derive(Clone)]</code></label></li>
|
||||
<li><label><input type="checkbox" />Implémenter le trait pour
|
||||
<code>pmoserver::Server</code></label></li>
|
||||
</ul>
|
||||
<h3 id="documentation-openapi">Documentation OpenAPI</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Ajouter <code>utoipa</code> dans les
|
||||
dépendances</label></li>
|
||||
<li><label><input type="checkbox" />Définir les schémas de
|
||||
réponse/requête avec <code>#[derive(ToSchema)]</code></label></li>
|
||||
<li><label><input type="checkbox" />Ajouter des exemples avec
|
||||
<code>#[schema(example = "...")]</code></label></li>
|
||||
<li><label><input type="checkbox" />Annoter chaque handler avec
|
||||
<code>#[utoipa::path(...)]</code></label></li>
|
||||
<li><label><input type="checkbox" />Créer la structure
|
||||
<code>#[derive(OpenApi)]</code> avec documentation complète</label></li>
|
||||
<li><label><input type="checkbox" />Lister tous les paths et schemas
|
||||
dans <code>#[openapi(...)]</code></label></li>
|
||||
</ul>
|
||||
<h3 id="handlers-et-routes">Handlers et routes</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Créer les handlers avec les
|
||||
extracteurs Axum appropriés</label></li>
|
||||
<li><label><input type="checkbox" />Gérer les erreurs avec des codes
|
||||
HTTP sémantiques</label></li>
|
||||
<li><label><input type="checkbox" />Créer le router et l’enregistrer
|
||||
avec <code>add_openapi()</code></label></li>
|
||||
<li><label><input type="checkbox" />Ajouter des logs (debug, info, warn,
|
||||
error)</label></li>
|
||||
</ul>
|
||||
<h3 id="performance-et-robustesse">Performance et robustesse</h3>
|
||||
<ul class="task-list">
|
||||
<li><label><input type="checkbox" />Utiliser <code>spawn_blocking</code>
|
||||
pour le code synchrone</label></li>
|
||||
<li><label><input type="checkbox" />Ajouter des timeouts pour les
|
||||
opérations réseau</label></li>
|
||||
<li><label><input type="checkbox" />Utiliser <code>spawn</code> pour les
|
||||
tâches en arrière-plan si nécessaire</label></li>
|
||||
</ul>
|
||||
<h2 id="exemple-complet-minimal">Exemple complet minimal</h2>
|
||||
<div class="sourceCode" id="cb24"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb24-1"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a><span class="co">// pmoexample/src/pmoserver_ext.rs</span></span>
|
||||
<span id="cb24-2"><a href="#cb24-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb24-3"><a href="#cb24-3" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-4"><a href="#cb24-4" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">async_trait::</span>async_trait<span class="op">;</span></span>
|
||||
<span id="cb24-5"><a href="#cb24-5" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-6"><a href="#cb24-6" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">axum::</span><span class="op">{</span>Router<span class="op">,</span> <span class="pp">routing::</span>get<span class="op">,</span> Json<span class="op">,</span> <span class="pp">extract::</span>State<span class="op">};</span></span>
|
||||
<span id="cb24-7"><a href="#cb24-7" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-8"><a href="#cb24-8" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">std::sync::</span>Arc<span class="op">;</span></span>
|
||||
<span id="cb24-9"><a href="#cb24-9" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-10"><a href="#cb24-10" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="kw">crate</span><span class="pp">::</span>ExampleResource<span class="op">;</span></span>
|
||||
<span id="cb24-11"><a href="#cb24-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb24-12"><a href="#cb24-12" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-13"><a href="#cb24-13" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Clone</span><span class="at">)]</span></span>
|
||||
<span id="cb24-14"><a href="#cb24-14" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ExampleState <span class="op">{</span></span>
|
||||
<span id="cb24-15"><a href="#cb24-15" aria-hidden="true" tabindex="-1"></a> resource<span class="op">:</span> Arc<span class="op"><</span>ExampleResource<span class="op">>,</span></span>
|
||||
<span id="cb24-16"><a href="#cb24-16" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb24-17"><a href="#cb24-17" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb24-18"><a href="#cb24-18" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-19"><a href="#cb24-19" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb24-20"><a href="#cb24-20" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> ExampleExt <span class="op">{</span></span>
|
||||
<span id="cb24-21"><a href="#cb24-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_example(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>ExampleResource<span class="op">>>;</span></span>
|
||||
<span id="cb24-22"><a href="#cb24-22" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb24-23"><a href="#cb24-23" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb24-24"><a href="#cb24-24" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-25"><a href="#cb24-25" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>async_trait<span class="at">]</span></span>
|
||||
<span id="cb24-26"><a href="#cb24-26" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> ExampleExt <span class="cf">for</span> <span class="pp">pmoserver::</span>Server <span class="op">{</span></span>
|
||||
<span id="cb24-27"><a href="#cb24-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> <span class="kw">fn</span> init_example(<span class="op">&</span><span class="kw">mut</span> <span class="kw">self</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>Arc<span class="op"><</span>ExampleResource<span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb24-28"><a href="#cb24-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> resource <span class="op">=</span> <span class="pp">Arc::</span>new(<span class="pp">ExampleResource::</span>new())<span class="op">;</span></span>
|
||||
<span id="cb24-29"><a href="#cb24-29" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> state <span class="op">=</span> ExampleState <span class="op">{</span> resource<span class="op">:</span> resource<span class="op">.</span>clone() <span class="op">};</span></span>
|
||||
<span id="cb24-30"><a href="#cb24-30" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb24-31"><a href="#cb24-31" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> router <span class="op">=</span> <span class="pp">Router::</span>new()</span>
|
||||
<span id="cb24-32"><a href="#cb24-32" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>route(<span class="st">"/items"</span><span class="op">,</span> get(list_items))</span>
|
||||
<span id="cb24-33"><a href="#cb24-33" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>with_state(state)<span class="op">;</span></span>
|
||||
<span id="cb24-34"><a href="#cb24-34" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb24-35"><a href="#cb24-35" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>add_router(<span class="st">"/api/example"</span><span class="op">,</span> router)<span class="op">.</span><span class="kw">await</span><span class="op">;</span></span>
|
||||
<span id="cb24-36"><a href="#cb24-36" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb24-37"><a href="#cb24-37" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(resource)</span>
|
||||
<span id="cb24-38"><a href="#cb24-38" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb24-39"><a href="#cb24-39" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb24-40"><a href="#cb24-40" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb24-41"><a href="#cb24-41" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>cfg<span class="at">(</span>feature <span class="op">=</span> <span class="st">"pmoserver"</span><span class="at">)]</span></span>
|
||||
<span id="cb24-42"><a href="#cb24-42" aria-hidden="true" tabindex="-1"></a><span class="kw">async</span> <span class="kw">fn</span> list_items(State(state)<span class="op">:</span> State<span class="op"><</span>ExampleState<span class="op">></span>) <span class="op">-></span> Json<span class="op"><</span><span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">>></span> <span class="op">{</span></span>
|
||||
<span id="cb24-43"><a href="#cb24-43" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> items <span class="op">=</span> state<span class="op">.</span>resource<span class="op">.</span>list()<span class="op">;</span></span>
|
||||
<span id="cb24-44"><a href="#cb24-44" aria-hidden="true" tabindex="-1"></a> Json(items)</span>
|
||||
<span id="cb24-45"><a href="#cb24-45" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h2 id="références">Références</h2>
|
||||
<h3 id="exemples-dans-le-codebase">Exemples dans le codebase</h3>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 28%" />
|
||||
<col style="width: 36%" />
|
||||
<col style="width: 36%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Crate</th>
|
||||
<th>Fichier</th>
|
||||
<th>Pattern</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pmoparadise</code></td>
|
||||
<td><code>src/pmoserver_ext.rs:367-392</code></td>
|
||||
<td>Extension simple avec OpenAPI</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pmoaudiocache</code></td>
|
||||
<td><code>src/lib.rs:225-260</code></td>
|
||||
<td>Extension avec cache et fichiers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pmomediaserver</code></td>
|
||||
<td><code>src/paradise_streaming.rs:70-148</code></td>
|
||||
<td>Extension avec routes dynamiques</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pmocontrol</code></td>
|
||||
<td><code>src/pmoserver_ext.rs:68-92</code></td>
|
||||
<td>Handlers avec <code>spawn_blocking</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pmoapp</code></td>
|
||||
<td><code>src/lib.rs:145-165</code></td>
|
||||
<td>Extension SPA avec RustEmbed</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="dépendances-communes">Dépendances communes</h3>
|
||||
<ul>
|
||||
<li><code>axum</code> : Framework HTTP (Router, handlers,
|
||||
extracteurs)</li>
|
||||
<li><code>async-trait</code> : Support des traits async</li>
|
||||
<li><code>tokio</code> : Runtime async (spawn, spawn_blocking,
|
||||
timeout)</li>
|
||||
<li><code>anyhow</code> : Gestion d’erreurs pour init</li>
|
||||
<li><code>tracing</code> : Logging structuré</li>
|
||||
<li><code>utoipa</code> : Documentation OpenAPI/Swagger</li>
|
||||
<li><code>serde</code> : Sérialisation JSON</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
1034
Blackboard_HTML/Done_Pinnable_cache_item.html
Normal file
1034
Blackboard_HTML/Done_Pinnable_cache_item.html
Normal file
File diff suppressed because it is too large
Load Diff
209
Blackboard_HTML/Done_WeabApp_debouncingSSE.html
Normal file
209
Blackboard_HTML/Done_WeabApp_debouncingSSE.html
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WeabApp_debouncingSSE</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="rapport-suppression-de-la-logique-de-débouncing-sse">Rapport :
|
||||
Suppression de la logique de débouncing SSE</h1>
|
||||
<p><strong>Date</strong>: 2026-01-12 <strong>Tâche</strong>:
|
||||
WeabApp_debouncingSSE.md</p>
|
||||
<h2 id="objectif">Objectif</h2>
|
||||
<p>Supprimer la logique de débouncing inutile sur le canal SSE de
|
||||
l’application web PMOControl, puisque le serveur contrôle déjà le flux
|
||||
des événements.</p>
|
||||
<h2 id="analyse-préalable">Analyse préalable</h2>
|
||||
<p>J’ai identifié trois endroits avec des mécanismes de temporisation
|
||||
dans l’application web :</p>
|
||||
<h3 id="mediabrowser.vue---débouncing-sse-à-supprimer">1.
|
||||
MediaBrowser.vue - Débouncing SSE (À SUPPRIMER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Débouncing</strong>: 200ms après invalidation du cache</li>
|
||||
<li><strong>Cooldown</strong>: 2 secondes entre les rechargements</li>
|
||||
<li><strong>Justification originale</strong>: “dédupliquer les
|
||||
événements SSE dans le même batch (polling 500ms)”</li>
|
||||
<li><strong>Problème</strong>: Cette logique est redondante puisque le
|
||||
serveur contrôle déjà le flux SSE</li>
|
||||
</ul>
|
||||
<h3 id="userenderers.ts---smart-fetching-à-conserver">2. useRenderers.ts
|
||||
- Smart fetching (À CONSERVER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Mécanisme</strong>: Comparaison des timestamps
|
||||
<code>lastEventAt</code> vs <code>lastSnapshotAt</code></li>
|
||||
<li><strong>But</strong>: Éviter de refetch un snapshot déjà à jour</li>
|
||||
<li><strong>Justification</strong>: Ce n’est PAS du débouncing, c’est
|
||||
une optimisation intelligente qui évite des appels API inutiles</li>
|
||||
</ul>
|
||||
<h3 id="volumecontrol.vue---ui-debouncing-à-conserver">3.
|
||||
VolumeControl.vue - UI debouncing (À CONSERVER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Débouncing</strong>: 300ms sur les changements de
|
||||
volume</li>
|
||||
<li><strong>But</strong>: Réduire les appels API pendant que
|
||||
l’utilisateur fait glisser le curseur</li>
|
||||
<li><strong>Justification</strong>: Débouncing légitime pour l’interface
|
||||
utilisateur</li>
|
||||
</ul>
|
||||
<h2 id="modifications-effectuées">Modifications effectuées</h2>
|
||||
<h3
|
||||
id="fichier-modifié-pmoappwebappsrccomponentspmocontrolmediabrowser.vue">Fichier
|
||||
modifié:
|
||||
<code>pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue</code></h3>
|
||||
<h4 id="suppression-des-variables-de-débouncing-ligne-27">1. Suppression
|
||||
des variables de débouncing (ligne ~27)</h4>
|
||||
<p><strong>Avant</strong>:</p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Flags pour gérer le rechargement automatique avec debounce et cooldown</span></span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> isRefreshing <span class="op">=</span> <span class="fu">ref</span>(<span class="kw">false</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> refreshTimeoutId <span class="op">=</span> <span class="fu">ref</span><span class="op"><</span><span class="dt">number</span> <span class="op">|</span> <span class="dt">null</span><span class="op">></span>(<span class="kw">null</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> lastRefreshTime <span class="op">=</span> <span class="fu">ref</span><span class="op"><</span><span class="dt">number</span><span class="op">></span>(<span class="dv">0</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> REFRESH_COOLDOWN_MS <span class="op">=</span> <span class="dv">2000</span><span class="op">;</span> <span class="co">// Ne pas recharger plus d'une fois toutes les 2 secondes</span></span></pre></div>
|
||||
<p><strong>Après</strong>:</p>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Flag pour gérer le rechargement automatique</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> isRefreshing <span class="op">=</span> <span class="fu">ref</span>(<span class="kw">false</span>)<span class="op">;</span></span></pre></div>
|
||||
<h4 id="simplification-du-watcher-de-cache-ligne-53">2. Simplification
|
||||
du watcher de cache (ligne ~53)</h4>
|
||||
<p><strong>Avant</strong>:</p>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="co">// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co">// ce qui déclenche un événement ContainersUpdated qui invalide le cache</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Utilise un debounce de 3 secondes pour regrouper les multiples invalidations</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">// et un cooldown de 5 secondes pour éviter les rechargements successifs</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="fu">watch</span>(</span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> () <span class="kw">=></span> browseData<span class="op">.</span><span class="at">value</span><span class="op">,</span></span>
|
||||
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> (data) <span class="kw">=></span> {</span>
|
||||
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>data <span class="op">&&</span> props<span class="op">.</span><span class="at">containerId</span> <span class="op">&&</span> <span class="op">!</span>loading<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="co">// Vérifier le cooldown: ignorer si on a rechargé il y a moins de 5 secondes</span></span>
|
||||
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> timeSinceLastRefresh <span class="op">=</span> <span class="bu">Date</span><span class="op">.</span><span class="fu">now</span>() <span class="op">-</span> lastRefreshTime<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
|
||||
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (timeSinceLastRefresh <span class="op"><</span> REFRESH_COOLDOWN_MS) {</span>
|
||||
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé mais cooldown actif (</span><span class="sc">${</span><span class="bu">Math</span><span class="op">.</span><span class="fu">round</span>((REFRESH_COOLDOWN_MS <span class="op">-</span> timeSinceLastRefresh) <span class="op">/</span> <span class="dv">1000</span>)<span class="sc">}</span><span class="vs">s restantes), rechargement ignoré`</span><span class="op">,</span></span>
|
||||
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span><span class="op">;</span></span>
|
||||
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="co">// Annuler tout timeout en cours</span></span>
|
||||
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">!==</span> <span class="kw">null</span>) {</span>
|
||||
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="pp">clearTimeout</span>(refreshTimeoutId<span class="op">.</span><span class="at">value</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="co">// Planifier le rechargement après 200ms</span></span>
|
||||
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="bu">window</span><span class="op">.</span><span class="fu">setTimeout</span>(<span class="kw">async</span> () <span class="kw">=></span> {</span>
|
||||
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>isRefreshing<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé pour </span><span class="sc">${</span>props<span class="op">.</span><span class="at">serverId</span><span class="sc">}</span><span class="vs">/</span><span class="sc">${</span>props<span class="op">.</span><span class="at">containerId</span><span class="sc">}</span><span class="vs">, rechargement après debounce...`</span><span class="op">,</span></span>
|
||||
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">true</span><span class="op">;</span></span>
|
||||
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">await</span> <span class="fu">browseContainer</span>(</span>
|
||||
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a> props<span class="op">.</span><span class="at">serverId</span><span class="op">,</span></span>
|
||||
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> props<span class="op">.</span><span class="at">containerId</span><span class="op">,</span></span>
|
||||
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="kw">false</span><span class="op">,</span></span>
|
||||
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> lastRefreshTime<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="bu">Date</span><span class="op">.</span><span class="fu">now</span>()<span class="op">;</span></span>
|
||||
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">false</span><span class="op">;</span></span>
|
||||
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">null</span><span class="op">;</span></span>
|
||||
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span> <span class="dv">200</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
|
||||
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></pre></div>
|
||||
<p><strong>Après</strong>:</p>
|
||||
<div class="sourceCode" id="cb4"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)</span></span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="co">// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,</span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co">// ce qui déclenche un événement ContainersUpdated qui invalide le cache</span></span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Le serveur contrôle déjà le flux SSE, pas besoin de debouncing côté client</span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="fu">watch</span>(</span>
|
||||
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> () <span class="kw">=></span> browseData<span class="op">.</span><span class="at">value</span><span class="op">,</span></span>
|
||||
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> (data) <span class="kw">=></span> {</span>
|
||||
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a> <span class="co">// Si browseData devient undefined alors que containerId est présent,</span></span>
|
||||
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> <span class="co">// et qu'on n'est pas déjà en train de charger, recharger immédiatement</span></span>
|
||||
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>data <span class="op">&&</span> props<span class="op">.</span><span class="at">containerId</span> <span class="op">&&</span> <span class="op">!</span>loading<span class="op">.</span><span class="at">value</span> <span class="op">&&</span> <span class="op">!</span>isRefreshing<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé pour </span><span class="sc">${</span>props<span class="op">.</span><span class="at">serverId</span><span class="sc">}</span><span class="vs">/</span><span class="sc">${</span>props<span class="op">.</span><span class="at">containerId</span><span class="sc">}</span><span class="vs">, rechargement...`</span><span class="op">,</span></span>
|
||||
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">true</span><span class="op">;</span></span>
|
||||
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">await</span> <span class="fu">browseContainer</span>(props<span class="op">.</span><span class="at">serverId</span><span class="op">,</span> props<span class="op">.</span><span class="at">containerId</span><span class="op">,</span> <span class="kw">false</span>)<span class="op">;</span></span>
|
||||
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">false</span><span class="op">;</span></span>
|
||||
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
|
||||
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></pre></div>
|
||||
<h2 id="résultats">Résultats</h2>
|
||||
<h3 id="changements-de-comportement">Changements de comportement</h3>
|
||||
<ul>
|
||||
<li><strong>Avant</strong>: Délai de 200ms + cooldown de 2s entre les
|
||||
rechargements de cache</li>
|
||||
<li><strong>Après</strong>: Rechargement immédiat dès l’invalidation du
|
||||
cache</li>
|
||||
<li><strong>Impact</strong>: Réactivité améliorée de l’interface, les
|
||||
mises à jour apparaissent immédiatement</li>
|
||||
</ul>
|
||||
<h3 id="réduction-de-complexité">Réduction de complexité</h3>
|
||||
<ul>
|
||||
<li><strong>3 variables supprimées</strong>:
|
||||
<code>refreshTimeoutId</code>, <code>lastRefreshTime</code>,
|
||||
<code>REFRESH_COOLDOWN_MS</code></li>
|
||||
<li><strong>Logique simplifiée</strong>: De ~40 lignes à ~10 lignes dans
|
||||
le watcher</li>
|
||||
<li><strong>Code plus lisible</strong>: Intention claire sans mécanismes
|
||||
de temporisation complexes</li>
|
||||
</ul>
|
||||
<h3 id="tests">Tests</h3>
|
||||
<ul>
|
||||
<li>✓ Le projet compile sans erreurs TypeScript</li>
|
||||
<li>✓ Le flag <code>isRefreshing</code> empêche toujours les
|
||||
rechargements concurrents</li>
|
||||
<li>✓ Les autres composants (useRenderers.ts, VolumeControl.vue)
|
||||
conservent leurs optimisations légitimes</li>
|
||||
</ul>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>La suppression du débouncing et du cooldown dans MediaBrowser.vue
|
||||
simplifie le code tout en améliorant la réactivité de l’interface.
|
||||
Puisque le serveur contrôle déjà le flux SSE, ces mécanismes côté client
|
||||
étaient redondants et ajoutaient une latence artificielle.</p>
|
||||
<p>Le code est maintenant plus simple, plus réactif, et fait confiance
|
||||
au serveur pour contrôler la fréquence des événements SSE.</p>
|
||||
<h2 id="fichiers-modifiés">Fichiers modifiés</h2>
|
||||
<ul>
|
||||
<li><code>pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue</code></li>
|
||||
</ul>
|
||||
<h2 id="lignes-de-code">Lignes de code</h2>
|
||||
<ul>
|
||||
<li><strong>Supprimées</strong>: ~35 lignes (logique de
|
||||
débouncing/cooldown)</li>
|
||||
<li><strong>Ajoutées</strong>: ~5 lignes (logique simplifiée)</li>
|
||||
<li><strong>Net</strong>: -30 lignes</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
518
Blackboard_HTML/Report_Pinnable_cache_item.html
Normal file
518
Blackboard_HTML/Report_Pinnable_cache_item.html
Normal file
@@ -0,0 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pinnable_cache_item</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1
|
||||
id="rapport-implémentation-des-items-épinglables-dans-pmocache">Rapport
|
||||
: Implémentation des items épinglables dans PMOcache</h1>
|
||||
<h2 id="résumé">Résumé</h2>
|
||||
<p>Implémentation réussie de la fonctionnalité d’items épinglables dans
|
||||
la crate PMOcache, permettant de protéger certains items de l’éviction
|
||||
automatique par la politique LRU. Cette fonctionnalité inclut également
|
||||
un système de TTL (Time To Live) avec une règle métier empêchant qu’un
|
||||
item soit à la fois épinglé et avec un TTL.</p>
|
||||
<h2 id="modifications-apportées">Modifications apportées</h2>
|
||||
<h3 id="structure-de-la-base-de-données-pmocachesrcdb.rs">1. Structure
|
||||
de la base de données (<code>pmocache/src/db.rs</code>)</h3>
|
||||
<h4 id="modification-du-schéma-de-la-table-asset">Modification du schéma
|
||||
de la table <code>asset</code></h4>
|
||||
<p>Ajout de deux nouvelles colonnes :</p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode sql"><code class="sourceCode sql"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> <span class="cf">IF</span> <span class="kw">NOT</span> <span class="kw">EXISTS</span> asset (</span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> pk TEXT <span class="kw">PRIMARY</span> <span class="kw">KEY</span>,</span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> collection TEXT,</span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">id</span> TEXT,</span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> hits <span class="dt">INTEGER</span> <span class="kw">DEFAULT</span> <span class="dv">0</span>,</span>
|
||||
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> last_used TEXT,</span>
|
||||
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> lazy_pk TEXT,</span>
|
||||
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a> pinned <span class="dt">INTEGER</span> <span class="kw">DEFAULT</span> <span class="dv">0</span> <span class="kw">CHECK</span> (pinned <span class="kw">IN</span> (<span class="dv">0</span>, <span class="dv">1</span>)),</span>
|
||||
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> ttl_expires_at TEXT</span>
|
||||
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>)</span></pre></div>
|
||||
<ul>
|
||||
<li><strong><code>pinned</code></strong> : Booléen (0 ou 1) indiquant si
|
||||
l’item est épinglé</li>
|
||||
<li><strong><code>ttl_expires_at</code></strong> : Date/heure
|
||||
d’expiration au format RFC3339 (optionnel)</li>
|
||||
</ul>
|
||||
<h4 id="mise-à-jour-de-la-structure-cacheentry">Mise à jour de la
|
||||
structure <code>CacheEntry</code></h4>
|
||||
<p>Ajout des champs correspondants :</p>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> CacheEntry <span class="op">{</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a> <span class="co">// ... champs existants ...</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> pinned<span class="op">:</span> <span class="dt">bool</span><span class="op">,</span></span>
|
||||
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> ttl_expires_at<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// ...</span></span>
|
||||
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="nouvelles-méthodes-dans-db">Nouvelles méthodes dans
|
||||
<code>DB</code></h4>
|
||||
<h5 id="gestion-du-comptage">Gestion du comptage</h5>
|
||||
<ul>
|
||||
<li><strong><code>count_unpinned()</code></strong> : Compte uniquement
|
||||
les items non épinglés
|
||||
<ul>
|
||||
<li>Les items épinglés ne comptent pas dans la limite du cache</li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
<h5 id="gestion-du-pinning">Gestion du pinning</h5>
|
||||
<ul>
|
||||
<li><p><strong><code>pin(pk: &str)</code></strong> : Épingle un
|
||||
item</p>
|
||||
<ul>
|
||||
<li>Vérifie que l’item n’a pas de TTL défini (règle métier)</li>
|
||||
<li>Retourne une erreur si le TTL est déjà défini</li>
|
||||
</ul></li>
|
||||
<li><p><strong><code>unpin(pk: &str)</code></strong> : Désépingle un
|
||||
item</p></li>
|
||||
<li><p><strong><code>is_pinned(pk: &str)</code></strong> : Vérifie
|
||||
si un item est épinglé</p></li>
|
||||
</ul>
|
||||
<h5 id="gestion-du-ttl">Gestion du TTL</h5>
|
||||
<ul>
|
||||
<li><p><strong><code>set_ttl(pk: &str, expires_at: &str)</code></strong>
|
||||
: Définit le TTL d’un item</p>
|
||||
<ul>
|
||||
<li>Vérifie que l’item n’est pas épinglé (règle métier)</li>
|
||||
<li>Retourne une erreur si l’item est épinglé</li>
|
||||
</ul></li>
|
||||
<li><p><strong><code>clear_ttl(pk: &str)</code></strong> : Supprime
|
||||
le TTL d’un item</p></li>
|
||||
<li><p><strong><code>get_expired()</code></strong> : Récupère tous les
|
||||
items dont le TTL est dépassé</p></li>
|
||||
</ul>
|
||||
<h5 id="modification-de-get_oldest">Modification de
|
||||
<code>get_oldest()</code></h5>
|
||||
<p>La requête SQL exclut maintenant les items épinglés :</p>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode sql"><code class="sourceCode sql"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> <span class="op">..</span>. <span class="kw">FROM</span> asset</span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> pinned <span class="op">=</span> <span class="dv">0</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> last_used <span class="kw">ASC</span>, hits <span class="kw">ASC</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="kw">LIMIT</span> ?<span class="dv">1</span></span></pre></div>
|
||||
<h3 id="logique-du-cache-pmocachesrccache.rs">2. Logique du cache
|
||||
(<code>pmocache/src/cache.rs</code>)</h3>
|
||||
<h4 id="méthodes-publiques-ajoutées">Méthodes publiques ajoutées</h4>
|
||||
<div class="sourceCode" id="cb4"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> pin(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span></span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> unpin(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> is_pinned(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">bool</span><span class="op">></span></span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> set_ttl(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span><span class="op">,</span> expires_at<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">async</span> <span class="kw">fn</span> clear_ttl(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> pk<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">></span></span></pre></div>
|
||||
<h4 id="modification-de-enforce_limit">Modification de
|
||||
<code>enforce_limit()</code></h4>
|
||||
<p>La politique d’éviction a été améliorée :</p>
|
||||
<ol type="1">
|
||||
<li><strong>Suppression prioritaire des items expirés</strong> : Les
|
||||
items dont le TTL est dépassé sont supprimés en premier</li>
|
||||
<li><strong>Comptage des items non épinglés</strong> : Utilise
|
||||
<code>count_unpinned()</code> au lieu de <code>count()</code></li>
|
||||
<li><strong>Protection des items épinglés</strong> : Ils ne peuvent pas
|
||||
être évincés par LRU</li>
|
||||
<li><strong>Logging amélioré</strong> : Messages distincts pour les
|
||||
items expirés et l’éviction LRU</li>
|
||||
</ol>
|
||||
<h3 id="tests-pmocacheteststest_pinnable.rs">3. Tests
|
||||
(<code>pmocache/tests/test_pinnable.rs</code>)</h3>
|
||||
<p>Création d’une suite complète de tests (9 tests, tous passants) :</p>
|
||||
<ol type="1">
|
||||
<li><strong><code>test_pin_unpin</code></strong> : Vérifie l’épinglage
|
||||
et le désépinglage basiques</li>
|
||||
<li><strong><code>test_pinned_excluded_from_lru</code></strong> :
|
||||
Vérifie que les items épinglés ne sont pas évincés</li>
|
||||
<li><strong><code>test_pinned_count_separately</code></strong> : Vérifie
|
||||
le comptage séparé des items épinglés</li>
|
||||
<li><strong><code>test_cannot_pin_with_ttl</code></strong> : Vérifie la
|
||||
règle métier TTL → pas de pinning</li>
|
||||
<li><strong><code>test_cannot_set_ttl_when_pinned</code></strong> :
|
||||
Vérifie la règle métier pinned → pas de TTL</li>
|
||||
<li><strong><code>test_ttl_expiration</code></strong> : Vérifie la
|
||||
suppression automatique des items expirés</li>
|
||||
<li><strong><code>test_clear_ttl</code></strong> : Vérifie la
|
||||
suppression du TTL</li>
|
||||
<li><strong><code>test_get_expired</code></strong> : Vérifie la
|
||||
récupération des items expirés</li>
|
||||
<li><strong><code>test_cache_entry_fields</code></strong> : Vérifie les
|
||||
valeurs des champs dans <code>CacheEntry</code></li>
|
||||
</ol>
|
||||
<h2 id="règles-métier-implémentées">Règles métier implémentées</h2>
|
||||
<h3 id="incompatibilité-ttl-pinned">Incompatibilité TTL ↔︎ Pinned</h3>
|
||||
<p>Un item ne peut pas être à la fois épinglé ET avoir un TTL :</p>
|
||||
<ul>
|
||||
<li><strong>Si TTL défini</strong> : <code>pin()</code> retourne une
|
||||
erreur</li>
|
||||
<li><strong>Si épinglé</strong> : <code>set_ttl()</code> retourne une
|
||||
erreur</li>
|
||||
</ul>
|
||||
<p>Cette règle garantit une sémantique claire : -
|
||||
<strong>Épinglé</strong> = permanent, protégé de l’éviction -
|
||||
<strong>TTL</strong> = temporaire, sera supprimé à expiration</p>
|
||||
<h3 id="comptage-des-items">Comptage des items</h3>
|
||||
<p>Les items épinglés sont <strong>exclus</strong> du comptage de la
|
||||
limite du cache :</p>
|
||||
<ul>
|
||||
<li>Un cache de limite 100 peut contenir 100 items non épinglés + N
|
||||
items épinglés</li>
|
||||
<li>Seuls les items non épinglés sont pris en compte pour l’éviction
|
||||
LRU</li>
|
||||
</ul>
|
||||
<h3 id="ordre-de-suppression-lors-de-enforce_limit">Ordre de suppression
|
||||
lors de <code>enforce_limit()</code></h3>
|
||||
<ol type="1">
|
||||
<li><strong>Items expirés (TTL dépassé)</strong> : supprimés en
|
||||
priorité</li>
|
||||
<li><strong>Items LRU</strong> : si la limite est toujours dépassée,
|
||||
suppression des plus vieux items <strong>non épinglés</strong></li>
|
||||
</ol>
|
||||
<h2 id="compatibilité">Compatibilité</h2>
|
||||
<h3 id="migration-de-base-de-données">Migration de base de données</h3>
|
||||
<p><strong>Aucune migration nécessaire</strong> : Les colonnes
|
||||
<code>pinned</code> et <code>ttl_expires_at</code> ont des valeurs par
|
||||
défaut : - <code>pinned = 0</code> (non épinglé) -
|
||||
<code>ttl_expires_at = NULL</code> (pas de TTL)</p>
|
||||
<p>Les bases existantes seront automatiquement mises à jour au prochain
|
||||
démarrage via le <code>CREATE TABLE IF NOT EXISTS</code> avec les
|
||||
nouvelles colonnes.</p>
|
||||
<h3 id="rétrocompatibilité-du-code">Rétrocompatibilité du code</h3>
|
||||
<p>Toutes les méthodes existantes continuent de fonctionner sans
|
||||
modification : - Les items existants ne sont pas épinglés par défaut -
|
||||
Le comportement LRU standard reste identique pour les items non
|
||||
épinglés</p>
|
||||
<h2 id="exemples-dutilisation">Exemples d’utilisation</h2>
|
||||
<h3 id="utilisation-programmatique-rust">Utilisation programmatique
|
||||
(Rust)</h3>
|
||||
<div class="sourceCode" id="cb5"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmocache::</span><span class="op">{</span>Cache<span class="op">,</span> CacheConfig<span class="op">};</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">chrono::</span><span class="op">{</span>Duration<span class="op">,</span> Utc<span class="op">};</span></span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Créer un cache</span></span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> cache <span class="op">=</span> <span class="pp">Cache::</span><span class="op"><</span>MyConfig<span class="op">></span><span class="pp">::</span>new(<span class="st">"./cache"</span><span class="op">,</span> <span class="dv">100</span>)<span class="op">.</span>unwrap()<span class="op">;</span></span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="co">// Ajouter un fichier</span></span>
|
||||
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> pk <span class="op">=</span> cache<span class="op">.</span>add_from_url(<span class="st">"https://example.com/file.dat"</span><span class="op">,</span> <span class="cn">None</span>)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="co">// Épingler pour protéger de l'éviction</span></span>
|
||||
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>cache<span class="op">.</span>pin(<span class="op">&</span>pk)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="co">// Ou définir un TTL de 24 heures</span></span>
|
||||
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> expires_at <span class="op">=</span> (<span class="pp">Utc::</span>now() <span class="op">+</span> <span class="pp">Duration::</span>hours(<span class="dv">24</span>))<span class="op">.</span>to_rfc3339()<span class="op">;</span></span>
|
||||
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>cache<span class="op">.</span>set_ttl(<span class="op">&</span>pk2<span class="op">,</span> <span class="op">&</span>expires_at)<span class="op">.</span><span class="kw">await</span><span class="op">?;</span></span>
|
||||
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="co">// Vérifier le statut</span></span>
|
||||
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> cache<span class="op">.</span>is_pinned(<span class="op">&</span>pk)<span class="op">.</span><span class="kw">await</span><span class="op">?</span> <span class="op">{</span></span>
|
||||
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> <span class="pp">println!</span>(<span class="st">"Fichier protégé"</span>)<span class="op">;</span></span>
|
||||
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h3 id="utilisation-via-lapi-rest">Utilisation via l’API REST</h3>
|
||||
<h4 id="récupérer-le-statut-de-pinning">Récupérer le statut de
|
||||
pinning</h4>
|
||||
<div class="sourceCode" id="cb6"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">GET</span> /api/cache/{pk}/pin</span>
|
||||
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 200 OK:</span>
|
||||
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"pk"</span><span class="ex">:</span> <span class="st">"1a2b3c4d5e6f7a8b"</span>,</span>
|
||||
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"pinned"</span><span class="ex">:</span> false,</span>
|
||||
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a> <span class="st">"ttl_expires_at"</span><span class="ex">:</span> null</span>
|
||||
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></pre></div>
|
||||
<h4 id="épingler-un-item">Épingler un item</h4>
|
||||
<div class="sourceCode" id="cb7"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">POST</span> /api/cache/{pk}/pin</span>
|
||||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 200 OK:</span>
|
||||
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"pk"</span><span class="ex">:</span> <span class="st">"1a2b3c4d5e6f7a8b"</span>,</span>
|
||||
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"Item '1a2b3c4d5e6f7a8b' pinned successfully"</span></span>
|
||||
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span>
|
||||
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 409 CONFLICT <span class="er">(</span><span class="ex">si</span> TTL défini<span class="kw">)</span><span class="bu">:</span></span>
|
||||
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"error"</span><span class="ex">:</span> <span class="st">"CONFLICT"</span>,</span>
|
||||
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"Cannot pin an item with TTL set. Clear TTL first."</span></span>
|
||||
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></pre></div>
|
||||
<h4 id="désépingler-un-item">Désépingler un item</h4>
|
||||
<div class="sourceCode" id="cb8"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">DELETE</span> /api/cache/{pk}/pin</span>
|
||||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 200 OK:</span>
|
||||
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"pk"</span><span class="ex">:</span> <span class="st">"1a2b3c4d5e6f7a8b"</span>,</span>
|
||||
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"Item '1a2b3c4d5e6f7a8b' unpinned successfully"</span></span>
|
||||
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></pre></div>
|
||||
<h4 id="définir-un-ttl">Définir un TTL</h4>
|
||||
<div class="sourceCode" id="cb9"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">POST</span> /api/cache/{pk}/ttl</span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Content-Type:</span> application/json</span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"expires_at"</span><span class="ex">:</span> <span class="st">"2025-01-20T10:30:00Z"</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 200 OK:</span>
|
||||
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a> <span class="st">"pk"</span><span class="ex">:</span> <span class="st">"1a2b3c4d5e6f7a8b"</span>,</span>
|
||||
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"TTL set successfully for item '1a2b3c4d5e6f7a8b'"</span></span>
|
||||
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span>
|
||||
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 409 CONFLICT <span class="er">(</span><span class="ex">si</span> épinglé<span class="kw">)</span><span class="bu">:</span></span>
|
||||
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a> <span class="st">"error"</span><span class="ex">:</span> <span class="st">"CONFLICT"</span>,</span>
|
||||
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"Cannot set TTL on a pinned item. Unpin first."</span></span>
|
||||
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span>
|
||||
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 400 BAD REQUEST <span class="er">(</span><span class="ex">format</span> invalide<span class="kw">)</span><span class="bu">:</span></span>
|
||||
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a> <span class="st">"error"</span><span class="ex">:</span> <span class="st">"INVALID_DATE"</span>,</span>
|
||||
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"Invalid RFC3339 date format"</span></span>
|
||||
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></pre></div>
|
||||
<h4 id="supprimer-un-ttl">Supprimer un TTL</h4>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">DELETE</span> /api/cache/{pk}/ttl</span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Response</span> 200 OK:</span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="kw">{</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"pk"</span><span class="ex">:</span> <span class="st">"1a2b3c4d5e6f7a8b"</span>,</span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"message"</span><span class="ex">:</span> <span class="st">"TTL cleared successfully for item '1a2b3c4d5e6f7a8b'"</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></pre></div>
|
||||
<h2 id="fichiers-modifiés">Fichiers modifiés</h2>
|
||||
<h3 id="phase-1-implémentation-de-base">Phase 1 : Implémentation de
|
||||
base</h3>
|
||||
<ol type="1">
|
||||
<li><strong><code>pmocache/src/db.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Modification du schéma SQL</li>
|
||||
<li>Ajout de champs dans <code>CacheEntry</code></li>
|
||||
<li>Ajout de 8 nouvelles méthodes</li>
|
||||
<li>Modification de <code>get_oldest()</code>, <code>get()</code>,
|
||||
<code>get_from_id()</code>, <code>get_all()</code>,
|
||||
<code>get_by_collection()</code></li>
|
||||
</ul></li>
|
||||
<li><strong><code>pmocache/src/cache.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Ajout de 5 méthodes publiques</li>
|
||||
<li>Modification de <code>enforce_limit()</code></li>
|
||||
</ul></li>
|
||||
<li><strong><code>pmocache/tests/test_pinnable.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Nouveau fichier de tests (9 tests)</li>
|
||||
</ul></li>
|
||||
</ol>
|
||||
<h3 id="phase-2-enrichissement-de-lapi-rest">Phase 2 : Enrichissement de
|
||||
l’API REST</h3>
|
||||
<ol start="4" type="1">
|
||||
<li><strong><code>pmocache/src/api.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Ajout de 3 nouvelles structures de données :
|
||||
<code>SetTtlRequest</code>, <code>PinResponse</code>,
|
||||
<code>PinStatus</code></li>
|
||||
<li>Ajout de 5 nouveaux handlers d’API :
|
||||
<ul>
|
||||
<li><code>get_pin_status()</code> : Récupération du statut de
|
||||
pinning</li>
|
||||
<li><code>pin_item()</code> : Épinglage d’un item</li>
|
||||
<li><code>unpin_item()</code> : Désépinglage d’un item</li>
|
||||
<li><code>set_item_ttl()</code> : Définition du TTL</li>
|
||||
<li><code>clear_item_ttl()</code> : Suppression du TTL</li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
<li><strong><code>pmocache/src/pmoserver_ext.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Ajout de 4 nouvelles routes dans <code>create_api_router()</code> :
|
||||
<ul>
|
||||
<li><code>GET /{pk}/pin</code> : Statut de pinning</li>
|
||||
<li><code>POST /{pk}/pin</code> : Épingler</li>
|
||||
<li><code>DELETE /{pk}/pin</code> : Désépingler</li>
|
||||
<li><code>POST /{pk}/ttl</code> : Définir TTL</li>
|
||||
<li><code>DELETE /{pk}/ttl</code> : Supprimer TTL</li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
<li><strong><code>pmocache/src/openapi.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Mise à jour de la macro <code>create_cache_openapi!</code> pour
|
||||
inclure :
|
||||
<ul>
|
||||
<li>Les 5 nouveaux endpoints dans la documentation</li>
|
||||
<li>Les 3 nouvelles structures dans les schémas OpenAPI</li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
<li><strong><code>pmocache/src/lib.rs</code></strong> :
|
||||
<ul>
|
||||
<li>Export des nouvelles structures publiques pour l’API</li>
|
||||
</ul></li>
|
||||
</ol>
|
||||
<h2 id="api-rest-et-documentation-openapi">API REST et Documentation
|
||||
OpenAPI</h2>
|
||||
<h3 id="routes-disponibles">Routes disponibles</h3>
|
||||
<p>Toutes les routes sont préfixées par <code>/api/{cache_name}/</code>
|
||||
(ex: <code>/api/covers/</code>, <code>/api/audio/</code>).</p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 31%" />
|
||||
<col style="width: 24%" />
|
||||
<col style="width: 44%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Méthode</th>
|
||||
<th>Route</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/{pk}/pin</code></td>
|
||||
<td>Récupère le statut de pinning d’un item</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/{pk}/pin</code></td>
|
||||
<td>Épingle un item (le protège de l’éviction LRU)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DELETE</code></td>
|
||||
<td><code>/{pk}/pin</code></td>
|
||||
<td>Désépingle un item</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/{pk}/ttl</code></td>
|
||||
<td>Définit le TTL d’un item (expiration automatique)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DELETE</code></td>
|
||||
<td><code>/{pk}/ttl</code></td>
|
||||
<td>Supprime le TTL d’un item</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="codes-de-statut-http">Codes de statut HTTP</h3>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 18%" />
|
||||
<col style="width: 42%" />
|
||||
<col style="width: 39%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Signification</th>
|
||||
<th>Cas d’usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>200 OK</code></td>
|
||||
<td>Opération réussie</td>
|
||||
<td>Tous les cas de succès</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400 BAD REQUEST</code></td>
|
||||
<td>Requête invalide</td>
|
||||
<td>Format de date TTL invalide</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404 NOT FOUND</code></td>
|
||||
<td>Item non trouvé</td>
|
||||
<td>PK inexistant dans le cache</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>409 CONFLICT</code></td>
|
||||
<td>Conflit de règle métier</td>
|
||||
<td>Tentative de pin avec TTL ou vice-versa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500 INTERNAL SERVER ERROR</code></td>
|
||||
<td>Erreur serveur</td>
|
||||
<td>Erreur de base de données</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="documentation-openapiswagger">Documentation OpenAPI/Swagger</h3>
|
||||
<p>La documentation OpenAPI est automatiquement générée et inclut :</p>
|
||||
<ul>
|
||||
<li><strong>Schémas de données</strong> :
|
||||
<ul>
|
||||
<li><code>PinStatus</code> : Statut de pinning (pinned,
|
||||
ttl_expires_at)</li>
|
||||
<li><code>PinResponse</code> : Réponse d’opération de pinning</li>
|
||||
<li><code>SetTtlRequest</code> : Requête de définition de TTL</li>
|
||||
<li><code>CacheEntry</code> : Mis à jour avec les champs
|
||||
<code>pinned</code> et <code>ttl_expires_at</code></li>
|
||||
</ul></li>
|
||||
<li><strong>Endpoints documentés</strong> :
|
||||
<ul>
|
||||
<li>Description détaillée de chaque route</li>
|
||||
<li>Exemples de requêtes et réponses</li>
|
||||
<li>Codes d’erreur possibles</li>
|
||||
</ul></li>
|
||||
<li><strong>Interface Swagger UI</strong> :
|
||||
<ul>
|
||||
<li>Accessible à <code>/swagger-ui/{cache_name}</code></li>
|
||||
<li>Permet de tester l’API directement depuis le navigateur</li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
<h3 id="gestion-des-erreurs">Gestion des erreurs</h3>
|
||||
<p>L’API suit une structure d’erreur cohérente :</p>
|
||||
<div class="sourceCode" id="cb11"><pre
|
||||
class="sourceCode json"><code class="sourceCode json"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">"error"</span><span class="fu">:</span> <span class="st">"CODE_ERREUR"</span><span class="fu">,</span></span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">"message"</span><span class="fu">:</span> <span class="st">"Description lisible de l'erreur"</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></pre></div>
|
||||
<p>Les règles métier sont appliquées strictement : - <strong>409
|
||||
CONFLICT</strong> si tentative de pin avec TTL défini - <strong>409
|
||||
CONFLICT</strong> si tentative de set TTL sur item épinglé - Messages
|
||||
d’erreur explicites guidant l’utilisateur</p>
|
||||
<h2 id="tests">Tests</h2>
|
||||
<ul>
|
||||
<li><strong>Suite de tests dédiée</strong> : 9 tests, tous passants</li>
|
||||
<li><strong>Tests existants</strong> : Tous les tests de
|
||||
<code>test_cache.rs</code> passent toujours</li>
|
||||
<li><strong>Couverture</strong> : Toutes les nouvelles fonctionnalités
|
||||
sont testées</li>
|
||||
<li><strong>Compilation</strong> : Aucune erreur, tous les modules
|
||||
compilent correctement</li>
|
||||
</ul>
|
||||
<h2 id="résultat">Résultat</h2>
|
||||
<p>✅ <strong>Implémentation complète et fonctionnelle</strong> des
|
||||
items épinglables avec TTL<br />
|
||||
✅ <strong>Règle métier</strong> TTL ↔︎ Pinned correctement
|
||||
implémentée<br />
|
||||
✅ <strong>Tests exhaustifs</strong> validant tous les cas d’usage<br />
|
||||
✅ <strong>Compatibilité</strong> avec les bases de données
|
||||
existantes<br />
|
||||
✅ <strong>Pas de régression</strong> sur les tests existants<br />
|
||||
✅ <strong>API REST complète</strong> avec 5 nouveaux endpoints<br />
|
||||
✅ <strong>Documentation OpenAPI</strong> automatiquement générée<br />
|
||||
✅ <strong>Gestion d’erreurs cohérente</strong> avec codes HTTP
|
||||
appropriés</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
209
Blackboard_HTML/Report_WeabApp_debouncingSSE.html
Normal file
209
Blackboard_HTML/Report_WeabApp_debouncingSSE.html
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WeabApp_debouncingSSE</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="rapport-suppression-de-la-logique-de-débouncing-sse">Rapport :
|
||||
Suppression de la logique de débouncing SSE</h1>
|
||||
<p><strong>Date</strong>: 2026-01-12 <strong>Tâche</strong>:
|
||||
WeabApp_debouncingSSE.md</p>
|
||||
<h2 id="objectif">Objectif</h2>
|
||||
<p>Supprimer la logique de débouncing inutile sur le canal SSE de
|
||||
l’application web PMOControl, puisque le serveur contrôle déjà le flux
|
||||
des événements.</p>
|
||||
<h2 id="analyse-préalable">Analyse préalable</h2>
|
||||
<p>J’ai identifié trois endroits avec des mécanismes de temporisation
|
||||
dans l’application web :</p>
|
||||
<h3 id="mediabrowser.vue---débouncing-sse-à-supprimer">1.
|
||||
MediaBrowser.vue - Débouncing SSE (À SUPPRIMER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Débouncing</strong>: 200ms après invalidation du cache</li>
|
||||
<li><strong>Cooldown</strong>: 2 secondes entre les rechargements</li>
|
||||
<li><strong>Justification originale</strong>: “dédupliquer les
|
||||
événements SSE dans le même batch (polling 500ms)”</li>
|
||||
<li><strong>Problème</strong>: Cette logique est redondante puisque le
|
||||
serveur contrôle déjà le flux SSE</li>
|
||||
</ul>
|
||||
<h3 id="userenderers.ts---smart-fetching-à-conserver">2. useRenderers.ts
|
||||
- Smart fetching (À CONSERVER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Mécanisme</strong>: Comparaison des timestamps
|
||||
<code>lastEventAt</code> vs <code>lastSnapshotAt</code></li>
|
||||
<li><strong>But</strong>: Éviter de refetch un snapshot déjà à jour</li>
|
||||
<li><strong>Justification</strong>: Ce n’est PAS du débouncing, c’est
|
||||
une optimisation intelligente qui évite des appels API inutiles</li>
|
||||
</ul>
|
||||
<h3 id="volumecontrol.vue---ui-debouncing-à-conserver">3.
|
||||
VolumeControl.vue - UI debouncing (À CONSERVER ✓)</h3>
|
||||
<ul>
|
||||
<li><strong>Débouncing</strong>: 300ms sur les changements de
|
||||
volume</li>
|
||||
<li><strong>But</strong>: Réduire les appels API pendant que
|
||||
l’utilisateur fait glisser le curseur</li>
|
||||
<li><strong>Justification</strong>: Débouncing légitime pour l’interface
|
||||
utilisateur</li>
|
||||
</ul>
|
||||
<h2 id="modifications-effectuées">Modifications effectuées</h2>
|
||||
<h3
|
||||
id="fichier-modifié-pmoappwebappsrccomponentspmocontrolmediabrowser.vue">Fichier
|
||||
modifié:
|
||||
<code>pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue</code></h3>
|
||||
<h4 id="suppression-des-variables-de-débouncing-ligne-27">1. Suppression
|
||||
des variables de débouncing (ligne ~27)</h4>
|
||||
<p><strong>Avant</strong>:</p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Flags pour gérer le rechargement automatique avec debounce et cooldown</span></span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> isRefreshing <span class="op">=</span> <span class="fu">ref</span>(<span class="kw">false</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> refreshTimeoutId <span class="op">=</span> <span class="fu">ref</span><span class="op"><</span><span class="dt">number</span> <span class="op">|</span> <span class="dt">null</span><span class="op">></span>(<span class="kw">null</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> lastRefreshTime <span class="op">=</span> <span class="fu">ref</span><span class="op"><</span><span class="dt">number</span><span class="op">></span>(<span class="dv">0</span>)<span class="op">;</span></span>
|
||||
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> REFRESH_COOLDOWN_MS <span class="op">=</span> <span class="dv">2000</span><span class="op">;</span> <span class="co">// Ne pas recharger plus d'une fois toutes les 2 secondes</span></span></pre></div>
|
||||
<p><strong>Après</strong>:</p>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Flag pour gérer le rechargement automatique</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> isRefreshing <span class="op">=</span> <span class="fu">ref</span>(<span class="kw">false</span>)<span class="op">;</span></span></pre></div>
|
||||
<h4 id="simplification-du-watcher-de-cache-ligne-53">2. Simplification
|
||||
du watcher de cache (ligne ~53)</h4>
|
||||
<p><strong>Avant</strong>:</p>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="co">// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co">// ce qui déclenche un événement ContainersUpdated qui invalide le cache</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Utilise un debounce de 3 secondes pour regrouper les multiples invalidations</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">// et un cooldown de 5 secondes pour éviter les rechargements successifs</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="fu">watch</span>(</span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> () <span class="kw">=></span> browseData<span class="op">.</span><span class="at">value</span><span class="op">,</span></span>
|
||||
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> (data) <span class="kw">=></span> {</span>
|
||||
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>data <span class="op">&&</span> props<span class="op">.</span><span class="at">containerId</span> <span class="op">&&</span> <span class="op">!</span>loading<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="co">// Vérifier le cooldown: ignorer si on a rechargé il y a moins de 5 secondes</span></span>
|
||||
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> timeSinceLastRefresh <span class="op">=</span> <span class="bu">Date</span><span class="op">.</span><span class="fu">now</span>() <span class="op">-</span> lastRefreshTime<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
|
||||
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (timeSinceLastRefresh <span class="op"><</span> REFRESH_COOLDOWN_MS) {</span>
|
||||
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé mais cooldown actif (</span><span class="sc">${</span><span class="bu">Math</span><span class="op">.</span><span class="fu">round</span>((REFRESH_COOLDOWN_MS <span class="op">-</span> timeSinceLastRefresh) <span class="op">/</span> <span class="dv">1000</span>)<span class="sc">}</span><span class="vs">s restantes), rechargement ignoré`</span><span class="op">,</span></span>
|
||||
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span><span class="op">;</span></span>
|
||||
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="co">// Annuler tout timeout en cours</span></span>
|
||||
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">!==</span> <span class="kw">null</span>) {</span>
|
||||
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="pp">clearTimeout</span>(refreshTimeoutId<span class="op">.</span><span class="at">value</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="co">// Planifier le rechargement après 200ms</span></span>
|
||||
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="bu">window</span><span class="op">.</span><span class="fu">setTimeout</span>(<span class="kw">async</span> () <span class="kw">=></span> {</span>
|
||||
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>isRefreshing<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé pour </span><span class="sc">${</span>props<span class="op">.</span><span class="at">serverId</span><span class="sc">}</span><span class="vs">/</span><span class="sc">${</span>props<span class="op">.</span><span class="at">containerId</span><span class="sc">}</span><span class="vs">, rechargement après debounce...`</span><span class="op">,</span></span>
|
||||
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">true</span><span class="op">;</span></span>
|
||||
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">await</span> <span class="fu">browseContainer</span>(</span>
|
||||
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a> props<span class="op">.</span><span class="at">serverId</span><span class="op">,</span></span>
|
||||
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> props<span class="op">.</span><span class="at">containerId</span><span class="op">,</span></span>
|
||||
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="kw">false</span><span class="op">,</span></span>
|
||||
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> lastRefreshTime<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="bu">Date</span><span class="op">.</span><span class="fu">now</span>()<span class="op">;</span></span>
|
||||
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">false</span><span class="op">;</span></span>
|
||||
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> refreshTimeoutId<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">null</span><span class="op">;</span></span>
|
||||
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span> <span class="dv">200</span>)<span class="op">;</span></span>
|
||||
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
|
||||
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></pre></div>
|
||||
<p><strong>Après</strong>:</p>
|
||||
<div class="sourceCode" id="cb4"><pre
|
||||
class="sourceCode typescript"><code class="sourceCode typescript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">// Recharger automatiquement si le cache est invalidé (ex: après un ContainersUpdated SSE)</span></span>
|
||||
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="co">// Cela se produit notamment quand on clique sur "Lire maintenant" sur une playlist,</span></span>
|
||||
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co">// ce qui déclenche un événement ContainersUpdated qui invalide le cache</span></span>
|
||||
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="co">// Le serveur contrôle déjà le flux SSE, pas besoin de debouncing côté client</span></span>
|
||||
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="fu">watch</span>(</span>
|
||||
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> () <span class="kw">=></span> browseData<span class="op">.</span><span class="at">value</span><span class="op">,</span></span>
|
||||
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">async</span> (data) <span class="kw">=></span> {</span>
|
||||
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a> <span class="co">// Si browseData devient undefined alors que containerId est présent,</span></span>
|
||||
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> <span class="co">// et qu'on n'est pas déjà en train de charger, recharger immédiatement</span></span>
|
||||
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>data <span class="op">&&</span> props<span class="op">.</span><span class="at">containerId</span> <span class="op">&&</span> <span class="op">!</span>loading<span class="op">.</span><span class="at">value</span> <span class="op">&&</span> <span class="op">!</span>isRefreshing<span class="op">.</span><span class="at">value</span>) {</span>
|
||||
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(</span>
|
||||
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="vs">`[MediaBrowser] Cache invalidé pour </span><span class="sc">${</span>props<span class="op">.</span><span class="at">serverId</span><span class="sc">}</span><span class="vs">/</span><span class="sc">${</span>props<span class="op">.</span><span class="at">containerId</span><span class="sc">}</span><span class="vs">, rechargement...`</span><span class="op">,</span></span>
|
||||
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> )<span class="op">;</span></span>
|
||||
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">true</span><span class="op">;</span></span>
|
||||
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">await</span> <span class="fu">browseContainer</span>(props<span class="op">.</span><span class="at">serverId</span><span class="op">,</span> props<span class="op">.</span><span class="at">containerId</span><span class="op">,</span> <span class="kw">false</span>)<span class="op">;</span></span>
|
||||
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a> isRefreshing<span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="kw">false</span><span class="op">;</span></span>
|
||||
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a> }</span>
|
||||
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a> }<span class="op">,</span></span>
|
||||
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>)<span class="op">;</span></span></pre></div>
|
||||
<h2 id="résultats">Résultats</h2>
|
||||
<h3 id="changements-de-comportement">Changements de comportement</h3>
|
||||
<ul>
|
||||
<li><strong>Avant</strong>: Délai de 200ms + cooldown de 2s entre les
|
||||
rechargements de cache</li>
|
||||
<li><strong>Après</strong>: Rechargement immédiat dès l’invalidation du
|
||||
cache</li>
|
||||
<li><strong>Impact</strong>: Réactivité améliorée de l’interface, les
|
||||
mises à jour apparaissent immédiatement</li>
|
||||
</ul>
|
||||
<h3 id="réduction-de-complexité">Réduction de complexité</h3>
|
||||
<ul>
|
||||
<li><strong>3 variables supprimées</strong>:
|
||||
<code>refreshTimeoutId</code>, <code>lastRefreshTime</code>,
|
||||
<code>REFRESH_COOLDOWN_MS</code></li>
|
||||
<li><strong>Logique simplifiée</strong>: De ~40 lignes à ~10 lignes dans
|
||||
le watcher</li>
|
||||
<li><strong>Code plus lisible</strong>: Intention claire sans mécanismes
|
||||
de temporisation complexes</li>
|
||||
</ul>
|
||||
<h3 id="tests">Tests</h3>
|
||||
<ul>
|
||||
<li>✓ Le projet compile sans erreurs TypeScript</li>
|
||||
<li>✓ Le flag <code>isRefreshing</code> empêche toujours les
|
||||
rechargements concurrents</li>
|
||||
<li>✓ Les autres composants (useRenderers.ts, VolumeControl.vue)
|
||||
conservent leurs optimisations légitimes</li>
|
||||
</ul>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>La suppression du débouncing et du cooldown dans MediaBrowser.vue
|
||||
simplifie le code tout en améliorant la réactivité de l’interface.
|
||||
Puisque le serveur contrôle déjà le flux SSE, ces mécanismes côté client
|
||||
étaient redondants et ajoutaient une latence artificielle.</p>
|
||||
<p>Le code est maintenant plus simple, plus réactif, et fait confiance
|
||||
au serveur pour contrôler la fréquence des événements SSE.</p>
|
||||
<h2 id="fichiers-modifiés">Fichiers modifiés</h2>
|
||||
<ul>
|
||||
<li><code>pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue</code></li>
|
||||
</ul>
|
||||
<h2 id="lignes-de-code">Lignes de code</h2>
|
||||
<ul>
|
||||
<li><strong>Supprimées</strong>: ~35 lignes (logique de
|
||||
débouncing/cooldown)</li>
|
||||
<li><strong>Ajoutées</strong>: ~5 lignes (logique simplifiée)</li>
|
||||
<li><strong>Net</strong>: -30 lignes</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
152
Blackboard_HTML/Report_config_ext.html
Normal file
152
Blackboard_HTML/Report_config_ext.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>config_ext</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="rapport-documentation-du-pattern-dextension-pmoconfig">Rapport :
|
||||
Documentation du pattern d’extension pmoconfig</h1>
|
||||
<h2 id="objectif-de-la-tâche">Objectif de la tâche</h2>
|
||||
<p>Créer une fiche descriptive documentant le pattern d’implémentation
|
||||
des traits d’extension de <code>pmoconfig::Config</code> en analysant
|
||||
les implémentations existantes dans les différents crates du projet.</p>
|
||||
<h2 id="travail-réalisé">Travail réalisé</h2>
|
||||
<h3 id="analyse-des-fichiers-source">1. Analyse des fichiers source</h3>
|
||||
<p>Les fichiers suivants ont été analysés :</p>
|
||||
<ul>
|
||||
<li><code>pmocovers/src/config_ext.rs</code> - Pattern cache avec
|
||||
conversion WebP</li>
|
||||
<li><code>pmoaudiocache/src/config_ext.rs</code> - Pattern cache avec
|
||||
conversion FLAC</li>
|
||||
<li><code>pmoqobuz/src/config_ext.rs</code> - Pattern authentification
|
||||
et rate limiting</li>
|
||||
<li><code>pmocache/src/config_ext.rs</code> - Trait générique de cache
|
||||
et macro</li>
|
||||
<li><code>pmoconfig/PASSWORD_ENCRYPTION.md</code> - Documentation du
|
||||
chiffrement</li>
|
||||
<li><code>pmoupnp/src/config_ext.rs</code> - Pattern configuration
|
||||
UPnP</li>
|
||||
<li><code>pmoparadise/src/config_ext.rs</code> - Pattern configuration
|
||||
minimale</li>
|
||||
</ul>
|
||||
<h3 id="patterns-identifiés">2. Patterns identifiés</h3>
|
||||
<h4 id="pattern-de-base">Pattern de base</h4>
|
||||
<p>Tous les traits d’extension suivent la même structure : - Trait
|
||||
public avec méthodes getter/setter - Implémentation pour
|
||||
<code>pmoconfig::Config</code> - Utilisation de
|
||||
<code>get_value</code>/<code>set_value</code> génériques - Constantes
|
||||
pour valeurs par défaut</p>
|
||||
<h4 id="patterns-spécialisés">Patterns spécialisés</h4>
|
||||
<ul>
|
||||
<li><strong>Cache</strong> : Utilisation de <code>CacheConfigExt</code>
|
||||
et factory methods</li>
|
||||
<li><strong>Authentification</strong> : Getters combinés, helpers de
|
||||
validation, déchiffrement automatique</li>
|
||||
<li><strong>Rate limiting</strong> : Configuration des limites avec
|
||||
valeurs par défaut</li>
|
||||
<li><strong>Configuration minimale</strong> : Auto-persistence des
|
||||
valeurs par défaut</li>
|
||||
<li><strong>UPnP</strong> : Configuration des identifiants devices</li>
|
||||
</ul>
|
||||
<h3 id="structure-de-la-documentation">3. Structure de la
|
||||
documentation</h3>
|
||||
<p>La documentation créée couvre :</p>
|
||||
<ol type="1">
|
||||
<li><strong>Vue d’ensemble</strong> : Objectif et principe du
|
||||
pattern</li>
|
||||
<li><strong>Architecture</strong> : Structure et flux de données</li>
|
||||
<li><strong>Implémentation</strong> : Guide détaillé avec patterns de
|
||||
code</li>
|
||||
<li><strong>Patterns spécialisés</strong> : Exemples pour chaque cas
|
||||
d’usage</li>
|
||||
<li><strong>Bonnes pratiques</strong> : Nommage, erreurs,
|
||||
documentation</li>
|
||||
<li><strong>Exemples complets</strong> : 3 implémentations complètes
|
||||
commentées</li>
|
||||
<li><strong>Checklist</strong> : Liste de vérification pour nouveaux
|
||||
traits</li>
|
||||
<li><strong>Philosophie</strong> : Principes directeurs et
|
||||
avantages</li>
|
||||
</ol>
|
||||
<h3 id="contenu-clé">4. Contenu clé</h3>
|
||||
<h4 id="patterns-de-getters">Patterns de getters</h4>
|
||||
<ul>
|
||||
<li>Getter simple avec valeur par défaut</li>
|
||||
<li>Getter avec auto-persistence</li>
|
||||
<li>Getter optionnel</li>
|
||||
<li>Getter avec déchiffrement</li>
|
||||
<li>Getter avec parsing et fallback</li>
|
||||
</ul>
|
||||
<h4 id="patterns-de-setters">Patterns de setters</h4>
|
||||
<ul>
|
||||
<li>Setter simple</li>
|
||||
<li>Setter avec transformation</li>
|
||||
<li>Setter multiple (transaction)</li>
|
||||
<li>Setter de nettoyage</li>
|
||||
</ul>
|
||||
<h4 id="helpers">Helpers</h4>
|
||||
<ul>
|
||||
<li>Factory methods</li>
|
||||
<li>Getters combinés</li>
|
||||
<li>Helpers de validation</li>
|
||||
</ul>
|
||||
<h3 id="hiérarchie-de-configuration-yaml">5. Hiérarchie de configuration
|
||||
YAML</h3>
|
||||
<p>Documentation des chemins standards : - <code>host.*</code> :
|
||||
Configuration hôte/système - <code>accounts.*</code> : Comptes et
|
||||
services - <code>sources.*</code> : Sources de médias</p>
|
||||
<h2 id="résultat">Résultat</h2>
|
||||
<p>Le document <code>Blackboard/Architecture/pmoconfig_ext.md</code> a
|
||||
été créé avec : - 800+ lignes de documentation complète - 3 exemples
|
||||
d’implémentation complète - Patterns pour tous les cas d’usage
|
||||
identifiés - Bonnes pratiques et anti-patterns - Checklist
|
||||
d’implémentation</p>
|
||||
<h2 id="fichiers-créés-ou-modifiés">Fichiers créés ou modifiés</h2>
|
||||
<ul>
|
||||
<li><strong>Créé</strong> :
|
||||
<code>Blackboard/Architecture/pmoconfig_ext.md</code> - Documentation
|
||||
complète du pattern</li>
|
||||
<li><strong>Créé</strong> : <code>Blackboard/Report/config_ext.md</code>
|
||||
- Ce rapport</li>
|
||||
</ul>
|
||||
<h2 id="conformité-avec-rules.md">Conformité avec Rules.md</h2>
|
||||
<ul>
|
||||
<li>Documentation placée dans <code>Blackboard/Architecture/</code>
|
||||
comme demandé</li>
|
||||
<li>Rapport créé dans <code>Blackboard/Report/</code> avec le même nom
|
||||
de fichier</li>
|
||||
<li>Analyse focalisée sur l’objectif principal</li>
|
||||
<li>Documentation prête pour classification (Done/ToDiscuss) par
|
||||
l’humain</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
238
Blackboard_HTML/Report_music_source.html
Normal file
238
Blackboard_HTML/Report_music_source.html
Normal file
@@ -0,0 +1,238 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>music_source</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1
|
||||
id="rapport-documentation-dimplémentation-dune-nouvelle-musicsource">Rapport
|
||||
: Documentation d’implémentation d’une nouvelle MusicSource</h1>
|
||||
<h2 id="objectif">Objectif</h2>
|
||||
<p>Créer une documentation complète et pratique pour guider
|
||||
l’implémentation d’une nouvelle source musicale dans l’écosystème
|
||||
PMOMusic.</p>
|
||||
<h2 id="travail-réalisé">Travail réalisé</h2>
|
||||
<h3 id="analyse-des-sources-existantes">1. Analyse des sources
|
||||
existantes</h3>
|
||||
<p>J’ai analysé deux implémentations de référence :</p>
|
||||
<ul>
|
||||
<li><strong>pmoparadise/src/source.rs</strong> : Source dynamique avec
|
||||
FIFO (radio streaming)</li>
|
||||
<li><strong>pmoqobuz/src/source.rs</strong> : Source catalogue avec
|
||||
playlists lazy</li>
|
||||
</ul>
|
||||
<p>Ainsi que la documentation du trait :</p>
|
||||
<ul>
|
||||
<li><strong>pmosource/README.md</strong> : Vue d’ensemble du trait
|
||||
MusicSource</li>
|
||||
<li><strong>pmosource/ARCHITECTURE.md</strong> : Architecture et design
|
||||
decisions</li>
|
||||
</ul>
|
||||
<h3 id="identification-des-patterns-principaux">2. Identification des
|
||||
patterns principaux</h3>
|
||||
<p>Deux patterns majeurs ont été identifiés :</p>
|
||||
<h4 id="pattern-1-source-dynamique-fifo-radio-paradise">Pattern 1 :
|
||||
Source dynamique FIFO (Radio Paradise)</h4>
|
||||
<p><strong>Caractéristiques :</strong> - Flux continu de tracks avec
|
||||
capacité limitée - Suppression automatique des plus anciens - Callbacks
|
||||
sur playlists pour détecter les changements - Notification du
|
||||
ContentDirectory via notifier injecté - Adaptation des IDs playlist →
|
||||
schema source</p>
|
||||
<p><strong>Éléments clés :</strong></p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>update_counter<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span><span class="dt">u32</span><span class="op">>></span></span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>last_change<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>SystemTime<span class="op">>></span></span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>callback_tokens<span class="op">:</span> Arc<span class="op"><</span>Mutex<span class="op"><</span><span class="dt">Vec</span><span class="op"><</span><span class="dt">u64</span><span class="op">>>></span></span>
|
||||
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>container_notifier<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span>Arc<span class="op"><</span><span class="kw">dyn</span> <span class="bu">Fn</span>(<span class="op">&</span>[<span class="dt">String</span>]) <span class="op">+</span> <span class="bu">Send</span> <span class="op">+</span> <span class="bu">Sync</span><span class="op">>></span></span></pre></div>
|
||||
<h4 id="pattern-2-source-catalogue-lazy-qobuz">Pattern 2 : Source
|
||||
catalogue lazy (Qobuz)</h4>
|
||||
<p><strong>Caractéristiques :</strong> - Catalogue vaste avec navigation
|
||||
hiérarchique - Cache lazy pour audio, eager pour covers - Playlists
|
||||
créées à la demande avec TTL - LazyProvider pour télécharger l’audio à
|
||||
la lecture - Métadonnées riches stockées dans le cache</p>
|
||||
<p><strong>Éléments clés :</strong></p>
|
||||
<div class="sourceCode" id="cb2"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>SourceCacheManager centralisé</span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>QobuzLazyProvider implémentant LazyProvider</span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>Playlists avec rôle Album et TTL de <span class="dv">7</span> jours</span>
|
||||
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>Adaptation IDs avec metadata source_track_id</span></pre></div>
|
||||
<h3 id="structure-du-document-créé">3. Structure du document créé</h3>
|
||||
<p>Le document <code>Blackboard/Architecture/music_source.md</code>
|
||||
contient :</p>
|
||||
<h4 id="table-des-matières">Table des matières</h4>
|
||||
<ol type="1">
|
||||
<li>Vue d’ensemble</li>
|
||||
<li>Structure d’une MusicSource</li>
|
||||
<li>Implémentation du trait MusicSource</li>
|
||||
<li>Patterns d’implémentation</li>
|
||||
<li>Intégration avec l’écosystème PMOMusic</li>
|
||||
<li>Checklist de mise en œuvre</li>
|
||||
<li>Exemples de référence</li>
|
||||
</ol>
|
||||
<h4 id="sections-détaillées">Sections détaillées</h4>
|
||||
<p><strong>Section 1 : Vue d’ensemble</strong> - Définition d’une
|
||||
MusicSource - Types de sources (dynamique vs statique) - Capacités du
|
||||
trait</p>
|
||||
<p><strong>Section 2 : Structure</strong> - Organisation du code -
|
||||
Dépendances recommandées - Features Cargo</p>
|
||||
<p><strong>Section 3 : Implémentation du trait</strong> - Informations
|
||||
de base (name, id, default_image) - Navigation ContentDirectory
|
||||
(root_container, browse, resolve_uri) - Support FIFO (append_track,
|
||||
remove_oldest, update_id) - Support statique (get_items, search)</p>
|
||||
<p><strong>Section 4 : Patterns</strong> - Pattern 1 : Source dynamique
|
||||
avec FIFO (code complet) - Pattern 2 : Source catalogue avec playlists
|
||||
lazy (code complet) - Pattern 3 : Adaptation des IDs entre playlist et
|
||||
source</p>
|
||||
<p><strong>Section 5 : Intégration écosystème</strong> - pmoplaylist :
|
||||
création et gestion de playlists - pmoaudiocache/pmocovers via
|
||||
SourceCacheManager - pmodidl : conversion vers DIDL-Lite - LazyProvider
|
||||
personnalisé</p>
|
||||
<p><strong>Section 6 : Checklist</strong> - Phase 1 : Structure de base
|
||||
- Phase 2 : Navigation ContentDirectory - Phase 3 : Résolution d’URI -
|
||||
Phase 4 : Support FIFO (si dynamique) - Phase 5 : Support statique (si
|
||||
catalogue) - Phase 6 : Intégration avancée - Phase 7 : Tests et
|
||||
validation</p>
|
||||
<p><strong>Section 7 : Exemples de référence</strong> - Radio Paradise
|
||||
(source dynamique FIFO) - Qobuz (source catalogue lazy) - Schemas
|
||||
d’Object ID détaillés</p>
|
||||
<h3 id="points-techniques-importants-documentés">4. Points techniques
|
||||
importants documentés</h3>
|
||||
<h4 id="schema-dobject-id">Schema d’Object ID</h4>
|
||||
<p>Format recommandé hiérarchique :</p>
|
||||
<pre><code><source-id>
|
||||
<source-id>:albums
|
||||
<source-id>:album:<album_id>
|
||||
<source-id>:track:<track_id>
|
||||
<source-id>:playlist:<playlist_id></pre>
|
||||
<p>Exemples concrets de Radio Paradise et Qobuz fournis.</p>
|
||||
<h4 id="adaptation-des-ids">Adaptation des IDs</h4>
|
||||
<p>Code complet pour adapter les items de playlist au schema de la
|
||||
source : - Extraction du cache_pk depuis l’URL - Récupération du
|
||||
source_track_id depuis metadata - Reconstruction de l’ID correct -
|
||||
Normalisation des URLs (relatives → absolues) - Ajout de champs requis
|
||||
(genre)</p>
|
||||
<h4 id="cache-lazy-vs-eager">Cache lazy vs eager</h4>
|
||||
<p>Stratégie claire : - <strong>Covers</strong> : Cache eager (petit, UI
|
||||
en a besoin immédiatement) - <strong>Audio</strong> : Cache lazy (grand,
|
||||
téléchargé à la demande)</p>
|
||||
<h4 id="thread-safety">Thread Safety</h4>
|
||||
<p>Règles explicites : - <code>Arc<RwLock<>></code> pour
|
||||
état mutable partagé - <code>tokio::sync::RwLock</code> pour async -
|
||||
Éviter <code>Rc<></code>, <code>RefCell</code> (non thread-safe) -
|
||||
Implémenter <code>Clone</code> via <code>Arc<></code></p>
|
||||
<h4 id="compatibilité-upnp">Compatibilité UPnP</h4>
|
||||
<p>Points de vigilance : - Genre obligatoire pour certains clients
|
||||
(gupnp-av-cp) - URLs absolues uniquement - Protocol Info correct pour
|
||||
FLAC - Duration au format <code>H:MM:SS</code> - childCount optionnel
|
||||
mais recommandé</p>
|
||||
<h3 id="code-dexemple-complet">5. Code d’exemple complet</h3>
|
||||
<p>Le document contient des exemples de code complets et fonctionnels
|
||||
pour :</p>
|
||||
<ol type="1">
|
||||
<li><strong>Structure de base</strong> : définition de la struct et
|
||||
implémentation basique</li>
|
||||
<li><strong>Navigation</strong> : root_container et browse avec pattern
|
||||
matching</li>
|
||||
<li><strong>Résolution URI</strong> : avec fallback cache →
|
||||
original</li>
|
||||
<li><strong>FIFO</strong> : append_track, remove_oldest, callbacks</li>
|
||||
<li><strong>Adaptation IDs</strong> : fonction complète
|
||||
d’adaptation</li>
|
||||
<li><strong>LazyProvider</strong> : implémentation personnalisée</li>
|
||||
<li><strong>Conversion DIDL</strong> : traits ToDIDLContainer et
|
||||
ToDIDLItem</li>
|
||||
</ol>
|
||||
<h2 id="couverture-des-besoins">Couverture des besoins</h2>
|
||||
<h3 id="sources-couvertes">Sources couvertes</h3>
|
||||
<ul>
|
||||
<li>✅ Radio Paradise : source dynamique FIFO</li>
|
||||
<li>✅ Qobuz : source catalogue lazy</li>
|
||||
<li>✅ Patterns génériques applicables à d’autres sources</li>
|
||||
</ul>
|
||||
<h3 id="cas-dusage-couverts">Cas d’usage couverts</h3>
|
||||
<ul>
|
||||
<li>✅ Source radio/streaming live</li>
|
||||
<li>✅ Source catalogue de streaming (Spotify, Deezer, etc.)</li>
|
||||
<li>✅ Source bibliothèque locale</li>
|
||||
<li>✅ Source playlists fixes</li>
|
||||
<li>✅ Source avec authentification (via client)</li>
|
||||
</ul>
|
||||
<h3 id="intégrations-couvertes">Intégrations couvertes</h3>
|
||||
<ul>
|
||||
<li>✅ pmoplaylist (FIFO et persistant)</li>
|
||||
<li>✅ pmoaudiocache (cache audio)</li>
|
||||
<li>✅ pmocovers (cache covers)</li>
|
||||
<li>✅ SourceCacheManager (centralisé)</li>
|
||||
<li>✅ LazyProvider (téléchargement lazy)</li>
|
||||
<li>✅ pmodidl (DIDL-Lite)</li>
|
||||
</ul>
|
||||
<h2 id="limitations-et-améliorations-futures">Limitations et
|
||||
améliorations futures</h2>
|
||||
<h3 id="limitations-actuelles">Limitations actuelles</h3>
|
||||
<ol type="1">
|
||||
<li><strong>Search</strong> : Pas d’exemple détaillé de search
|
||||
(optionnel dans le trait)</li>
|
||||
<li><strong>Authentification</strong> : Mentionné mais pas d’exemple
|
||||
complet</li>
|
||||
<li><strong>Multi-format</strong> : Pas d’exemple de source supportant
|
||||
plusieurs formats</li>
|
||||
<li><strong>Offline</strong> : Pas de pattern pour source
|
||||
offline/synchronisation</li>
|
||||
</ol>
|
||||
<h3 id="améliorations-possibles">Améliorations possibles</h3>
|
||||
<ol type="1">
|
||||
<li>Ajouter un exemple complet de search avec filtres</li>
|
||||
<li>Documenter l’intégration avec un système d’auth OAuth</li>
|
||||
<li>Ajouter un pattern pour sources multi-formats (FLAC/MP3/AAC)</li>
|
||||
<li>Documenter la gestion offline avec synchronisation</li>
|
||||
</ol>
|
||||
<h2 id="fichiers-créés">Fichiers créés</h2>
|
||||
<ul>
|
||||
<li><code>Blackboard/Architecture/music_source.md</code> : Documentation
|
||||
complète (15 sections, ~800 lignes)</li>
|
||||
</ul>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>Le document créé fournit un guide complet et pratique pour
|
||||
implémenter une nouvelle MusicSource. Il combine :</p>
|
||||
<ul>
|
||||
<li><strong>Théorie</strong> : Architecture, design patterns,
|
||||
principes</li>
|
||||
<li><strong>Pratique</strong> : Code complet, exemples réels,
|
||||
checklist</li>
|
||||
<li><strong>Référence</strong> : Schemas d’Object ID, intégrations,
|
||||
compatibilité</li>
|
||||
</ul>
|
||||
<p>Un développeur peut suivre ce guide étape par étape pour créer une
|
||||
nouvelle source musicale compatible avec l’écosystème PMOMusic, en
|
||||
s’inspirant des patterns éprouvés de Radio Paradise et Qobuz.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
120
Blackboard_HTML/Report_pmoserver_ext.html
Normal file
120
Blackboard_HTML/Report_pmoserver_ext.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pmoserver_ext</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<h1 id="rapport-documentation-du-pattern-pmoserver_ext">Rapport :
|
||||
Documentation du pattern pmoserver_ext</h1>
|
||||
<h2 id="contexte">Contexte</h2>
|
||||
<p>Documentation du pattern d’extension du PMOServer à travers plusieurs
|
||||
itérations basées sur les retours utilisateur.</p>
|
||||
<h2 id="travail-réalisé">Travail réalisé</h2>
|
||||
<h3 id="analyse-des-fichiers-sources">Analyse des fichiers sources</h3>
|
||||
<p>Les fichiers suivants ont été analysés pour extraire le pattern :</p>
|
||||
<ul>
|
||||
<li><code>pmoapp/src/lib.rs</code> : Pattern SPA avec RustEmbed</li>
|
||||
<li><code>pmocontrol/src/pmoserver_ext.rs</code> : API REST avec Control
|
||||
Point (1506+ lignes)</li>
|
||||
<li><code>pmoparadise/src/pmoserver_ext.rs</code> : API REST simple avec
|
||||
client externe</li>
|
||||
<li><code>pmoaudiocache/src/lib.rs</code> : Extension avec cache et
|
||||
fichiers</li>
|
||||
<li><code>pmomediaserver/src/paradise_streaming.rs</code> : Extension
|
||||
complexe avec streaming</li>
|
||||
</ul>
|
||||
<h3 id="round-1-document-initial">Round 1 : Document initial</h3>
|
||||
<p>Premier jet documentant exhaustivement tous les aspects des
|
||||
extensions (~850 lignes).</p>
|
||||
<h3 id="round-2-recentrage-sur-le-pattern">Round 2 : Recentrage sur le
|
||||
pattern</h3>
|
||||
<p><strong>Annotation</strong> : “se recentrer sur le sujet
|
||||
principal”</p>
|
||||
<p><strong>Actions</strong> : - Réduction de ~850 à ~400 lignes -
|
||||
Suppression des digressions (OpenAPI détaillé, handlers spécifiques) -
|
||||
Focus sur l’anatomie du pattern en 5 étapes - Ajout d’une checklist et
|
||||
d’un exemple minimal</p>
|
||||
<p><strong>Résultat</strong> : Document focalisé sur l’implémentation du
|
||||
pattern uniquement.</p>
|
||||
<h3 id="round-3-réintégration-openapi">Round 3 : Réintégration
|
||||
OpenAPI</h3>
|
||||
<p><strong>Annotation</strong> : “Je trouve que le fait de devoir
|
||||
déclarer et documenter les URL dans OpenAPI / utopia était quelque chose
|
||||
d’important. Remets le.”</p>
|
||||
<p><strong>Actions</strong> : - Ajout d’une section complète
|
||||
“Documentation OpenAPI avec utoipa” (~260 lignes) - 5 sous-sections
|
||||
détaillées : 1. Configuration de base (dépendances Cargo) 2. Définition
|
||||
des schémas avec <code>#[derive(ToSchema)]</code> 3. Annotation des
|
||||
handlers avec <code>#[utoipa::path]</code> 4. Création de la structure
|
||||
<code>#[derive(OpenApi)]</code> 5. Exemple complet extrait de Radio
|
||||
Paradise - Mise à jour de la checklist avec section “Documentation
|
||||
OpenAPI” - Ajout des dépendances <code>utoipa</code> et
|
||||
<code>serde</code> dans la section références</p>
|
||||
<p><strong>Positionnement</strong> : Section insérée après “Méthodes
|
||||
disponibles du serveur” et avant “Patterns courants”, car elle fait
|
||||
partie intégrante de l’implémentation.</p>
|
||||
<h2 id="structure-finale-du-document">Structure finale du document</h2>
|
||||
<ol type="1">
|
||||
<li><strong>Vue d’ensemble</strong> : Principe du pattern</li>
|
||||
<li><strong>Anatomie d’une extension</strong> : 5 étapes détaillées</li>
|
||||
<li><strong>Méthodes disponibles du serveur</strong> : API de
|
||||
<code>pmoserver::Server</code></li>
|
||||
<li><strong>Documentation OpenAPI avec utoipa</strong> : Guide complet
|
||||
en 5 étapes ⭐ <em>Ajouté au Round 3</em></li>
|
||||
<li><strong>Patterns courants</strong> : 3 exemples concrets</li>
|
||||
<li><strong>Gestion des opérations longues</strong> : spawn_blocking,
|
||||
timeouts, background tasks</li>
|
||||
<li><strong>Checklist d’implémentation</strong> : Organisée par
|
||||
catégories</li>
|
||||
<li><strong>Exemple complet minimal</strong> : Code fonctionnel</li>
|
||||
<li><strong>Références</strong> : Fichiers sources et dépendances</li>
|
||||
</ol>
|
||||
<h2 id="résultat-final">Résultat final</h2>
|
||||
<p>Le document est maintenant :</p>
|
||||
<ul>
|
||||
<li><strong>Complet</strong> : Couvre tous les aspects essentiels
|
||||
incluant OpenAPI</li>
|
||||
<li><strong>Structuré</strong> : Progression logique de la configuration
|
||||
à l’implémentation</li>
|
||||
<li><strong>Pratique</strong> : Exemples de code concrets extraits du
|
||||
codebase</li>
|
||||
<li><strong>Actionnable</strong> : Checklist détaillée en 4
|
||||
catégories</li>
|
||||
</ul>
|
||||
<p>Taille finale : ~660 lignes (avec section OpenAPI complète)</p>
|
||||
<h2 id="fichiers-modifiés">Fichiers modifiés</h2>
|
||||
<ul>
|
||||
<li><code>Blackboard/Architecture/pmoserver_ext.md</code> : Document
|
||||
complet avec OpenAPI (660 lignes)</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
54
Blackboard_HTML/ToDiscuss_Pinnable_cache_item.html
Normal file
54
Blackboard_HTML/ToDiscuss_Pinnable_cache_item.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pinnable_cache_item</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<p>La crâte PMOcache, implémente un system de cache qui pourrait être
|
||||
étendu pour permettre une utilisation plus large. L’idée est de modifier
|
||||
les règles de déletion des items. Actuellement le cache a une capacité
|
||||
maximale. Et les items ont des TTL, qui peuvent être non définies.
|
||||
Lorsque le cash est plein, les plus vieux items en termes d’utilisation
|
||||
ou ceux qui ont dépassé leur TTL peuvent être détruits. Je propose de
|
||||
rajouter une fonctionnalité qui permet d’épingler certains items pour
|
||||
les rendre non destructibles. Ils pourraient aussi sortir du comptage
|
||||
général des items pour savoir si le cache est plein.</p>
|
||||
<p>Il faudra modifier la structure de la base de données. Ajouter une
|
||||
colonne indiquant cette propriété. Mettre une règle métier en disant
|
||||
qu’on ne peut pas être à la fois épinglés et avec un TTL.</p>
|
||||
<p>On se moque de maintenir la compatibilité avec la base de données
|
||||
actuelle, il n’y a pas à prévoir de phase de transition. Nous sommes en
|
||||
période de développement.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
59
Blackboard_HTML/ToDiscuss_pmoserver_ext.html
Normal file
59
Blackboard_HTML/ToDiscuss_pmoserver_ext.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>pmoserver_ext</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<p>Partir des fichiers suivants:</p>
|
||||
<ul>
|
||||
<li>pmoapp/src/lib.rs</li>
|
||||
<li>pmocontrol/src/pmoserver_ext.rs</li>
|
||||
<li>pmoparadise/src/pmoserver_ext.rs</li>
|
||||
<li>pmoaudiocache/src/lib.rs</li>
|
||||
<li>pmomediaserver/src/paradise_streaming.rs</li>
|
||||
</ul>
|
||||
<p>réalise une fiche descriptive sur le pattern à réaliser pour
|
||||
implémenter un trait d’extension du PMO serveur.</p>
|
||||
<p>Le résultat sera une documentation d’implémentation qui sera placé
|
||||
dans le fichier:
|
||||
<code>Blackboard/Architecture/pmoserver_ext.md</code></p>
|
||||
<h2 id="round-2">Round 2</h2>
|
||||
<p>J’ai regardé ton document généré et je trouve que tu t’élargis du
|
||||
sujet central documenter lecture d’une extension PMOserver. Peux-tu te
|
||||
recentrer sur le sujet principal.</p>
|
||||
<h2 id="round-3">Round 3</h2>
|
||||
<p>Je trouve que le fait de devoir déclarer et documenter les URL dans
|
||||
OpenAPI / utopia était quelque chose d’important. Remets le.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
563
Blackboard_HTML/ToThinkAbout_MusicBoxSource.html
Normal file
563
Blackboard_HTML/ToThinkAbout_MusicBoxSource.html
Normal file
@@ -0,0 +1,563 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MusicBoxSource</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<h1 id="musicboxsource-bibliothèque-musicale-universelle">MusicBoxSource
|
||||
: Bibliothèque musicale universelle</h1>
|
||||
<p>Créer une <strong>“boîte à musique”</strong> personnelle : un
|
||||
catalogue unifié de morceaux provenant de n’importe quelle source
|
||||
(Qobuz, URLs, fichiers locaux, Radio Paradise, etc.), avec taxonomie de
|
||||
tags et playlists intelligentes.</p>
|
||||
<hr />
|
||||
<h2 id="vision">🎯 Vision</h2>
|
||||
<h3 id="concept">Concept</h3>
|
||||
<p><strong>MusicBoxSource</strong> est une bibliothèque musicale
|
||||
curatoriale qui permet de : - <strong>Collecter</strong> : Ajouter des
|
||||
morceaux depuis n’importe quelle source PMOMusic ou URL -
|
||||
<strong>Organiser</strong> : Classifier avec une taxonomie de tags
|
||||
extensible - <strong>Requêter</strong> : Créer des playlists statiques
|
||||
et smart playlists (requêtes dynamiques) - <strong>Exposer</strong> :
|
||||
Servir via UPnP/DIDL-Lite avec navigation multi-axes</p>
|
||||
<h3 id="différence-avec-pmoplaylist">Différence avec
|
||||
<code>pmoplaylist</code></h3>
|
||||
<ul>
|
||||
<li><strong><code>pmoplaylist</code></strong> : Playlists FIFO
|
||||
<strong>éphémères</strong> pour sources live (Radio Paradise)</li>
|
||||
<li><strong><code>pmomusicbox</code></strong> : Bibliothèque
|
||||
<strong>persistante</strong> cross-sources avec métadonnées
|
||||
enrichies</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="architecture-globale">🏛️ Architecture globale</h2>
|
||||
<pre class="mermaid">flowchart TB
|
||||
subgraph Sources[Sources PMOMusic]
|
||||
QOBUZ[pmoqobuz]
|
||||
PARADISE[pmoparadise]
|
||||
LOCAL[pmolocal - à créer]
|
||||
URL[URLs directes]
|
||||
end
|
||||
|
||||
subgraph Import[Import Layer]
|
||||
IMPORTER[MusicBox Importer]
|
||||
JSPF[pmojspf - Parser playlists]
|
||||
META[pmometadata - Extraction]
|
||||
end
|
||||
|
||||
subgraph Core[pmomusicbox Core]
|
||||
DB[(SQLite Database)]
|
||||
TAXONOMY[Taxonomie Tags]
|
||||
QUERY[Smart Query Engine]
|
||||
end
|
||||
|
||||
subgraph Cache[Cache Layer]
|
||||
AUDIO[pmoaudiocache]
|
||||
COVERS[pmocovers]
|
||||
end
|
||||
|
||||
subgraph Export[Export UPnP]
|
||||
SOURCE[MusicSource Trait]
|
||||
DIDL[DIDL-Lite Generator]
|
||||
BROWSE[Multi-Axis Browser]
|
||||
end
|
||||
|
||||
Sources --> IMPORTER
|
||||
URL --> IMPORTER
|
||||
JSPF --> IMPORTER
|
||||
META --> IMPORTER
|
||||
|
||||
IMPORTER --> DB
|
||||
DB --> TAXONOMY
|
||||
DB --> QUERY
|
||||
|
||||
DB <--> AUDIO
|
||||
DB <--> COVERS
|
||||
|
||||
DB --> SOURCE
|
||||
TAXONOMY --> BROWSE
|
||||
QUERY --> BROWSE
|
||||
SOURCE --> DIDL
|
||||
BROWSE --> DIDL</pre>
|
||||
<hr />
|
||||
<h2 id="modèle-de-données-sqlite">🗄️ Modèle de données (SQLite)</h2>
|
||||
<h3 id="tables-principales">Tables principales</h3>
|
||||
<pre class="mermaid">erDiagram
|
||||
TAG_CATEGORIES ||--o{ TAGS : contient
|
||||
TAG_CATEGORIES ||--o{ TAG_CATEGORIES : parent
|
||||
TAGS ||--o{ ITEM_TAGS : associe
|
||||
MUSIC_ITEMS ||--o{ ITEM_TAGS : a
|
||||
MUSIC_ITEMS ||--o{ PLAYLIST_ITEMS : dans
|
||||
PLAYLISTS ||--o{ PLAYLIST_ITEMS : contient
|
||||
|
||||
TAG_CATEGORIES {
|
||||
text id PK "Ex: mood, genre"
|
||||
text name "Nom affiché"
|
||||
text parent_id FK "Hiérarchie"
|
||||
text color "Hex color"
|
||||
text icon "Emoji/icon"
|
||||
int display_order
|
||||
}
|
||||
|
||||
TAGS {
|
||||
text id PK "Ex: mood:energetic"
|
||||
text category_id FK
|
||||
text name "energetic, chill"
|
||||
text description
|
||||
text color "Override"
|
||||
}
|
||||
|
||||
MUSIC_ITEMS {
|
||||
text id PK "UUID"
|
||||
text source_type "qobuz, url, local"
|
||||
text source_id "ID source"
|
||||
text original_uri "URI source"
|
||||
text cache_audio_pk FK "pmoaudiocache"
|
||||
text cache_cover_pk FK "pmocovers"
|
||||
text title
|
||||
text artist
|
||||
text album
|
||||
int year
|
||||
int rating "1-5 étoiles"
|
||||
int play_count
|
||||
}
|
||||
|
||||
ITEM_TAGS {
|
||||
text item_id PK,FK
|
||||
text tag_id PK,FK
|
||||
int added_at
|
||||
text source "user, auto"
|
||||
}
|
||||
|
||||
PLAYLISTS {
|
||||
text id PK
|
||||
text name
|
||||
bool is_smart
|
||||
text smart_query "JSON"
|
||||
}
|
||||
|
||||
PLAYLIST_ITEMS {
|
||||
text playlist_id PK,FK
|
||||
text item_id FK
|
||||
int position PK
|
||||
}</pre>
|
||||
<h3 id="tables-dassociation">Tables d’association</h3>
|
||||
<ul>
|
||||
<li><strong><code>item_tags</code></strong> : Liens items ↔︎ tags
|
||||
(N:M)</li>
|
||||
<li><strong><code>playlist_items</code></strong> : Items dans playlists
|
||||
statiques (position, ordre)</li>
|
||||
<li><strong><code>tag_synonyms</code></strong> : Synonymes pour
|
||||
recherche (ex: “jazz” → “swing”)</li>
|
||||
</ul>
|
||||
<h3 id="index-recherche">Index & Recherche</h3>
|
||||
<ul>
|
||||
<li><strong>Indexes B-tree</strong> : artist, album, genre, year,
|
||||
rating, play_count</li>
|
||||
<li><strong>FTS5 (Full-Text Search)</strong> : title, artist, album,
|
||||
comment</li>
|
||||
<li><strong>Triggers</strong> : Maintien des tables FTS en sync avec
|
||||
<code>music_items</code></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="taxonomie-par-défaut">🎨 Taxonomie par défaut</h2>
|
||||
<p>Catégories préchargées à l’initialisation :</p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 37%" />
|
||||
<col style="width: 48%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Catégorie</th>
|
||||
<th>Description</th>
|
||||
<th>Exemples de tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Mood</strong></td>
|
||||
<td>État d’esprit, émotion</td>
|
||||
<td>energetic, chill, melancholic, happy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Genre</strong></td>
|
||||
<td>Style musical</td>
|
||||
<td>rock, jazz, classical, electronic, metal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Era</strong></td>
|
||||
<td>Période, décennie</td>
|
||||
<td>60s, 70s, 80s, 90s, contemporary</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Occasion</strong></td>
|
||||
<td>Contexte d’écoute</td>
|
||||
<td>workout, focus, party, driving, sleep</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Tempo</strong></td>
|
||||
<td>Vitesse</td>
|
||||
<td>slow, medium, fast</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Instrument</strong></td>
|
||||
<td>Instrument dominant</td>
|
||||
<td>piano, guitar, vocal, synthesizer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Quality</strong></td>
|
||||
<td>Qualité audio</td>
|
||||
<td>lossless, high-res, remastered, live</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Origin</strong></td>
|
||||
<td>Origine géographique</td>
|
||||
<td>usa, uk, france, japan, latin, africa</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Extensibilité</strong> : L’utilisateur peut créer ses propres
|
||||
catégories et tags.</p>
|
||||
<hr />
|
||||
<h2 id="crates-architecture">📦 Crates architecture</h2>
|
||||
<h3 id="pmojspf---parser-de-playlists-utilitaire">1.
|
||||
<strong><code>pmojspf</code></strong> - Parser de playlists
|
||||
(utilitaire)</h3>
|
||||
<p><strong>But</strong> : Parser/écrire différents formats de playlists
|
||||
vers/depuis un format pivot JSPF (JSON).</p>
|
||||
<pre><code>pmojspf/
|
||||
├── model.rs # Structures JSPF (Playlist, Track, Meta)
|
||||
├── reader/
|
||||
│ ├── jspf.rs # JSON natif
|
||||
│ ├── xspf.rs # XML (via quick-xml ou crate xspf)
|
||||
│ ├── m3u.rs # M3U/M3U8 (parsing ligne par ligne)
|
||||
│ └── pls.rs # PLS (format INI-like)
|
||||
└── writer.rs # Export JSPF</pre>
|
||||
<p><strong>Dépendances</strong> : <code>serde</code>,
|
||||
<code>serde_json</code>, <code>quick-xml</code> (ou <code>xspf</code>
|
||||
crate)</p>
|
||||
<p><strong>Usage</strong> : Réutilisé par <code>pmomusicbox</code> pour
|
||||
import/export</p>
|
||||
<hr />
|
||||
<h3 id="pmomusicbox---bibliothèque-musicale-core">2.
|
||||
<strong><code>pmomusicbox</code></strong> - Bibliothèque musicale
|
||||
core</h3>
|
||||
<p><strong>Responsabilités</strong> : - Gestion base SQLite (CRUD items,
|
||||
tags, playlists) - Import depuis sources PMO (Qobuz, Paradise, Local,
|
||||
URLs) - Smart playlists (query builder + exécution SQL) - Implémentation
|
||||
<code>MusicSource</code> trait (exposition UPnP) - Intégration caches
|
||||
audio/covers</p>
|
||||
<pre><code>pmomusicbox/
|
||||
├── db/
|
||||
│ ├── schema.rs # DDL SQLite + migrations
|
||||
│ ├── items.rs # CRUD music_items
|
||||
│ ├── tags.rs # CRUD tags + taxonomie
|
||||
│ ├── playlists.rs # CRUD playlists statiques
|
||||
│ ├── smart.rs # Smart playlists
|
||||
│ └── search.rs # Full-text search (FTS5)
|
||||
│
|
||||
├── import/
|
||||
│ ├── url.rs # Import URL directe
|
||||
│ ├── source.rs # Import depuis MusicSource
|
||||
│ ├── local.rs # Import fichiers locaux (via pmometadata)
|
||||
│ └── playlist.rs # Import JSPF/M3U8 (via pmojspf)
|
||||
│
|
||||
├── export/
|
||||
│ └── playlist.rs # Export playlists (JSPF, M3U8)
|
||||
│
|
||||
├── query/
|
||||
│ ├── builder.rs # SmartPlaylistQuery (DSL)
|
||||
│ └── executor.rs # Génération + exécution SQL
|
||||
│
|
||||
├── didl/
|
||||
│ └── generator.rs # Conversion items → DIDL-Lite
|
||||
│
|
||||
├── source.rs # Impl MusicSource trait
|
||||
├── taxonomy.rs # Taxonomie par défaut + CRUD
|
||||
└── config_ext.rs # Extension pmoconfig</pre>
|
||||
<p><strong>Dépendances</strong> : - <code>pmosource</code>,
|
||||
<code>pmoaudiocache</code>, <code>pmocovers</code>,
|
||||
<code>pmodidl</code>, <code>pmometadata</code> - <code>pmojspf</code>
|
||||
(import/export playlists) - <code>rusqlite</code> (features:
|
||||
<code>bundled</code>, <code>serde_json</code>) - <code>uuid</code>,
|
||||
<code>serde</code>, <code>tokio</code>, <code>async-trait</code></p>
|
||||
<hr />
|
||||
<h3 id="pmolocal---source-fichiers-locaux-à-créer">3.
|
||||
<strong><code>pmolocal</code></strong> - Source fichiers locaux (à
|
||||
créer)</h3>
|
||||
<p><strong>But</strong> : Scanner des répertoires locaux et exposer les
|
||||
fichiers audio via <code>MusicSource</code>.</p>
|
||||
<pre><code>pmolocal/
|
||||
├── scanner.rs # Scan récursif de répertoires
|
||||
├── watcher.rs # Hot reload (notify)
|
||||
├── source.rs # Impl MusicSource
|
||||
└── config_ext.rs # Extension pmoconfig</pre>
|
||||
<p><strong>Workflow</strong> : 1. <code>pmolocal</code> scanne
|
||||
<code>/home/user/Music</code> 2. <code>pmomusicbox</code> importe les
|
||||
items découverts 3. Tags automatiques basés sur métadonnées (genre,
|
||||
année)</p>
|
||||
<hr />
|
||||
<h2 id="flux-dimport">🔄 Flux d’import</h2>
|
||||
<h3 id="import-depuis-une-source-pmo-ex-qobuz">Import depuis une source
|
||||
PMO (ex: Qobuz)</h3>
|
||||
<pre class="mermaid">sequenceDiagram
|
||||
participant QS as Qobuz Source
|
||||
participant MB as MusicBox Importer
|
||||
participant DB as SQLite DB
|
||||
participant AC as pmoaudiocache
|
||||
participant CC as pmocovers
|
||||
|
||||
QS->>MB: get_item(object_id)
|
||||
MB->>QS: resolve_uri(object_id)
|
||||
|
||||
Note over MB: 1. Extraire métadonnées DIDL-Lite<br/>2. Générer UUID
|
||||
|
||||
MB->>DB: INSERT INTO music_items
|
||||
|
||||
opt Auto-cache activé
|
||||
MB->>AC: Cache audio
|
||||
MB->>CC: Cache cover
|
||||
AC-->>DB: Retourner cache_audio_pk
|
||||
CC-->>DB: Retourner cache_cover_pk
|
||||
end
|
||||
|
||||
MB-->>QS: item_id (UUID)</pre>
|
||||
<h3 id="import-url-directe">Import URL directe</h3>
|
||||
<pre class="mermaid">flowchart LR
|
||||
URL[URL simple] --> META["pmometadata<br/>Extraction"]
|
||||
META --> UUID[Générer UUID]
|
||||
UUID --> DB[("music_items")]
|
||||
DB --> CACHE{"Auto-cache?"}
|
||||
CACHE -->|Oui| AC[pmoaudiocache]
|
||||
CACHE -->|Non| END[Fin]
|
||||
AC --> END</pre>
|
||||
<h3 id="import-playlist-jspfm3u8">Import playlist JSPF/M3U8</h3>
|
||||
<pre class="mermaid">flowchart LR
|
||||
FILE[Fichier playlist] --> JSPF["pmojspf<br/>Parser"]
|
||||
JSPF --> STRUCT[Structure JSPF]
|
||||
STRUCT --> LOOP{"Pour chaque track"}
|
||||
LOOP --> IMPORT[Import comme URL]
|
||||
IMPORT --> DB[("music_items")]
|
||||
DB --> PLAYLIST[Créer playlist statique]
|
||||
PLAYLIST --> LINK[Lier tracks à playlist]</pre>
|
||||
<hr />
|
||||
<h2 id="smart-playlists-query-dsl">🔍 Smart Playlists (Query DSL)</h2>
|
||||
<h3 id="concept-1">Concept</h3>
|
||||
<p>Les smart playlists sont des <strong>requêtes sauvegardées</strong>
|
||||
qui génèrent dynamiquement une liste de tracks.</p>
|
||||
<h3 id="structure-de-requête-json">Structure de requête (JSON)</h3>
|
||||
<div class="sourceCode" id="cb9"><pre
|
||||
class="sourceCode json"><code class="sourceCode json"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">"include_all_tags"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"mood:energetic"</span><span class="ot">,</span> <span class="st">"genre:rock"</span><span class="ot">]</span><span class="fu">,</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">"exclude_tags"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"mood:melancholic"</span><span class="ot">]</span><span class="fu">,</span></span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">"year_min"</span><span class="fu">:</span> <span class="dv">1980</span><span class="fu">,</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">"year_max"</span><span class="fu">:</span> <span class="dv">1989</span><span class="fu">,</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> <span class="dt">"min_rating"</span><span class="fu">:</span> <span class="dv">4</span><span class="fu">,</span></span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a> <span class="dt">"lossless_only"</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
|
||||
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a> <span class="dt">"order_by"</span><span class="fu">:</span> <span class="st">"play_count"</span><span class="fu">,</span></span>
|
||||
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a> <span class="dt">"order"</span><span class="fu">:</span> <span class="st">"desc"</span><span class="fu">,</span></span>
|
||||
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a> <span class="dt">"limit"</span><span class="fu">:</span> <span class="dv">50</span></span>
|
||||
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></pre></div>
|
||||
<h3 id="traduction-sql">Traduction SQL</h3>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode sql"><code class="sourceCode sql"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> <span class="op">*</span> <span class="kw">FROM</span> music_items</span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> <span class="kw">id</span> <span class="kw">IN</span> (</span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> item_id <span class="kw">FROM</span> item_tags <span class="kw">WHERE</span> tag_id <span class="kw">IN</span> (<span class="st">'mood:energetic'</span>, <span class="st">'genre:rock'</span>)</span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">GROUP</span> <span class="kw">BY</span> item_id <span class="kw">HAVING</span> <span class="fu">COUNT</span>(<span class="kw">DISTINCT</span> tag_id) <span class="op">=</span> <span class="dv">2</span> <span class="co">-- ALL tags</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>)</span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a><span class="kw">AND</span> <span class="kw">id</span> <span class="kw">NOT</span> <span class="kw">IN</span> (</span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> item_id <span class="kw">FROM</span> item_tags <span class="kw">WHERE</span> tag_id <span class="op">=</span> <span class="st">'mood:melancholic'</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a>)</span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="kw">AND</span> <span class="dt">year</span> <span class="kw">BETWEEN</span> <span class="dv">1980</span> <span class="kw">AND</span> <span class="dv">1989</span></span>
|
||||
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="kw">AND</span> rating <span class="op">>=</span> <span class="dv">4</span></span>
|
||||
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a><span class="kw">AND</span> codec <span class="kw">IN</span> (<span class="st">'flac'</span>, <span class="st">'alac'</span>)</span>
|
||||
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> play_count <span class="kw">DESC</span></span>
|
||||
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a><span class="kw">LIMIT</span> <span class="dv">50</span>;</span></pre></div>
|
||||
<hr />
|
||||
<h2 id="exposition-upnp-musicsource">🎭 Exposition UPnP
|
||||
(MusicSource)</h2>
|
||||
<h3 id="structure-de-navigation">Structure de navigation</h3>
|
||||
<pre class="mermaid">graph TB
|
||||
ROOT[musicbox/] --> ARTIST[by-artist/]
|
||||
ROOT --> ALBUM[by-album/]
|
||||
ROOT --> GENRE[by-genre/]
|
||||
ROOT --> TAG[by-tag/]
|
||||
ROOT --> PLAYLISTS[playlists/]
|
||||
ROOT --> SMART[smart-playlists/]
|
||||
ROOT --> FAV[favorites/]
|
||||
ROOT --> RECENT[recent/]
|
||||
|
||||
ARTIST --> PF[Pink Floyd/]
|
||||
ARTIST --> Q[Queen/]
|
||||
PF --> WALL[The Wall/]
|
||||
PF --> WYWH[Wish You Were Here/]
|
||||
WALL --> ITEM1[Another Brick... 🎵]
|
||||
|
||||
TAG --> MOOD[mood/]
|
||||
TAG --> OCC[occasion/]
|
||||
TAG --> ERA[era/]
|
||||
|
||||
MOOD --> ENRG[energetic/]
|
||||
MOOD --> CHILL[chill/]
|
||||
ENRG --> ITEMS1[items taggués 🎵]
|
||||
|
||||
OCC --> WORK[workout/]
|
||||
OCC --> FOCUS[focus/]
|
||||
|
||||
ERA --> E80[80s/]
|
||||
ERA --> E90[90s/]
|
||||
|
||||
PLAYLISTS --> PL1[My Favorites/]
|
||||
PLAYLISTS --> PL2[Summer 2024/]
|
||||
|
||||
SMART --> SP1[80s Rock Workout/]
|
||||
SMART --> SP2[Jazz Dinner/]
|
||||
|
||||
style ITEM1 fill:#e1f5ff
|
||||
style ITEMS1 fill:#e1f5ff</pre>
|
||||
<h3 id="object-ids">Object IDs</h3>
|
||||
<pre><code>musicbox:by-artist:{artist_name}
|
||||
musicbox:by-album:{album_id}
|
||||
musicbox:by-tag:{category}:{tag_name}
|
||||
musicbox:playlist:{playlist_id}
|
||||
musicbox:smart:{smart_playlist_id}
|
||||
musicbox:item:{item_id}</pre>
|
||||
<hr />
|
||||
<h2 id="intégration-avec-lécosystème-pmomusic">🔌 Intégration avec
|
||||
l’écosystème PMOMusic</h2>
|
||||
<h3 id="avec-pmoaudiocache">Avec pmoaudiocache</h3>
|
||||
<ul>
|
||||
<li>Import → Déclencher cache automatique (si
|
||||
<code>auto_cache: true</code>)</li>
|
||||
<li><code>resolve_uri()</code> → Retourner URI cachée si disponible</li>
|
||||
</ul>
|
||||
<h3 id="avec-pmocovers">Avec pmocovers</h3>
|
||||
<ul>
|
||||
<li>Import → Télécharger cover art</li>
|
||||
<li>Browse → Inclure <code>album_art</code> dans DIDL-Lite</li>
|
||||
</ul>
|
||||
<h3 id="avec-pmoserver-feature-server">Avec pmoserver (feature
|
||||
<code>server</code>)</h3>
|
||||
<ul>
|
||||
<li>API REST pour manipulation (CRUD items, tags, playlists)</li>
|
||||
<li>SSE pour notifications de changements</li>
|
||||
<li>Endpoints OpenAPI (utoipa)</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="plan-dimplémentation-phases">📝 Plan d’implémentation
|
||||
(Phases)</h2>
|
||||
<h3 id="phase-1-fondations">Phase 1 : Fondations</h3>
|
||||
<ul>
|
||||
<li>Schéma SQLite complet</li>
|
||||
<li>Crate <code>pmojspf</code> (parser playlists)</li>
|
||||
<li>CRUD basique dans <code>pmomusicbox</code> (items, tags)</li>
|
||||
<li>Taxonomie par défaut</li>
|
||||
<li>Import URL simple</li>
|
||||
<li>Extension pmoconfig</li>
|
||||
</ul>
|
||||
<h3 id="phase-2-import-cross-sources">Phase 2 : Import
|
||||
cross-sources</h3>
|
||||
<ul>
|
||||
<li>Import depuis MusicSource (Qobuz, Paradise)</li>
|
||||
<li>Import playlists (JSPF/M3U8)</li>
|
||||
<li>Intégration caches (audio, covers)</li>
|
||||
<li>Crate <code>pmolocal</code> (fichiers locaux)</li>
|
||||
</ul>
|
||||
<h3 id="phase-3-smart-playlists">Phase 3 : Smart Playlists</h3>
|
||||
<ul>
|
||||
<li>Query builder (DSL)</li>
|
||||
<li>Exécuteur SQL</li>
|
||||
<li>CRUD smart playlists</li>
|
||||
<li>Export JSPF</li>
|
||||
</ul>
|
||||
<h3 id="phase-4-musicsource-upnp">Phase 4 : MusicSource UPnP</h3>
|
||||
<ul>
|
||||
<li>Implémentation trait <code>MusicSource</code></li>
|
||||
<li>Génération DIDL-Lite</li>
|
||||
<li>Browse multi-axes (artist, album, tag)</li>
|
||||
<li>Recherche full-text (FTS5)</li>
|
||||
</ul>
|
||||
<h3 id="phase-5-fonctionnalités-avancées">Phase 5 : Fonctionnalités
|
||||
avancées</h3>
|
||||
<ul>
|
||||
<li>Statistiques d’écoute (play_count, last_played)</li>
|
||||
<li>Auto-tagging (genre depuis métadonnées)</li>
|
||||
<li>API REST (feature <code>server</code>)</li>
|
||||
<li>Recommandations (items similaires)</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id="cas-dusage">🎯 Cas d’usage</h2>
|
||||
<h3 id="workflow-typique">Workflow typique</h3>
|
||||
<ol type="1">
|
||||
<li><strong>Découverte</strong> : Écouter Radio Paradise, tomber sur un
|
||||
morceau génial</li>
|
||||
<li><strong>Ajout</strong> :
|
||||
<code>musicbox.import_from_source(&paradise, "track-123")</code></li>
|
||||
<li><strong>Organisation</strong> : Ajouter tags
|
||||
<code>mood:chill</code>, <code>occasion:focus</code></li>
|
||||
<li><strong>Playlist</strong> : Smart playlist “Focus Music” avec
|
||||
requête <code>mood:chill + occasion:focus</code></li>
|
||||
<li><strong>Écoute</strong> : Naviguer dans UPnP →
|
||||
<code>musicbox/smart-playlists/Focus Music/</code></li>
|
||||
</ol>
|
||||
<h3 id="scénario-bibliothèque-mixte">Scénario : Bibliothèque mixte</h3>
|
||||
<ul>
|
||||
<li>Albums Qobuz haute résolution</li>
|
||||
<li>Playlists M3U8 importées depuis iTunes</li>
|
||||
<li>Fichiers FLAC locaux scannés</li>
|
||||
<li>URLs de SoundCloud</li>
|
||||
<li>Tracks Radio Paradise capturés</li>
|
||||
</ul>
|
||||
<p><strong>Tout unifié dans MusicBox, accessible via UPnP, organisé par
|
||||
tags.</strong></p>
|
||||
<hr />
|
||||
<h2 id="références">📚 Références</h2>
|
||||
<h3 id="standards">Standards</h3>
|
||||
<ul>
|
||||
<li><a href="https://www.xspf.org/jspf">JSPF Spec</a></li>
|
||||
<li><a href="https://www.xspf.org/spec">XSPF Spec</a></li>
|
||||
<li><a href="https://www.sqlite.org/fts5.html">SQLite FTS5</a></li>
|
||||
</ul>
|
||||
<h3 id="inspirations">Inspirations</h3>
|
||||
<ul>
|
||||
<li><a href="https://beets.io/">Beets</a> - Music library manager</li>
|
||||
<li><a href="https://www.navidrome.org/">Navidrome</a> - Music
|
||||
server</li>
|
||||
<li><a href="https://picard.musicbrainz.org/">MusicBrainz Picard</a> -
|
||||
Tagger</li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
539
Blackboard_HTML/ToThinkAbout_PlayListSource.html
Normal file
539
Blackboard_HTML/ToThinkAbout_PlayListSource.html
Normal file
@@ -0,0 +1,539 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PlayListSource</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<h1 id="playlistsource-musicsource-pour-playlists">PlaylistSource :
|
||||
MusicSource pour playlists</h1>
|
||||
<p>Implémenter une source PMOMusic capable de servir un catalogue de
|
||||
playlists hiérarchisé via UPnP.</p>
|
||||
<hr />
|
||||
<h2 id="décisions-de-conception">📋 Décisions de conception</h2>
|
||||
<h3 id="format-pivot-jspf-json">Format pivot : JSPF (JSON)</h3>
|
||||
<p><strong>Choix</strong> : JSPF comme format interne central -
|
||||
Métadonnées riches (title, creator, album, annotation, image, duration,
|
||||
etc.) - JSON natif avec serde (Rust-friendly) - Standard ouvert
|
||||
(Xiph.Org) - Extensible via champ <code>meta</code></p>
|
||||
<p><strong>Formats supportés</strong> : - ✅ <strong>JSPF</strong>
|
||||
(.jspf) - JSON, format natif - ✅ <strong>XSPF</strong> (.xspf) - XML,
|
||||
conversion vers JSPF - ✅ <strong>M3U8</strong> (.m3u8) - Texte,
|
||||
métadonnées limitées - ✅ <strong>PLS</strong> (.pls) - INI-like, très
|
||||
basique</p>
|
||||
<p><strong>Architecture</strong> : 1 Writer (JSPF) + 4 Readers (JSPF,
|
||||
XSPF, M3U8, PLS) → Structure JSPF centrale</p>
|
||||
<pre class="mermaid">flowchart LR
|
||||
JSPF[JSPF JSON] --> JR[JspfReader]
|
||||
XSPF[XSPF XML] --> XR[XspfReader]
|
||||
M3U8[M3U8 Text] --> MR[M3uReader]
|
||||
PLS[PLS INI] --> PR[PlsReader]
|
||||
|
||||
JR --> CORE[JSPF Structure]
|
||||
XR --> CORE
|
||||
MR --> CORE
|
||||
PR --> CORE
|
||||
|
||||
CORE --> W[JspfWriter]
|
||||
W --> OUT[.jspf]</pre>
|
||||
<hr />
|
||||
<h2 id="structure-du-répertoire">🗂️ Structure du répertoire</h2>
|
||||
<pre><code>playlists/
|
||||
├── metadata.json # Métadonnées du conteneur racine
|
||||
├── Jazz/
|
||||
│ ├── metadata.json # Métadonnées catégorie Jazz
|
||||
│ ├── standards.jspf
|
||||
│ ├── bebop.jspf
|
||||
│ └── covers/
|
||||
│ └── standards.webp
|
||||
├── Classical/
|
||||
│ ├── metadata.json
|
||||
│ ├── baroque.jspf
|
||||
│ └── romantic.jspf
|
||||
└── Rock/
|
||||
├── metadata.json
|
||||
└── 70s.jspf</pre>
|
||||
<h3 id="fichier-metadata.json-conteneur">Fichier
|
||||
<code>metadata.json</code> (conteneur)</h3>
|
||||
<div class="sourceCode" id="cb3"><pre
|
||||
class="sourceCode json"><code class="sourceCode json"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
|
||||
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">"container"</span><span class="fu">:</span> <span class="fu">{</span></span>
|
||||
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a> <span class="dt">"title"</span><span class="fu">:</span> <span class="st">"Collection Jazz"</span><span class="fu">,</span></span>
|
||||
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a> <span class="dt">"description"</span><span class="fu">:</span> <span class="st">"Mes playlists jazz favorites"</span><span class="fu">,</span></span>
|
||||
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="dt">"creator"</span><span class="fu">:</span> <span class="st">"John Doe"</span><span class="fu">,</span></span>
|
||||
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> <span class="dt">"image"</span><span class="fu">:</span> <span class="st">"covers/jazz-collection.webp"</span><span class="fu">,</span></span>
|
||||
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> <span class="dt">"date"</span><span class="fu">:</span> <span class="st">"2026-01-15"</span><span class="fu">,</span></span>
|
||||
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> <span class="dt">"meta"</span><span class="fu">:</span> <span class="ot">[</span></span>
|
||||
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">{</span><span class="dt">"rel"</span><span class="fu">:</span> <span class="st">"genre"</span><span class="fu">,</span> <span class="dt">"content"</span><span class="fu">:</span> <span class="st">"Jazz"</span><span class="fu">}</span><span class="ot">,</span></span>
|
||||
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">{</span><span class="dt">"rel"</span><span class="fu">:</span> <span class="st">"mood"</span><span class="fu">,</span> <span class="dt">"content"</span><span class="fu">:</span> <span class="st">"Relaxing"</span><span class="fu">}</span></span>
|
||||
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="ot">]</span></span>
|
||||
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">}</span></span>
|
||||
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></pre></div>
|
||||
<hr />
|
||||
<h2 id="composants-à-implémenter">🏗️ Composants à implémenter</h2>
|
||||
<h3 id="crate-pmojspf-parsing-playlists">1. Crate <code>pmojspf</code>
|
||||
(parsing playlists)</h3>
|
||||
<p><strong>Responsabilité</strong> : Parser différents formats de
|
||||
playlist vers structure JSPF unifiée</p>
|
||||
<h4 id="structure">Structure</h4>
|
||||
<pre><code>pmojspf/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── lib.rs # API publique
|
||||
│ ├── model.rs # Structures JSPF
|
||||
│ ├── writer.rs # JspfWriter
|
||||
│ ├── reader/
|
||||
│ │ ├── mod.rs # Trait PlaylistReader
|
||||
│ │ ├── jspf.rs # Reader JSON natif (serde_json)
|
||||
│ │ ├── xspf.rs # Reader XML (xml-rs)
|
||||
│ │ ├── m3u.rs # Reader M3U8 (parsing ligne par ligne)
|
||||
│ │ └── pls.rs # Reader PLS (format INI-like)
|
||||
│ └── error.rs
|
||||
└── tests/
|
||||
└── fixtures/</pre>
|
||||
<h4 id="modèle-de-données">Modèle de données</h4>
|
||||
<p><strong>Inspiré de la crate <a
|
||||
href="https://crates.io/crates/xspf">xspf</a> v0.4.2</strong></p>
|
||||
<div class="sourceCode" id="cb5"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">serde::</span><span class="op">{</span>Deserialize<span class="op">,</span> Serialize<span class="op">};</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="at">)]</span></span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> Jspf <span class="op">{</span></span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> playlist<span class="op">:</span> JspfPlaylist<span class="op">,</span></span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="op">,</span> <span class="bu">Default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>serde<span class="at">(</span>rename_all <span class="op">=</span> <span class="st">"camelCase"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> JspfPlaylist <span class="op">{</span></span>
|
||||
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> title<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> creator<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> annotation<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> info<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> location<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> identifier<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> image<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> date<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> license<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Vec::is_empty"</span><span class="op">,</span> <span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> attribution<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>JspfAttribution<span class="op">>,</span></span>
|
||||
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Vec::is_empty"</span><span class="op">,</span> <span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> meta<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>JspfMeta<span class="op">>,</span></span>
|
||||
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span><span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> track<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>JspfTrack<span class="op">>,</span></span>
|
||||
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="op">,</span> <span class="bu">Default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>serde<span class="at">(</span>rename_all <span class="op">=</span> <span class="st">"camelCase"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> JspfTrack <span class="op">{</span></span>
|
||||
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Vec::is_empty"</span><span class="op">,</span> <span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> location<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Vec::is_empty"</span><span class="op">,</span> <span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> identifier<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> title<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> creator<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> annotation<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> info<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> image<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> album<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> track_num<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u32</span><span class="op">>,</span></span>
|
||||
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Option::is_none"</span><span class="at">)]</span></span>
|
||||
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> duration<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">u64</span><span class="op">>,</span> <span class="co">// millisecondes</span></span>
|
||||
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a> <span class="at">#[</span>serde<span class="at">(</span>skip_serializing_if <span class="op">=</span> <span class="st">"Vec::is_empty"</span><span class="op">,</span> <span class="kw">default</span><span class="at">)]</span></span>
|
||||
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> meta<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>JspfMeta<span class="op">>,</span></span>
|
||||
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="at">)]</span></span>
|
||||
<span id="cb5-65"><a href="#cb5-65" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>serde<span class="at">(</span>untagged<span class="at">)]</span></span>
|
||||
<span id="cb5-66"><a href="#cb5-66" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">enum</span> JspfAttribution <span class="op">{</span></span>
|
||||
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a> Location <span class="op">{</span> location<span class="op">:</span> <span class="dt">String</span> <span class="op">},</span></span>
|
||||
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a> Identifier <span class="op">{</span> identifier<span class="op">:</span> <span class="dt">String</span> <span class="op">},</span></span>
|
||||
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a><span class="at">#[</span>derive<span class="at">(</span><span class="bu">Debug</span><span class="op">,</span> <span class="bu">Clone</span><span class="op">,</span> Serialize<span class="op">,</span> Deserialize<span class="at">)]</span></span>
|
||||
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> JspfMeta <span class="op">{</span></span>
|
||||
<span id="cb5-73"><a href="#cb5-73" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> rel<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb5-74"><a href="#cb5-74" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> content<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb5-75"><a href="#cb5-75" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="trait-playlistreader">Trait PlaylistReader</h4>
|
||||
<div class="sourceCode" id="cb6"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">std::io::</span><span class="bu">Read</span><span class="op">;</span></span>
|
||||
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> PlaylistReader <span class="op">{</span></span>
|
||||
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> read<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">>;</span></span>
|
||||
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> from_str(s<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">>;</span></span>
|
||||
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> from_file<span class="op"><</span>P<span class="op">:</span> <span class="bu">AsRef</span><span class="op"><</span><span class="dt">Path</span><span class="op">>></span>(path<span class="op">:</span> P) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">>;</span></span>
|
||||
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="implémentations-des-readers">Implémentations des Readers</h4>
|
||||
<h5 id="jspfreader-simple---serde_json">JspfReader (✅ Simple -
|
||||
serde_json)</h5>
|
||||
<div class="sourceCode" id="cb7"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> JspfReader<span class="op">;</span></span>
|
||||
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistReader <span class="cf">for</span> JspfReader <span class="op">{</span></span>
|
||||
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> read<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="pp">serde_json::</span>from_reader(reader)</span>
|
||||
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>map_err(<span class="op">|</span>e<span class="op">|</span> <span class="bu">Error</span><span class="pp">::</span>ParseError(<span class="pp">format!</span>(<span class="st">"JSON: {}"</span><span class="op">,</span> e)))</span>
|
||||
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Dépendances</strong> : <code>serde_json</code></p>
|
||||
<h5 id="xspfreader-complexe---xml-rs">XspfReader (⚠️ Complexe -
|
||||
xml-rs)</h5>
|
||||
<p><strong>Approche</strong> : Machine à états XML pour parser
|
||||
<code><playlist></code>, <code><track></code>, etc.</p>
|
||||
<p><strong>Alternative</strong> : Utiliser la crate <code>xspf</code>
|
||||
existante puis convertir → JSPF</p>
|
||||
<div class="sourceCode" id="cb8"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> XspfReader<span class="op">;</span></span>
|
||||
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistReader <span class="cf">for</span> XspfReader <span class="op">{</span></span>
|
||||
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> read<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// Parser XML avec EventReader</span></span>
|
||||
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> <span class="co">// État : in_playlist, in_track, current_element</span></span>
|
||||
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a> <span class="co">// Mapping: <title> → playlist.title, <track> → JspfTrack</span></span>
|
||||
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Dépendances</strong> : <code>xml-rs</code> ou réutiliser
|
||||
<code>xspf</code> crate</p>
|
||||
<h5 id="m3ureader-modéré---ligne-par-ligne">M3uReader (⚙️ Modéré - ligne
|
||||
par ligne)</h5>
|
||||
<p><strong>Format</strong> :</p>
|
||||
<pre class="m3u"><code>#EXTM3U
|
||||
#PLAYLIST:Ma Playlist Jazz
|
||||
#EXTINF:284,John Coltrane - Giant Steps
|
||||
#EXTART:John Coltrane
|
||||
#EXTALB:Giant Steps
|
||||
file:///music/coltrane.flac</pre>
|
||||
<div class="sourceCode" id="cb10"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> M3uReader<span class="op">;</span></span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistReader <span class="cf">for</span> M3uReader <span class="op">{</span></span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> read<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// BufReader ligne par ligne</span></span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="co">// Parser #EXTINF:duration,artist - title</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> <span class="co">// Gérer extensions non-standard (#EXTART, #EXTALB, #EXTIMG)</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Dépendances</strong> : stdlib uniquement</p>
|
||||
<p><strong>Limitations</strong> : Métadonnées pauvres, beaucoup de
|
||||
champs <code>None</code></p>
|
||||
<h5 id="plsreader-modéré---format-ini">PlsReader (⚙️ Modéré - format
|
||||
INI)</h5>
|
||||
<p><strong>Format</strong> :</p>
|
||||
<div class="sourceCode" id="cb11"><pre
|
||||
class="sourceCode ini"><code class="sourceCode ini"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[playlist]</span></span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="dt">NumberOfEntries</span><span class="ot">=</span><span class="dv">2</span></span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="dt">File1</span><span class="ot">=</span><span class="st">file:///music/coltrane.flac</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="dt">Title1</span><span class="ot">=</span><span class="st">John Coltrane - Giant Steps</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="dt">Length1</span><span class="ot">=</span><span class="dv">284</span></span></pre></div>
|
||||
<div class="sourceCode" id="cb12"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> PlsReader<span class="op">;</span></span>
|
||||
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistReader <span class="cf">for</span> PlsReader <span class="op">{</span></span>
|
||||
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> read<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// HashMap<index, (file, title, duration)></span></span>
|
||||
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a> <span class="co">// Parser FileN=..., TitleN=..., LengthN=...</span></span>
|
||||
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a> <span class="co">// Trier par index et convertir en JspfTrack</span></span>
|
||||
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Dépendances</strong> : stdlib uniquement</p>
|
||||
<p><strong>Limitations</strong> : File, Title, Length seulement</p>
|
||||
<h4 id="jspfwriter">JspfWriter</h4>
|
||||
<div class="sourceCode" id="cb13"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> JspfWriter<span class="op">;</span></span>
|
||||
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> JspfWriter <span class="op">{</span></span>
|
||||
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> write<span class="op"><</span>W<span class="op">:</span> <span class="bu">Write</span><span class="op">></span>(jspf<span class="op">:</span> <span class="op">&</span>Jspf<span class="op">,</span> writer<span class="op">:</span> W) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">>;</span></span>
|
||||
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> write_pretty<span class="op"><</span>W<span class="op">:</span> <span class="bu">Write</span><span class="op">></span>(jspf<span class="op">:</span> <span class="op">&</span>Jspf<span class="op">,</span> writer<span class="op">:</span> W) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>()<span class="op">>;</span></span>
|
||||
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> to_string(jspf<span class="op">:</span> <span class="op">&</span>Jspf) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">String</span><span class="op">>;</span></span>
|
||||
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> to_string_pretty(jspf<span class="op">:</span> <span class="op">&</span>Jspf) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span><span class="dt">String</span><span class="op">>;</span></span>
|
||||
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="api-publique">API publique</h4>
|
||||
<div class="sourceCode" id="cb14"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">use</span> <span class="pp">model::</span><span class="op">{</span>Jspf<span class="op">,</span> JspfPlaylist<span class="op">,</span> JspfTrack<span class="op">,</span> JspfMeta<span class="op">,</span> JspfAttribution<span class="op">};</span></span>
|
||||
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">use</span> <span class="pp">reader::</span><span class="op">{</span>PlaylistReader<span class="op">,</span> JspfReader<span class="op">,</span> XspfReader<span class="op">,</span> M3uReader<span class="op">,</span> PlsReader<span class="op">};</span></span>
|
||||
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">use</span> <span class="pp">writer::</span>JspfWriter<span class="op">;</span></span>
|
||||
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">enum</span> PlaylistFormat <span class="op">{</span></span>
|
||||
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a> Jspf<span class="op">,</span></span>
|
||||
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a> Xspf<span class="op">,</span></span>
|
||||
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a> M3u8<span class="op">,</span></span>
|
||||
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a> Pls<span class="op">,</span></span>
|
||||
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistFormat <span class="op">{</span></span>
|
||||
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> <span class="kw">fn</span> from_extension(ext<span class="op">:</span> <span class="op">&</span><span class="dt">str</span>) <span class="op">-></span> <span class="dt">Option</span><span class="op"><</span><span class="dt">Self</span><span class="op">>;</span></span>
|
||||
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">fn</span> read_playlist<span class="op"><</span>R<span class="op">:</span> <span class="bu">Read</span><span class="op">></span>(reader<span class="op">:</span> R<span class="op">,</span> format<span class="op">:</span> PlaylistFormat) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>Jspf<span class="op">>;</span></span></pre></div>
|
||||
<hr />
|
||||
<h3 id="crate-pmoplaylists-playlistsource">2. Crate
|
||||
<code>pmoplaylists</code> (PlaylistSource)</h3>
|
||||
<p><strong>Responsabilité</strong> : Implémenter
|
||||
<code>MusicSource</code> pour servir playlists via UPnP</p>
|
||||
<h4 id="structures-principales">Structures principales</h4>
|
||||
<div class="sourceCode" id="cb15"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> PlaylistSource <span class="op">{</span></span>
|
||||
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a> root_path<span class="op">:</span> <span class="dt">PathBuf</span><span class="op">,</span></span>
|
||||
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a> playlists<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>HashMap<span class="op"><</span><span class="dt">String</span><span class="op">,</span> ParsedPlaylist<span class="op">>>>,</span></span>
|
||||
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a> containers<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>HashMap<span class="op"><</span><span class="dt">PathBuf</span><span class="op">,</span> ContainerMetadata<span class="op">>>>,</span></span>
|
||||
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a> watcher<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="pp">notify::</span>RecommendedWatcher<span class="op">>,</span></span>
|
||||
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a> base_url<span class="op">:</span> <span class="dt">String</span><span class="op">,</span></span>
|
||||
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a> update_counter<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span><span class="dt">u32</span><span class="op">>>,</span></span>
|
||||
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a> last_change<span class="op">:</span> Arc<span class="op"><</span>RwLock<span class="op"><</span>SystemTime<span class="op">>>,</span></span>
|
||||
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ParsedPlaylist <span class="op">{</span></span>
|
||||
<span id="cb15-12"><a href="#cb15-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> metadata<span class="op">:</span> PlaylistMetadata<span class="op">,</span></span>
|
||||
<span id="cb15-13"><a href="#cb15-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> tracks<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>PlaylistTrack<span class="op">>,</span></span>
|
||||
<span id="cb15-14"><a href="#cb15-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> source_path<span class="op">:</span> <span class="dt">PathBuf</span><span class="op">,</span></span>
|
||||
<span id="cb15-15"><a href="#cb15-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> format<span class="op">:</span> PlaylistFormat<span class="op">,</span></span>
|
||||
<span id="cb15-16"><a href="#cb15-16" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb15-17"><a href="#cb15-17" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-18"><a href="#cb15-18" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ContainerMetadata <span class="op">{</span></span>
|
||||
<span id="cb15-19"><a href="#cb15-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> title<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb15-20"><a href="#cb15-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> description<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb15-21"><a href="#cb15-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> creator<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb15-22"><a href="#cb15-22" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> image<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb15-23"><a href="#cb15-23" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> date<span class="op">:</span> <span class="dt">Option</span><span class="op"><</span><span class="dt">String</span><span class="op">>,</span></span>
|
||||
<span id="cb15-24"><a href="#cb15-24" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> meta<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span>MetaEntry<span class="op">>,</span></span>
|
||||
<span id="cb15-25"><a href="#cb15-25" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb15-26"><a href="#cb15-26" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb15-27"><a href="#cb15-27" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">struct</span> ContainerMetadataFile <span class="op">{</span></span>
|
||||
<span id="cb15-28"><a href="#cb15-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">pub</span> container<span class="op">:</span> ContainerMetadata<span class="op">,</span></span>
|
||||
<span id="cb15-29"><a href="#cb15-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<h4 id="fonctionnalités">Fonctionnalités</h4>
|
||||
<ol type="1">
|
||||
<li><strong>Scan hiérarchique</strong> : Parser récursivement dossiers +
|
||||
<code>metadata.json</code> + playlists</li>
|
||||
<li><strong>Cache</strong> : Éviter re-parsing (playlists +
|
||||
conteneurs)</li>
|
||||
<li><strong>Hot reload</strong> : <code>notify</code> pour détecter
|
||||
changements</li>
|
||||
<li><strong>Browse UPnP</strong> : Générer DIDL-Lite avec métadonnées
|
||||
conteneurs</li>
|
||||
<li><strong>Content resolution</strong> : Résoudre URIs via
|
||||
<code>SourceCacheManager</code></li>
|
||||
<li><strong>Cover art</strong> : Servir images playlists, tracks,
|
||||
conteneurs</li>
|
||||
</ol>
|
||||
<h4 id="object-ids">Object IDs</h4>
|
||||
<pre><code>playlists # Racine
|
||||
playlists:category:{path} # Catégorie (dossier)
|
||||
playlists:playlist:{id} # Playlist
|
||||
playlists:playlist:{id}:track:{index} # Track dans playlist</pre>
|
||||
<h4 id="gestion-metadata.json">Gestion <code>metadata.json</code></h4>
|
||||
<div class="sourceCode" id="cb17"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="kw">fn</span> load_container_metadata(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> dir_path<span class="op">:</span> <span class="op">&</span><span class="dt">Path</span>) <span class="op">-></span> <span class="dt">Result</span><span class="op"><</span>ContainerMetadata<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> metadata_path <span class="op">=</span> dir_path<span class="op">.</span>join(<span class="st">"metadata.json"</span>)<span class="op">;</span></span>
|
||||
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> metadata_path<span class="op">.</span>exists() <span class="op">{</span></span>
|
||||
<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> content <span class="op">=</span> <span class="pp">fs::</span>read_to_string(<span class="op">&</span>metadata_path)<span class="op">?;</span></span>
|
||||
<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> file<span class="op">:</span> ContainerMetadataFile <span class="op">=</span> <span class="pp">serde_json::</span>from_str(<span class="op">&</span>content)<span class="op">?;</span></span>
|
||||
<span id="cb17-7"><a href="#cb17-7" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(file<span class="op">.</span>container)</span>
|
||||
<span id="cb17-8"><a href="#cb17-8" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
|
||||
<span id="cb17-9"><a href="#cb17-9" aria-hidden="true" tabindex="-1"></a> <span class="co">// Fallback : nom du répertoire</span></span>
|
||||
<span id="cb17-10"><a href="#cb17-10" aria-hidden="true" tabindex="-1"></a> <span class="cn">Ok</span>(ContainerMetadata <span class="op">{</span></span>
|
||||
<span id="cb17-11"><a href="#cb17-11" aria-hidden="true" tabindex="-1"></a> title<span class="op">:</span> <span class="cn">Some</span>(dir_path<span class="op">.</span>file_name()<span class="op">?.</span>to_str()<span class="op">?.</span>to_string())<span class="op">,</span></span>
|
||||
<span id="cb17-12"><a href="#cb17-12" aria-hidden="true" tabindex="-1"></a> <span class="op">..</span><span class="bu">Default</span><span class="pp">::</span><span class="kw">default</span>()</span>
|
||||
<span id="cb17-13"><a href="#cb17-13" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb17-14"><a href="#cb17-14" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb17-15"><a href="#cb17-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<hr />
|
||||
<h3 id="extension-pmoconfig">3. Extension pmoconfig</h3>
|
||||
<p><strong>Fichier</strong> :
|
||||
<code>pmoplaylists/src/config_ext.rs</code></p>
|
||||
<p><strong>Pattern</strong> : <a
|
||||
href="../Architecture/pmoconfig_ext.md">pmoconfig_ext.md</a></p>
|
||||
<div class="sourceCode" id="cb18"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoconfig::</span>Config<span class="op">;</span></span>
|
||||
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">std::path::</span><span class="op">{</span><span class="dt">Path</span><span class="op">,</span> <span class="dt">PathBuf</span><span class="op">};</span></span>
|
||||
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> DEFAULT_PLAYLISTS_DIR<span class="op">:</span> <span class="op">&</span><span class="dt">str</span> <span class="op">=</span> <span class="st">"playlists"</span><span class="op">;</span></span>
|
||||
<span id="cb18-5"><a href="#cb18-5" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb18-6"><a href="#cb18-6" aria-hidden="true" tabindex="-1"></a><span class="kw">pub</span> <span class="kw">trait</span> PlaylistSourceConfigExt <span class="op">{</span></span>
|
||||
<span id="cb18-7"><a href="#cb18-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_dir(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">PathBuf</span><span class="op">;</span></span>
|
||||
<span id="cb18-8"><a href="#cb18-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_dir<span class="op"><</span>P<span class="op">:</span> <span class="bu">AsRef</span><span class="op"><</span><span class="dt">Path</span><span class="op">>></span>(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> path<span class="op">:</span> P) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">>;</span></span>
|
||||
<span id="cb18-9"><a href="#cb18-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_enabled(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">bool</span><span class="op">;</span></span>
|
||||
<span id="cb18-10"><a href="#cb18-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_enabled(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> enabled<span class="op">:</span> <span class="dt">bool</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">>;</span></span>
|
||||
<span id="cb18-11"><a href="#cb18-11" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_supported_formats(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">>;</span></span>
|
||||
<span id="cb18-12"><a href="#cb18-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_supported_formats(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> formats<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">></span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">>;</span></span>
|
||||
<span id="cb18-13"><a href="#cb18-13" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
|
||||
<span id="cb18-14"><a href="#cb18-14" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb18-15"><a href="#cb18-15" aria-hidden="true" tabindex="-1"></a><span class="kw">impl</span> PlaylistSourceConfigExt <span class="cf">for</span> Config <span class="op">{</span></span>
|
||||
<span id="cb18-16"><a href="#cb18-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_dir(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">PathBuf</span> <span class="op">{</span></span>
|
||||
<span id="cb18-17"><a href="#cb18-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>get_managed_dir(<span class="st">"sources.playlists.directory"</span><span class="op">,</span> DEFAULT_PLAYLISTS_DIR)</span>
|
||||
<span id="cb18-18"><a href="#cb18-18" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>expect(<span class="st">"Failed to get playlists directory"</span>)</span>
|
||||
<span id="cb18-19"><a href="#cb18-19" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-20"><a href="#cb18-20" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-21"><a href="#cb18-21" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_dir<span class="op"><</span>P<span class="op">:</span> <span class="bu">AsRef</span><span class="op"><</span><span class="dt">Path</span><span class="op">>></span>(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> path<span class="op">:</span> P) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb18-22"><a href="#cb18-22" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>set_managed_dir(<span class="st">"sources.playlists.directory"</span><span class="op">,</span> path)</span>
|
||||
<span id="cb18-23"><a href="#cb18-23" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-24"><a href="#cb18-24" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-25"><a href="#cb18-25" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_enabled(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">bool</span> <span class="op">{</span></span>
|
||||
<span id="cb18-26"><a href="#cb18-26" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>get_value(<span class="st">"sources.playlists.enabled"</span>)</span>
|
||||
<span id="cb18-27"><a href="#cb18-27" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>unwrap_or_else(<span class="op">|</span>_<span class="op">|</span> <span class="op">{</span></span>
|
||||
<span id="cb18-28"><a href="#cb18-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> _ <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>set_value(<span class="st">"sources.playlists.enabled"</span><span class="op">,</span> <span class="cn">true</span>)<span class="op">;</span></span>
|
||||
<span id="cb18-29"><a href="#cb18-29" aria-hidden="true" tabindex="-1"></a> <span class="cn">true</span></span>
|
||||
<span id="cb18-30"><a href="#cb18-30" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb18-31"><a href="#cb18-31" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-32"><a href="#cb18-32" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-33"><a href="#cb18-33" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_enabled(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> enabled<span class="op">:</span> <span class="dt">bool</span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb18-34"><a href="#cb18-34" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>set_value(<span class="st">"sources.playlists.enabled"</span><span class="op">,</span> enabled)</span>
|
||||
<span id="cb18-35"><a href="#cb18-35" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-36"><a href="#cb18-36" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-37"><a href="#cb18-37" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> get_playlists_supported_formats(<span class="op">&</span><span class="kw">self</span>) <span class="op">-></span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb18-38"><a href="#cb18-38" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>get_value(<span class="st">"sources.playlists.formats"</span>)</span>
|
||||
<span id="cb18-39"><a href="#cb18-39" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span>unwrap_or_else(<span class="op">|</span>_<span class="op">|</span> <span class="op">{</span></span>
|
||||
<span id="cb18-40"><a href="#cb18-40" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> <span class="kw">default</span> <span class="op">=</span> <span class="pp">vec!</span>[<span class="st">"jspf"</span><span class="op">.</span>into()<span class="op">,</span> <span class="st">"xspf"</span><span class="op">.</span>into()<span class="op">,</span> <span class="st">"m3u8"</span><span class="op">.</span>into()<span class="op">,</span> <span class="st">"pls"</span><span class="op">.</span>into()]<span class="op">;</span></span>
|
||||
<span id="cb18-41"><a href="#cb18-41" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> _ <span class="op">=</span> <span class="kw">self</span><span class="op">.</span>set_value(<span class="st">"sources.playlists.formats"</span><span class="op">,</span> <span class="op">&</span><span class="kw">default</span>)<span class="op">;</span></span>
|
||||
<span id="cb18-42"><a href="#cb18-42" aria-hidden="true" tabindex="-1"></a> <span class="kw">default</span></span>
|
||||
<span id="cb18-43"><a href="#cb18-43" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span>)</span>
|
||||
<span id="cb18-44"><a href="#cb18-44" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-45"><a href="#cb18-45" aria-hidden="true" tabindex="-1"></a> </span>
|
||||
<span id="cb18-46"><a href="#cb18-46" aria-hidden="true" tabindex="-1"></a> <span class="kw">fn</span> set_playlists_supported_formats(<span class="op">&</span><span class="kw">self</span><span class="op">,</span> formats<span class="op">:</span> <span class="dt">Vec</span><span class="op"><</span><span class="dt">String</span><span class="op">></span>) <span class="op">-></span> <span class="pp">anyhow::</span><span class="dt">Result</span><span class="op"><</span>()<span class="op">></span> <span class="op">{</span></span>
|
||||
<span id="cb18-47"><a href="#cb18-47" aria-hidden="true" tabindex="-1"></a> <span class="kw">self</span><span class="op">.</span>set_value(<span class="st">"sources.playlists.formats"</span><span class="op">,</span> formats)</span>
|
||||
<span id="cb18-48"><a href="#cb18-48" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span>
|
||||
<span id="cb18-49"><a href="#cb18-49" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<p><strong>Config YAML</strong> :</p>
|
||||
<div class="sourceCode" id="cb19"><pre
|
||||
class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sources</span><span class="kw">:</span></span>
|
||||
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">playlists</span><span class="kw">:</span></span>
|
||||
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">enabled</span><span class="kw">:</span><span class="at"> </span><span class="ch">true</span></span>
|
||||
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">directory</span><span class="kw">:</span><span class="at"> </span><span class="st">"playlists"</span></span>
|
||||
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">formats</span><span class="kw">:</span></span>
|
||||
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> jspf</span></span>
|
||||
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> xspf</span></span>
|
||||
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> m3u8</span></span>
|
||||
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> pls</span></span></pre></div>
|
||||
<p><strong>Utilisation</strong> :</p>
|
||||
<div class="sourceCode" id="cb20"><pre
|
||||
class="sourceCode rust"><code class="sourceCode rust"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoconfig::</span>Config<span class="op">;</span></span>
|
||||
<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a><span class="kw">use</span> <span class="pp">pmoplaylists::config_ext::</span>PlaylistSourceConfigExt<span class="op">;</span></span>
|
||||
<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb20-4"><a href="#cb20-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> config <span class="op">=</span> <span class="pp">Config::</span>load()<span class="op">?;</span></span>
|
||||
<span id="cb20-5"><a href="#cb20-5" aria-hidden="true" tabindex="-1"></a></span>
|
||||
<span id="cb20-6"><a href="#cb20-6" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> config<span class="op">.</span>get_playlists_enabled() <span class="op">{</span></span>
|
||||
<span id="cb20-7"><a href="#cb20-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> playlists_dir <span class="op">=</span> config<span class="op">.</span>get_playlists_dir()<span class="op">;</span></span>
|
||||
<span id="cb20-8"><a href="#cb20-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> playlist_source <span class="op">=</span> <span class="pp">PlaylistSource::</span>new(playlists_dir<span class="op">,</span> config<span class="op">.</span>clone())<span class="op">?;</span></span>
|
||||
<span id="cb20-9"><a href="#cb20-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></pre></div>
|
||||
<hr />
|
||||
<h2 id="intégration-musicbrainz-optionnelle---phase-2">🔌 Intégration
|
||||
MusicBrainz (optionnelle - Phase 2)</h2>
|
||||
<h3 id="crate-recommandée-musicbrainz_rs">Crate recommandée :
|
||||
<code>musicbrainz_rs</code></h3>
|
||||
<p><a href="https://crates.io/crates/musicbrainz_rs">musicbrainz_rs</a>
|
||||
v0.5+ - Client async/blocking - Rate limiting automatique (1 req/sec) -
|
||||
Support CoverArt Archive - MSRV: Rust 1.71.1</p>
|
||||
<h3 id="cas-dusage">Cas d’usage</h3>
|
||||
<ol type="1">
|
||||
<li><p><strong>Résolution d’identifiants</strong> :</p>
|
||||
<div class="sourceCode" id="cb21"><pre
|
||||
class="sourceCode json"><code class="sourceCode json"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span><span class="dt">"identifier"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"musicbrainz://recording/abc123"</span><span class="ot">]</span><span class="fu">,</span> <span class="dt">"title"</span><span class="fu">:</span> <span class="kw">null</span><span class="fu">}</span></span></pre></div>
|
||||
<p>→ Récupérer métadonnées depuis MusicBrainz</p></li>
|
||||
<li><p><strong>Enrichissement playlists pauvres</strong> : M3U8/PLS →
|
||||
MusicBrainz → métadonnées complètes</p></li>
|
||||
<li><p><strong>Cover art</strong> : CoverArt Archive</p></li>
|
||||
</ol>
|
||||
<h3 id="configuration">Configuration</h3>
|
||||
<div class="sourceCode" id="cb22"><pre
|
||||
class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sources</span><span class="kw">:</span></span>
|
||||
<span id="cb22-2"><a href="#cb22-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">playlists</span><span class="kw">:</span></span>
|
||||
<span id="cb22-3"><a href="#cb22-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">musicbrainz</span><span class="kw">:</span></span>
|
||||
<span id="cb22-4"><a href="#cb22-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">enabled</span><span class="kw">:</span><span class="at"> </span><span class="ch">false</span></span>
|
||||
<span id="cb22-5"><a href="#cb22-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">enrich_metadata</span><span class="kw">:</span><span class="at"> </span><span class="ch">false</span></span>
|
||||
<span id="cb22-6"><a href="#cb22-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">rate_limit_per_sec</span><span class="kw">:</span><span class="at"> </span><span class="dv">1</span></span></pre></div>
|
||||
<p><strong>Stratégie</strong> : - <strong>Phase 1 (MVP)</strong> : Ne
|
||||
pas implémenter, stocker identifiants tel quel - <strong>Phase
|
||||
2</strong> : Dépendance optionnelle, service asynchrone,
|
||||
configurable</p>
|
||||
<hr />
|
||||
<h2 id="prochaines-étapes">📝 Prochaines étapes</h2>
|
||||
<ol type="1">
|
||||
<li>✅ Choix format : JSPF central</li>
|
||||
<li>✅ Modèle données : Structures JSPF</li>
|
||||
<li>✅ Extension pmoconfig : Trait défini</li>
|
||||
<li>⏳ <strong>Implémenter <code>pmojspf</code></strong> :
|
||||
<ul>
|
||||
<li><code>JspfReader</code> (serde_json)</li>
|
||||
<li><code>XspfReader</code> (xml-rs ou crate xspf)</li>
|
||||
<li><code>M3uReader</code> (parsing ligne par ligne)</li>
|
||||
<li><code>PlsReader</code> (format INI)</li>
|
||||
<li><code>JspfWriter</code> (serde_json)</li>
|
||||
</ul></li>
|
||||
<li>⏳ <strong>Implémenter <code>pmoplaylists</code></strong> :
|
||||
<ul>
|
||||
<li><code>PlaylistSource</code> (trait <code>MusicSource</code>)</li>
|
||||
<li>Scan hiérarchique + cache</li>
|
||||
<li>Hot reload (notify)</li>
|
||||
<li>Browse UPnP (DIDL-Lite)</li>
|
||||
<li>Gestion <code>metadata.json</code></li>
|
||||
</ul></li>
|
||||
<li>⏳ Tests avec clients UPnP</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<h2 id="sources">📚 Sources</h2>
|
||||
<h3 id="spécifications">Spécifications</h3>
|
||||
<ul>
|
||||
<li><a href="https://www.xspf.org/spec">XSPF Spec</a></li>
|
||||
<li><a href="https://www.xspf.org/jspf">JSPF Spec</a></li>
|
||||
<li><a href="https://en.wikipedia.org/wiki/M3U">M3U - Wikipedia</a></li>
|
||||
<li><a href="https://en.wikipedia.org/wiki/PLS_(file_format)">PLS -
|
||||
Wikipedia</a></li>
|
||||
</ul>
|
||||
<h3 id="crates-rust">Crates Rust</h3>
|
||||
<ul>
|
||||
<li><a href="https://crates.io/crates/xspf">xspf</a> - Parser XML
|
||||
XSPF</li>
|
||||
<li><a href="https://crates.io/crates/musicbrainz_rs">musicbrainz_rs</a>
|
||||
- API MusicBrainz</li>
|
||||
<li><a href="https://musicbrainz.org/doc/MusicBrainz_API">MusicBrainz
|
||||
API Docs</a></li>
|
||||
</ul>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
55
Blackboard_HTML/Todo_config_ext.html
Normal file
55
Blackboard_HTML/Todo_config_ext.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>config_ext</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<p>Partir des fichiers suivants:</p>
|
||||
<ul>
|
||||
<li>pmocovers/src/config_ext.rs</li>
|
||||
<li>pmoaudiocache/src/config_ext.rs</li>
|
||||
<li>pmoqobuz/src/config_ext.rs</li>
|
||||
<li>pmocache/src/config_ext.rs</li>
|
||||
<li>pmoconfig/PASSWORD_ENCRYPTION.md</li>
|
||||
<li>pmoupnp/src/config_ext.rs</li>
|
||||
<li>pmoparadise/src/config_ext.rs</li>
|
||||
</ul>
|
||||
<p>réalise une fiche descriptive sur le pattern à réaliser pour
|
||||
implémenter un trait d’extension de PMOConfig (pmoconfig::Config).</p>
|
||||
<p>Le résultat sera une documentation d’implémentation qui sera placé
|
||||
dans le fichier:
|
||||
<code>Blackboard/Architecture/pmoconfig_ext.md</code></p>
|
||||
<p>Reste bien focalisé sur l’objectif principal.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
52
Blackboard_HTML/Todo_music_source.html
Normal file
52
Blackboard_HTML/Todo_music_source.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>music_source</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({startOnLoad: true, theme: "default"});
|
||||
</script>
|
||||
<style>
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
pre.mermaid {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
<p class="back-link"><a href="index.html">← Retour à l'index</a></p>
|
||||
<p><strong>Il faut suivre les instructions générales placées dans le
|
||||
fichier : Blackboard/Rules.md</strong></p>
|
||||
<p>Partir des fichiers suivants:</p>
|
||||
<ul>
|
||||
<li>pmoparadise/src/source.rs</li>
|
||||
<li>pmoqobuz/src/source.rs</li>
|
||||
<li>pmosource/README.md</li>
|
||||
<li>pmosource/ARCHITECTURE.md</li>
|
||||
</ul>
|
||||
<p>D’écrire dans un fichier d’architecture L’implémentation d’une
|
||||
nouvelle MusicSource.</p>
|
||||
<p>Le résultat sera une documentation d’implémentation qui sera placé
|
||||
dans le fichier:
|
||||
<code>Blackboard/Architecture/music_source.md</code></p>
|
||||
<p>Reste bien focalisé sur l’objectif principal.</p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
40
Blackboard_HTML/index.html
Normal file
40
Blackboard_HTML/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr"><head><meta charset="utf-8">
|
||||
<title>PMOMusic Blackboard</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;margin:20px;background:#f5f5f5}
|
||||
h1{color:#2c3e50}ul{list-style:none;padding:0}
|
||||
li{margin:10px 0}a{color:#3498db;text-decoration:none}
|
||||
a:hover{text-decoration:underline}.category{margin-top:30px}
|
||||
.category h2{color:#e74c3c;border-bottom:2px solid #e74c3c;padding-bottom:5px}
|
||||
</style></head><body>
|
||||
<h1>📋 PMOMusic Blackboard</h1>
|
||||
<div class='category'><h2>Architecture</h2><ul>
|
||||
<li><a href='Architecture_music_source.html'>music_source</a></li>
|
||||
<li><a href='Architecture_pmoconfig_ext.html'>pmoconfig_ext</a></li>
|
||||
<li><a href='Architecture_pmoserver_ext.html'>pmoserver_ext</a></li>
|
||||
</ul></div>
|
||||
<div class='category'><h2>ToThinkAbout</h2><ul>
|
||||
<li><a href='ToThinkAbout_MusicBoxSource.html'>MusicBoxSource</a></li>
|
||||
<li><a href='ToThinkAbout_PlayListSource.html'>PlayListSource</a></li>
|
||||
</ul></div>
|
||||
<div class='category'><h2>ToDiscuss</h2><ul>
|
||||
<li><a href='ToDiscuss_Pinnable_cache_item.html'>Pinnable_cache_item</a></li>
|
||||
<li><a href='ToDiscuss_pmoserver_ext.html'>pmoserver_ext</a></li>
|
||||
</ul></div>
|
||||
<div class='category'><h2>Todo</h2><ul>
|
||||
<li><a href='Todo_config_ext.html'>config_ext</a></li>
|
||||
<li><a href='Todo_music_source.html'>music_source</a></li>
|
||||
</ul></div>
|
||||
<div class='category'><h2>Done</h2><ul>
|
||||
<li><a href='Done_Pinnable_cache_item.html'>Pinnable_cache_item</a></li>
|
||||
<li><a href='Done_WeabApp_debouncingSSE.html'>WeabApp_debouncingSSE</a></li>
|
||||
</ul></div>
|
||||
<div class='category'><h2>Report</h2><ul>
|
||||
<li><a href='Report_Pinnable_cache_item.html'>Pinnable_cache_item</a></li>
|
||||
<li><a href='Report_WeabApp_debouncingSSE.html'>WeabApp_debouncingSSE</a></li>
|
||||
<li><a href='Report_config_ext.html'>config_ext</a></li>
|
||||
<li><a href='Report_music_source.html'>music_source</a></li>
|
||||
<li><a href='Report_pmoserver_ext.html'>pmoserver_ext</a></li>
|
||||
</ul></div>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user