/
/
/
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Music Assistant API - Schemas Reference</title>
7 <link rel="stylesheet" href="../resources/common.css">
8 <style>
9 .nav-container {
10 background: var(--panel);
11 padding: 1rem 2rem;
12 box-shadow: 0 2px 5px rgba(0,0,0,0.05);
13 position: sticky;
14 top: 0;
15 z-index: 100;
16 }
17 .search-box input {
18 width: 100%;
19 max-width: 600px;
20 padding: 0.6rem 1rem;
21 font-size: 0.95em;
22 border: 2px solid var(--border);
23 border-radius: 8px;
24 display: block;
25 margin: 0 auto;
26 background: var(--panel);
27 color: var(--fg);
28 }
29 .search-box input:focus {
30 outline: none;
31 border-color: var(--primary);
32 }
33 .container {
34 max-width: 1200px;
35 margin: 2rem auto;
36 padding: 0 2rem;
37 }
38 .loading {
39 text-align: center;
40 padding: 3rem;
41 color: var(--text-secondary);
42 font-size: 1.1em;
43 }
44 .error {
45 text-align: center;
46 padding: 3rem;
47 color: #e74c3c;
48 font-size: 1.1em;
49 }
50 .schema {
51 background: var(--panel);
52 margin-bottom: 1.5rem;
53 border-radius: 12px;
54 box-shadow: 0 2px 10px rgba(0,0,0,0.08);
55 overflow: hidden;
56 scroll-margin-top: 100px;
57 }
58 .schema-header {
59 background: var(--primary);
60 color: var(--fg);
61 padding: 1rem 1.5rem;
62 cursor: pointer;
63 user-select: none;
64 display: flex;
65 justify-content: space-between;
66 align-items: center;
67 }
68 .schema-header:hover {
69 opacity: 0.95;
70 }
71 .schema-name {
72 font-size: 1.3em;
73 font-weight: 600;
74 font-family: 'Monaco', 'Courier New', monospace;
75 }
76 .schema-expand-icon {
77 font-size: 1.2em;
78 transition: transform 0.3s;
79 }
80 .schema-expand-icon.expanded {
81 transform: rotate(180deg);
82 }
83 .schema-content {
84 padding: 1.5rem;
85 display: none;
86 }
87 .schema-content.show {
88 display: block;
89 }
90 .schema-description {
91 color: var(--text-secondary);
92 margin-bottom: 1rem;
93 font-style: italic;
94 }
95 .properties-section {
96 margin-top: 1rem;
97 }
98 .properties-title {
99 font-weight: 600;
100 margin-bottom: 0.5rem;
101 font-size: 1.1em;
102 }
103 .property {
104 background: var(--background);
105 padding: 0.75rem 1rem;
106 margin: 0.5rem 0;
107 border-radius: 6px;
108 border-left: 3px solid var(--primary);
109 }
110 .property-name {
111 font-family: 'Monaco', 'Courier New', monospace;
112 color: var(--primary);
113 font-weight: 600;
114 font-size: 1em;
115 }
116 .property-required {
117 display: inline-block;
118 background: #e74c3c;
119 color: #ffffff;
120 padding: 0.15rem 0.5rem;
121 border-radius: 4px;
122 font-size: 0.75em;
123 font-weight: 600;
124 margin-left: 0.5rem;
125 }
126 .property-optional {
127 display: inline-block;
128 background: #95a5a6;
129 color: #ffffff;
130 padding: 0.15rem 0.5rem;
131 border-radius: 4px;
132 font-size: 0.75em;
133 font-weight: 600;
134 margin-left: 0.5rem;
135 }
136 .property-nullable {
137 display: inline-block;
138 background: #f39c12;
139 color: #ffffff;
140 padding: 0.15rem 0.5rem;
141 border-radius: 4px;
142 font-size: 0.75em;
143 font-weight: 600;
144 margin-left: 0.5rem;
145 }
146 .property-type {
147 color: var(--text-secondary);
148 font-size: 0.9em;
149 margin-left: 0.5rem;
150 font-family: 'Monaco', 'Courier New', monospace;
151 }
152 .property-description {
153 color: var(--text-secondary);
154 margin-top: 0.25rem;
155 font-size: 0.95em;
156 }
157 .type-link {
158 color: var(--primary);
159 text-decoration: none;
160 border-bottom: 1px dashed var(--primary);
161 transition: all 0.2s;
162 }
163 .type-link:hover {
164 opacity: 0.8;
165 border-bottom-color: transparent;
166 }
167 .hidden {
168 display: none;
169 }
170 .back-link {
171 display: inline-block;
172 margin-bottom: 1rem;
173 padding: 0.5rem 1rem;
174 background: var(--primary);
175 color: #ffffff;
176 text-decoration: none;
177 border-radius: 6px;
178 transition: background 0.2s;
179 }
180 .back-link:hover {
181 opacity: 0.9;
182 }
183 .openapi-link {
184 display: inline-block;
185 padding: 0.5rem 1rem;
186 background: #2e7d32;
187 color: #ffffff;
188 text-decoration: none;
189 border-radius: 6px;
190 transition: background 0.2s;
191 }
192 .openapi-link:hover {
193 background: #1b5e20;
194 }
195 .enum-values {
196 margin-top: 0.5rem;
197 padding: 0.5rem;
198 background: var(--panel);
199 border-radius: 4px;
200 }
201 .enum-values-title {
202 font-weight: 600;
203 color: var(--text-secondary);
204 font-size: 0.9em;
205 margin-bottom: 0.25rem;
206 }
207 .enum-value {
208 display: inline-block;
209 padding: 0.2rem 0.5rem;
210 margin: 0.2rem;
211 background: var(--success-bg);
212 border: 1px solid var(--success-border);
213 border-radius: 4px;
214 font-family: 'Monaco', 'Courier New', monospace;
215 font-size: 0.85em;
216 color: var(--success);
217 }
218 .header .logo {
219 margin-bottom: 1rem;
220 }
221 .header .logo img {
222 width: 60px;
223 height: 60px;
224 object-fit: contain;
225 }
226 .array-type {
227 font-style: italic;
228 }
229 </style>
230</head>
231<body>
232 <div class="header">
233 <div class="logo">
234 <img src="/logo.png" alt="Music Assistant">
235 </div>
236 <h1>Schemas Reference</h1>
237 <p>Data models and types used in the Music Assistant API</p>
238 </div>
239
240 <div class="nav-container">
241 <div class="search-box">
242 <input type="text" id="search" placeholder="Search schemas..." />
243 </div>
244 </div>
245
246 <div class="container">
247 <a href="/api-docs" class="back-link">â Back to API Documentation</a>
248 <div id="schemas-container">
249 <div class="loading">Loading schemas...</div>
250 </div>
251 <div style="text-align: center; margin-top: 3rem; padding: 2rem 0;">
252 <a href="/api-docs/openapi.json" class="openapi-link" download>
253 Download OpenAPI Spec
254 </a>
255 </div>
256 </div>
257
258 <script>
259 // Fetch schemas from API and render them
260 async function loadSchemas() {
261 const container = document.getElementById('schemas-container');
262
263 try {
264 const response = await fetch('/api-docs/schemas.json');
265 if (!response.ok) {
266 throw new Error(`HTTP error! status: ${response.status}`);
267 }
268 const schemas = await response.json();
269
270 // Clear loading message
271 container.innerHTML = '';
272
273 // Render each schema
274 const sortedSchemaNames = Object.keys(schemas).sort();
275 sortedSchemaNames.forEach(schemaName => {
276 const schemaElement = createSchemaElement(schemaName, schemas[schemaName]);
277 container.appendChild(schemaElement);
278 });
279
280 // Handle deep linking after schemas are loaded
281 handleDeepLinking();
282
283 } catch (error) {
284 console.error('Error loading schemas:', error);
285 container.innerHTML = '<div class="error">Failed to load schemas. Please try again later.</div>';
286 }
287 }
288
289 function createSchemaElement(schemaName, schemaDef) {
290 const schemaDiv = document.createElement('div');
291 schemaDiv.className = 'schema';
292 schemaDiv.id = `schema-${schemaName}`;
293 schemaDiv.setAttribute('data-schema', schemaName);
294
295 // Schema header
296 const headerDiv = document.createElement('div');
297 headerDiv.className = 'schema-header';
298 headerDiv.onclick = function() { toggleSchema(this); };
299
300 const nameDiv = document.createElement('div');
301 nameDiv.className = 'schema-name';
302 nameDiv.textContent = schemaName;
303
304 const iconDiv = document.createElement('div');
305 iconDiv.className = 'schema-expand-icon';
306 iconDiv.textContent = 'â¼';
307
308 headerDiv.appendChild(nameDiv);
309 headerDiv.appendChild(iconDiv);
310 schemaDiv.appendChild(headerDiv);
311
312 // Schema content
313 const contentDiv = document.createElement('div');
314 contentDiv.className = 'schema-content';
315
316 // Add description if available
317 if (schemaDef.description) {
318 const descDiv = document.createElement('div');
319 descDiv.className = 'schema-description';
320 descDiv.textContent = schemaDef.description;
321 contentDiv.appendChild(descDiv);
322 }
323
324 // Add properties if available
325 if (schemaDef.properties) {
326 const propertiesSection = document.createElement('div');
327 propertiesSection.className = 'properties-section';
328
329 const propertiesTitle = document.createElement('div');
330 propertiesTitle.className = 'properties-title';
331 propertiesTitle.textContent = 'Properties:';
332 propertiesSection.appendChild(propertiesTitle);
333
334 const requiredFields = schemaDef.required || [];
335
336 Object.entries(schemaDef.properties).forEach(([propName, propDef]) => {
337 const propertyDiv = createPropertyElement(propName, propDef, requiredFields);
338 propertiesSection.appendChild(propertyDiv);
339 });
340
341 contentDiv.appendChild(propertiesSection);
342 }
343
344 schemaDiv.appendChild(contentDiv);
345 return schemaDiv;
346 }
347
348 function createPropertyElement(propName, propDef, requiredFields) {
349 const propertyDiv = document.createElement('div');
350 propertyDiv.className = 'property';
351
352 // Property name
353 const nameSpan = document.createElement('span');
354 nameSpan.className = 'property-name';
355 nameSpan.textContent = propName;
356 propertyDiv.appendChild(nameSpan);
357
358 // Check if field is required
359 const isRequired = requiredFields.includes(propName);
360
361 // Check if field is nullable
362 const isNullable = isPropertyNullable(propDef);
363
364 // Add required/optional badge
365 const badge = document.createElement('span');
366 badge.className = isRequired ? 'property-required' : 'property-optional';
367 badge.textContent = isRequired ? 'REQUIRED' : 'OPTIONAL';
368 propertyDiv.appendChild(badge);
369
370 // Add nullable badge if applicable
371 if (isNullable) {
372 const nullableBadge = document.createElement('span');
373 nullableBadge.className = 'property-nullable';
374 nullableBadge.textContent = 'NULLABLE';
375 propertyDiv.appendChild(nullableBadge);
376 }
377
378 // Add type
379 const typeSpan = document.createElement('span');
380 typeSpan.className = 'property-type';
381 typeSpan.innerHTML = formatPropertyType(propDef);
382 propertyDiv.appendChild(typeSpan);
383
384 // Add description
385 if (propDef.description) {
386 const descDiv = document.createElement('div');
387 descDiv.className = 'property-description';
388 descDiv.textContent = propDef.description;
389 propertyDiv.appendChild(descDiv);
390 }
391
392 // Add enum values if present
393 if (propDef.enum) {
394 const enumDiv = document.createElement('div');
395 enumDiv.className = 'enum-values';
396
397 const enumTitle = document.createElement('div');
398 enumTitle.className = 'enum-values-title';
399 enumTitle.textContent = 'Possible values:';
400 enumDiv.appendChild(enumTitle);
401
402 propDef.enum.forEach(enumVal => {
403 const enumSpan = document.createElement('span');
404 enumSpan.className = 'enum-value';
405 enumSpan.textContent = enumVal;
406 enumDiv.appendChild(enumSpan);
407 });
408
409 propertyDiv.appendChild(enumDiv);
410 }
411
412 return propertyDiv;
413 }
414
415 function isPropertyNullable(propDef) {
416 if (propDef.type === 'null') {
417 return true;
418 }
419 if (propDef.anyOf) {
420 return propDef.anyOf.some(item => item.type === 'null');
421 }
422 if (propDef.oneOf) {
423 return propDef.oneOf.some(item => item.type === 'null');
424 }
425 return false;
426 }
427
428 function formatPropertyType(propDef) {
429 // Handle simple types
430 if (propDef.type && propDef.type !== 'null') {
431 if (propDef.type === 'array' && propDef.items) {
432 const itemType = formatPropertyType(propDef.items);
433 return `<span class="array-type">array of ${itemType}</span>`;
434 }
435 return propDef.type;
436 }
437
438 // Handle $ref
439 if (propDef.$ref) {
440 const refType = propDef.$ref.split('/').pop();
441 return `<a href="#schema-${refType}" class="type-link">${refType}</a>`;
442 }
443
444 // Handle anyOf/oneOf
445 if (propDef.anyOf) {
446 const types = propDef.anyOf
447 .filter(item => item.type !== 'null')
448 .map(item => formatPropertyType(item));
449 return types.join(' | ');
450 }
451
452 if (propDef.oneOf) {
453 const types = propDef.oneOf
454 .filter(item => item.type !== 'null')
455 .map(item => formatPropertyType(item));
456 return types.join(' | ');
457 }
458
459 return 'any';
460 }
461
462 // Toggle schema details
463 function toggleSchema(header) {
464 const schema = header.parentElement;
465 const content = schema.querySelector('.schema-content');
466 const icon = header.querySelector('.schema-expand-icon');
467
468 content.classList.toggle('show');
469 icon.classList.toggle('expanded');
470 }
471
472 // Handle deep linking - expand and scroll to schema
473 function handleDeepLinking() {
474 const hash = window.location.hash;
475 if (hash && hash.startsWith('#schema-')) {
476 scrollToSchema(hash);
477 }
478 }
479
480 function scrollToSchema(hash) {
481 const schemaElement = document.querySelector(hash);
482 if (schemaElement) {
483 // Expand the schema
484 const content = schemaElement.querySelector('.schema-content');
485 const icon = schemaElement.querySelector('.schema-expand-icon');
486 if (content && icon) {
487 content.classList.add('show');
488 icon.classList.add('expanded');
489 }
490 // Scroll to it
491 setTimeout(() => {
492 schemaElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
493 // Highlight temporarily
494 schemaElement.style.transition = 'opacity 0.3s';
495 const originalOpacity = schemaElement.style.opacity || '1';
496 schemaElement.style.opacity = '0.5';
497 setTimeout(() => {
498 schemaElement.style.opacity = originalOpacity;
499 }, 300);
500 setTimeout(() => {
501 schemaElement.style.opacity = originalOpacity;
502 }, 600);
503 }, 100);
504 }
505 }
506
507 // Listen for hash changes (when user clicks a type link)
508 window.addEventListener('hashchange', function() {
509 const hash = window.location.hash;
510 if (hash && hash.startsWith('#schema-')) {
511 scrollToSchema(hash);
512 }
513 });
514
515 // Search functionality
516 document.getElementById('search').addEventListener('input', function(e) {
517 const searchTerm = e.target.value.toLowerCase();
518 const schemas = document.querySelectorAll('.schema');
519
520 schemas.forEach(schema => {
521 const schemaName = schema.dataset.schema;
522 const schemaText = schema.textContent.toLowerCase();
523 const nameMatch = schemaName.toLowerCase().includes(searchTerm);
524 const textMatch = schemaText.includes(searchTerm);
525
526 if (nameMatch || textMatch) {
527 schema.classList.remove('hidden');
528 // Expand if search term is present and not just in the name
529 if (searchTerm && textMatch) {
530 const content = schema.querySelector('.schema-content');
531 const icon = schema.querySelector('.schema-expand-icon');
532 if (content && icon && !content.classList.contains('show')) {
533 content.classList.add('show');
534 icon.classList.add('expanded');
535 }
536 }
537 } else {
538 schema.classList.add('hidden');
539 }
540 });
541 });
542
543 // Load schemas on page load
544 loadSchemas();
545 </script>
546</body>
547</html>
548