WIP: hi-res support

This commit is contained in:
Julien Maille 2026-01-14 23:12:20 +01:00
parent 3e212a7e5b
commit 1e7595c159
7 changed files with 658 additions and 48 deletions

View file

@ -1106,6 +1106,7 @@
<span class="description">Quality for streaming playback</span>
</div>
<select id="streaming-quality-setting">
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option>
<option value="LOSSLESS">FLAC (Lossless)</option>
<option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option>
@ -1117,6 +1118,7 @@
<span class="description">Quality for track downloads</span>
</div>
<select id="download-quality-setting">
<option value="HI_RES_LOSSLESS">Hi-Res FLAC (24-bit)</option>
<option value="LOSSLESS">FLAC (Lossless)</option>
<option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option>

View file

@ -2,6 +2,7 @@
import { RATE_LIMIT_ERROR_MESSAGE, deriveTrackQuality, delay } from './utils.js';
import { APICache } from './cache.js';
import { addMetadataToAudio } from './metadata.js';
import { DashDownloader } from './dash-downloader.js';
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
@ -215,6 +216,12 @@ export class LosslessAPI {
try {
const decoded = atob(manifest);
// Check if it's a DASH manifest (XML)
if (decoded.includes('<MPD')) {
const blob = new Blob([decoded], { type: 'application/dash+xml' });
return URL.createObjectURL(blob);
}
try {
const parsed = JSON.parse(decoded);
if (parsed?.urls?.[0]) {
@ -883,6 +890,7 @@ export class LosslessAPI {
try {
const lookup = await this.getTrack(id, quality);
let streamUrl;
let blob;
if (lookup.originalTrackUrl) {
streamUrl = lookup.originalTrackUrl;
@ -893,51 +901,70 @@ export class LosslessAPI {
}
}
const response = await fetch(streamUrl, {
cache: 'no-store',
signal: options.signal,
});
// Handle DASH streams (blob URLs)
if (streamUrl.startsWith('blob:')) {
try {
const downloader = new DashDownloader();
blob = await downloader.downloadDashStream(streamUrl, {
signal: options.signal,
onProgress: options.onProgress
});
} catch (dashError) {
console.error('DASH download failed:', dashError);
// Fallback to LOSSLESS if DASH fails
if (quality !== 'LOSSLESS') {
console.warn('Falling back to LOSSLESS (16-bit) download.');
return this.downloadTrack(id, 'LOSSLESS', filename, options);
}
throw dashError;
}
} else {
const response = await fetch(streamUrl, {
cache: 'no-store',
signal: options.signal,
});
if (!response.ok) {
throw new Error(`Fetch failed: ${response.status}`);
}
if (!response.ok) {
throw new Error(`Fetch failed: ${response.status}`);
}
// ... (standard handling for Content-Length and body reader)
const contentLength = response.headers.get('Content-Length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
const contentLength = response.headers.get('Content-Length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
let receivedBytes = 0;
let receivedBytes = 0;
let blob;
if (response.body && onProgress) {
const reader = response.body.getReader();
const chunks = [];
if (response.body && onProgress) {
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
chunks.push(value);
receivedBytes += value.byteLength;
if (value) {
chunks.push(value);
receivedBytes += value.byteLength;
onProgress({
stage: 'downloading',
receivedBytes,
totalBytes: totalBytes || undefined,
});
}
}
blob = new Blob(chunks, { type: response.headers.get('Content-Type') || 'audio/flac' });
} else {
blob = await response.blob();
if (onProgress) {
onProgress({
stage: 'downloading',
receivedBytes,
totalBytes: totalBytes || undefined,
receivedBytes: blob.size,
totalBytes: blob.size,
});
}
}
blob = new Blob(chunks, { type: response.headers.get('Content-Type') || 'audio/flac' });
} else {
blob = await response.blob();
if (onProgress) {
onProgress({
stage: 'downloading',
receivedBytes: blob.size,
totalBytes: blob.size,
});
}
}
// Add metadata if track information is provided

200
js/dash-downloader.js Normal file
View file

@ -0,0 +1,200 @@
export class DashDownloader {
constructor() {}
async downloadDashStream(manifestBlobUrl, options = {}) {
const { onProgress, signal } = options;
// 1. Fetch and Parse Manifest
const response = await fetch(manifestBlobUrl);
const manifestText = await response.text();
const manifest = this.parseManifest(manifestText);
if (!manifest) {
throw new Error("Failed to parse DASH manifest");
}
// 2. Generate URLs
const urls = this.generateSegmentUrls(manifest);
// 3. Download Segments
const chunks = [];
let downloadedBytes = 0;
// Estimate total size? Hard to know exactly without Content-Length of each.
// We can just track progress by segment count.
const totalSegments = urls.length;
for (let i = 0; i < urls.length; i++) {
if (signal?.aborted) throw new Error('AbortError');
const url = urls[i];
const segmentResponse = await fetch(url, { signal });
if (!segmentResponse.ok) {
// Retry once?
console.warn(`Failed to fetch segment ${i}, retrying...`);
await new Promise(r => setTimeout(r, 1000));
const retryResponse = await fetch(url, { signal });
if (!retryResponse.ok) throw new Error(`Failed to fetch segment ${i}: ${retryResponse.status}`);
const chunk = await retryResponse.arrayBuffer();
chunks.push(chunk);
downloadedBytes += chunk.byteLength;
} else {
const chunk = await segmentResponse.arrayBuffer();
chunks.push(chunk);
downloadedBytes += chunk.byteLength;
}
if (onProgress) {
onProgress({
stage: 'downloading',
receivedBytes: downloadedBytes, // accurate byte count
totalBytes: undefined, // Unknown total
currentSegment: i + 1,
totalSegments: totalSegments
});
}
}
// 4. Concatenate
return new Blob(chunks, { type: 'audio/mp4' });
}
parseManifest(manifestText) {
const parser = new DOMParser();
const xml = parser.parseFromString(manifestText, "text/xml");
const mpd = xml.querySelector("MPD");
if (!mpd) throw new Error("Invalid DASH manifest: No MPD tag");
const period = mpd.querySelector("Period");
if (!period) throw new Error("Invalid DASH manifest: No Period tag");
// Prefer highest bandwidth audio adaptation set
const adaptationSets = Array.from(period.querySelectorAll("AdaptationSet"));
let audioSet = adaptationSets.find(as => as.getAttribute("mimeType")?.startsWith("audio"));
// Fallback: look for any adaptation set if mimeType is missing (rare)
if (!audioSet && adaptationSets.length > 0) audioSet = adaptationSets[0];
if (!audioSet) throw new Error("No AdaptationSet found");
// Find Representation
// Get all representations and sort by bandwidth descending
const representations = Array.from(audioSet.querySelectorAll("Representation"))
.sort((a, b) => {
const bwA = parseInt(a.getAttribute("bandwidth") || "0");
const bwB = parseInt(b.getAttribute("bandwidth") || "0");
return bwB - bwA;
});
if (representations.length === 0) throw new Error("No Representation found");
const rep = representations[0];
const repId = rep.getAttribute("id");
// Find SegmentTemplate
// Can be in Representation or AdaptationSet
const segmentTemplate = rep.querySelector("SegmentTemplate") || audioSet.querySelector("SegmentTemplate");
if (!segmentTemplate) throw new Error("No SegmentTemplate found");
const initialization = segmentTemplate.getAttribute("initialization");
const media = segmentTemplate.getAttribute("media");
const startNumber = parseInt(segmentTemplate.getAttribute("startNumber") || "1", 10);
// BaseURL
// Can be at MPD, Period, AdaptationSet, or Representation level.
// We strictly need to find the "deepest" one or combine them?
// Usually simpler manifests have it at one level.
// Let's resolve closest BaseURL.
const baseUrlTag = rep.querySelector("BaseURL") ||
audioSet.querySelector("BaseURL") ||
period.querySelector("BaseURL") ||
mpd.querySelector("BaseURL");
const baseUrl = baseUrlTag ? baseUrlTag.textContent.trim() : "";
// SegmentTimeline
const segmentTimeline = segmentTemplate.querySelector("SegmentTimeline");
const segments = [];
if (segmentTimeline) {
const sElements = segmentTimeline.querySelectorAll("S");
let currentTime = 0;
let currentNumber = startNumber;
sElements.forEach(s => {
// t is optional, defaults to previous end
const tAttr = s.getAttribute("t");
if (tAttr) currentTime = parseInt(tAttr, 10);
const d = parseInt(s.getAttribute("d"), 10);
const r = parseInt(s.getAttribute("r") || "0", 10);
// Initial segment
segments.push({ number: currentNumber, time: currentTime });
currentTime += d;
currentNumber++;
// Repeats
// r is the number of REPEATS (so total occurrences = 1 + r)
// If r is negative, it refers to open-ended? (Usually not in static manifests)
for (let i = 0; i < r; i++) {
segments.push({ number: currentNumber, time: currentTime });
currentTime += d;
currentNumber++;
}
});
}
return {
baseUrl,
initialization,
media,
segments,
repId
};
}
generateSegmentUrls(manifest) {
const { baseUrl, initialization, media, segments, repId } = manifest;
const urls = [];
// Helper to resolve template strings
const resolveTemplate = (template, number, time) => {
return template
.replace(/\$RepresentationID\$/g, repId)
.replace(/\$Number(?:%0([0-9]+)d)?\$/g, (match, width) => {
if (width) {
return number.toString().padStart(parseInt(width), '0');
}
return number;
})
.replace(/\$Time(?:%0([0-9]+)d)?\$/g, (match, width) => {
if (width) {
return time.toString().padStart(parseInt(width), '0');
}
return time;
});
};
// Helper to join paths handling slashes
const joinPath = (base, part) => {
if (!base) return part;
if (part.startsWith('http')) return part; // Absolute path
return base.endsWith('/') ? base + part : base + '/' + part;
};
// 1. Initialization Segment
if (initialization) {
const initPath = resolveTemplate(initialization, 0, 0); // Init often doesn't use Number/Time but just in case
urls.push(joinPath(baseUrl, initPath));
}
// 2. Media Segments
if (segments && segments.length > 0) {
segments.forEach(seg => {
const path = resolveTemplate(media, seg.number, seg.time);
urls.push(joinPath(baseUrl, path));
});
}
return urls;
}
}

View file

@ -11,6 +11,7 @@ import {
} from './utils.js';
import { lyricsSettings } from './storage.js';
import { addMetadataToAudio } from './metadata.js';
import { DashDownloader } from './dash-downloader.js';
const downloadTasks = new Map();
const bulkDownloadTasks = new Map();
@ -223,13 +224,28 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign
}
}
const response = await fetch(streamUrl, { signal });
if (!response.ok) {
throw new Error(`Failed to fetch track: ${response.status}`);
// Handle DASH streams (blob URLs)
if (streamUrl.startsWith('blob:')) {
try {
const downloader = new DashDownloader();
blob = await downloader.downloadDashStream(streamUrl, { signal });
} catch (dashError) {
console.error('DASH download failed:', dashError);
// Fallback
if (quality !== 'LOSSLESS') {
console.warn('Falling back to LOSSLESS (16-bit) download.');
return downloadTrackBlob(track, 'LOSSLESS', api, lyricsManager, signal);
}
throw dashError;
}
} else {
const response = await fetch(streamUrl, { signal });
if (!response.ok) {
throw new Error(`Failed to fetch track: ${response.status}`);
}
blob = await response.blob();
}
let blob = await response.blob();
// Add metadata to the blob
blob = await addMetadataToAudio(blob, enrichedTrack, api, quality);

View file

@ -1,4 +1,5 @@
//js/player.js
import { MediaPlayer } from 'dashjs';
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle, getTrackArtistsHTML } from './utils.js';
import { queueManager, replayGainSettings } from './storage.js';
@ -24,6 +25,17 @@ export class Player {
this.sleepTimerEndTime = null;
this.sleepTimerInterval = null;
// Initialize dash.js player
this.dashPlayer = MediaPlayer().create();
this.dashPlayer.updateSettings({
streaming: {
buffer: {
fastSwitchEnabled: true,
},
},
});
this.dashInitialized = false;
this.loadQueueState();
this.setupMediaSession();
@ -208,10 +220,13 @@ export class Player {
this.preloadCache.set(track.id, streamUrl);
// Warm connection/cache
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {});
// For Blob URLs (DASH), this head request is not needed and can cause errors.
if (!streamUrl.startsWith('blob:')) {
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {});
}
} catch (error) {
if (error.name !== 'AbortError') {
console.debug('Failed to get stream URL for preload:', trackTitle);
// console.debug('Failed to get stream URL for preload:', trackTitle);
}
}
}
@ -256,9 +271,16 @@ export class Player {
let streamUrl;
if (track.isLocal && track.file) {
this.dashPlayer.reset(); // Ensure dash is off
streamUrl = URL.createObjectURL(track.file);
this.currentRgValues = null; // No replaygain for local files yet
this.applyReplayGain();
this.audio.src = streamUrl;
if (startTime > 0) {
this.audio.currentTime = startTime;
}
await this.audio.play();
} else {
// Get track data for ReplayGain (should be cached by API)
const trackData = await this.api.getTrack(track.id, this.quality);
@ -282,13 +304,27 @@ export class Player {
} else {
streamUrl = this.api.extractStreamUrlFromManifest(trackData.info.manifest);
}
}
this.audio.src = streamUrl;
if (startTime > 0) {
this.audio.currentTime = startTime;
// Handle playback
if (streamUrl && streamUrl.startsWith('blob:') && !track.isLocal) {
// It's likely a DASH manifest blob URL
this.dashPlayer.initialize(this.audio, streamUrl, true);
this.dashInitialized = true;
if (startTime > 0) {
this.dashPlayer.seek(startTime);
}
} else {
if (this.dashInitialized) {
this.dashPlayer.reset();
this.dashInitialized = false;
}
this.audio.src = streamUrl;
if (startTime > 0) {
this.audio.currentTime = startTime;
}
await this.audio.play();
}
}
await this.audio.play();
// Update Media Session AFTER play starts to ensure metadata is captured
this.updateMediaSession(track);
@ -674,4 +710,4 @@ export class Player {
updateBtn(timerBtn);
updateBtn(timerBtnDesktop);
}
}
}

330
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dashjs": "^5.1.1",
"pocketbase": "^0.26.5"
},
"devDependencies": {
@ -73,6 +74,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -1602,6 +1604,7 @@
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@ -1643,6 +1646,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -1686,6 +1690,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -3021,6 +3026,130 @@
"string.prototype.matchall": "^4.0.6"
}
},
"node_modules/@svta/cml-608": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.1.tgz",
"integrity": "sha512-Y/Ier9VPUSOBnf0bJqdDyTlPrt4dDB+jk5mYHa1bnD2kcRl8qn7KkW3PRuj4w1aVN+BS2eHmsLxodt7P2hylUg==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
}
},
"node_modules/@svta/cml-cmcd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-1.0.1.tgz",
"integrity": "sha512-eox305g+QUJgXqOLVrbgxeQHCgl90ewwQ9O2bIoo7m+hanR8Xswu5CknFnT5qqIbLOHfw80ug+raycoAFHTQ+w==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-cta": "1.0.1",
"@svta/cml-structured-field-values": "1.0.1",
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-cmsd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.1.tgz",
"integrity": "sha512-+nIB8PuSfb/qw+xGaArPhNqPm84tBJUbe3H1DnPL5QUsjSUI7mUIUQwAtRV1ZdEu0+80g9i0op79woB0OIwr/g==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-cta": "1.0.1",
"@svta/cml-structured-field-values": "1.0.1",
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-cta": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.1.tgz",
"integrity": "sha512-jcXqNIPv26bmFxIOFh8/c3+6WLH4qBjKpq9qTQcggDPoHuV1YBydMsJLOnYPDeK8rNMKcAkFLbnDRvyJthu5yw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-structured-field-values": "1.0.1",
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-dash": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.1.tgz",
"integrity": "sha512-lYnD1I7FUbbQND+xICI+kcRaRXuT+whKk27R8m8me5VMVu2sMsAMc7Yui6l9sxw2cBKt8pSETPYRm/1+n4LZkw==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-id3": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.1.tgz",
"integrity": "sha512-90fGlL1qRI88CcaB89k6NG6cC3kky4Eu2jwqU4HefqK+S5k2OASUxf8JXkGz+DsdaiY7sh51vGPYdolfBZS7ug==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-request": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.1.tgz",
"integrity": "sha512-enL19BuXUjFkDDDF9jdNwUclMNPRsagnjGAetVC7xcmpDMpEx+ZLgsDip6BFNg5p6izSEk/OyujTWW1r8bDNiA==",
"license": "Apache-2.0",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-utils": "1.0.1",
"@svta/cml-xml": "1.0.1"
}
},
"node_modules/@svta/cml-structured-field-values": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.0.1.tgz",
"integrity": "sha512-Kibciki59Pon3Pn/sl5uyrbJcSpZQDKqdCfDrokBvOdLoqqcd0oFrkEPsZBiuuIODX1CB80612xe8hopeFDyBA==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@svta/cml-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.0.1.tgz",
"integrity": "sha512-kso3curTJfp00I1mKFoBliBApjn4aPE+wF8cPucf7TrSDVWZDeLLuF14ASmUE9m7rnrqTTK4878VvmXaXcCCfQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=20"
}
},
"node_modules/@svta/cml-xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@svta/cml-utils": "1.0.1"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -3062,6 +3191,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3085,6 +3215,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -3290,6 +3421,45 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bcp-47": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
"integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-alphanumerical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/bcp-47-match": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
"integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/bcp-47-normalize": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz",
"integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==",
"license": "MIT",
"dependencies": {
"bcp-47": "^2.0.0",
"bcp-47-match": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@ -3333,6 +3503,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -3492,6 +3663,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/codem-isoboxer": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz",
"integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==",
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3653,6 +3830,30 @@
"node": ">=4"
}
},
"node_modules/dashjs": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.1.tgz",
"integrity": "sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA==",
"license": "BSD-3-Clause",
"dependencies": {
"@svta/cml-608": "1.0.1",
"@svta/cml-cmcd": "1.0.1",
"@svta/cml-cmsd": "1.0.1",
"@svta/cml-dash": "1.0.1",
"@svta/cml-id3": "1.0.1",
"@svta/cml-request": "1.0.1",
"@svta/cml-xml": "1.0.1",
"bcp-47-match": "^2.0.3",
"bcp-47-normalize": "^2.3.0",
"codem-isoboxer": "0.3.10",
"fast-deep-equal": "3.1.3",
"html-entities": "^2.5.2",
"imsc": "^1.1.5",
"localforage": "^1.10.0",
"path-browserify": "^1.0.1",
"ua-parser-js": "^1.0.37"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@ -4087,6 +4288,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4310,7 +4512,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@ -4965,6 +5166,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@ -5111,6 +5328,12 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -5128,6 +5351,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/imsc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz",
"integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==",
"license": "BSD-2-Clause",
"dependencies": {
"sax": "1.2.1"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@ -5160,6 +5392,30 @@
"node": ">= 0.4"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -5302,6 +5558,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-decimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -5822,6 +6088,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -5829,6 +6104,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"license": "Apache-2.0",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -6235,6 +6519,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -6355,6 +6645,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -6438,6 +6729,7 @@
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -6859,6 +7151,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==",
"license": "ISC"
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -7381,6 +7679,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
@ -7795,6 +8094,7 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@ -7942,6 +8242,32 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
"integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"license": "MIT",
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@ -8093,6 +8419,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -8480,6 +8807,7 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},

View file

@ -43,6 +43,7 @@
"source-map": "^0.7.4"
},
"dependencies": {
"pocketbase": "^0.26.5"
"pocketbase": "^0.26.5",
"dashjs": "^5.1.1"
}
}