diff --git a/index.html b/index.html index a7545a4..785778d 100644 --- a/index.html +++ b/index.html @@ -1106,6 +1106,7 @@ Quality for streaming playback + Hi-Res FLAC (24-bit) FLAC (Lossless) AAC 320kbps AAC 96kbps @@ -1117,6 +1118,7 @@ Quality for track downloads + Hi-Res FLAC (24-bit) FLAC (Lossless) AAC 320kbps AAC 96kbps diff --git a/js/api.js b/js/api.js index 6510b08..a35addf 100644 --- a/js/api.js +++ b/js/api.js @@ -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(' 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; + } +} diff --git a/js/downloads.js b/js/downloads.js index b7f2528..0b69b0b 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -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); diff --git a/js/player.js b/js/player.js index 69a9dd3..5002177 100644 --- a/js/player.js +++ b/js/player.js @@ -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); } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3a224e6..1872c8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index 5653eab..c777a7f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "source-map": "^0.7.4" }, "dependencies": { - "pocketbase": "^0.26.5" + "pocketbase": "^0.26.5", + "dashjs": "^5.1.1" } }