- Move native shells (Zig/src, Tauri/src-tauri, app.zon, releases) to legacy/
- Add Bun + Hono server with yt-dlp proxy endpoints (search, channel, streams,
download), libsql (concurrent SQLite fork) for fingerprint-keyed playlist/
history sync, and static file serving for the frontend
- Add Dockerfile + docker-compose.yml (single container, volume-mounted DB)
- Add frontend/sw.js: app-shell cache-first, /api/* network-only,
thumbnails stale-while-revalidate, SW_UPDATE_AVAILABLE broadcast,
SKIP_WAITING message handler for seamless auto-update
- Add frontend/manifest.webmanifest: standalone PWA, vermilion theme,
search/history shortcuts
- Add frontend/icons/icon-{192,512}.png: generated PWA icons
- Add frontend/fingerprint.js: canvas+UA djb2 fingerprint, localStorage-cached,
exposes window.getFingerprint() for server-side playlist keying
- Add frontend/opfs.js: full OPFS video store (writeFromResponse streams
directly without full-file buffering), exposes window.OPFS
- Add scripts/make-pwa-icons.js: regenerate icons without external deps
- Patch frontend/app.js: WEB mode detection, webFetch + opfs* bridge wrappers,
API object routes to WEB helpers when no native bridge present,
Player.loadVideo handles OPFS blob URLs + revokes them on next load,
SW registration + update banner in boot()
- Patch frontend/index.html: manifest link, theme-color, Apple PWA meta,
CSP blob:/worker-src, fingerprint.js + opfs.js script tags
- Patch frontend/styles.css: .toast-update + .toast-reload-btn for update banner
- Native Tauri/Zig builds unchanged — all new code is additive via WEB flag
82 lines
2.8 KiB
JavaScript
82 lines
2.8 KiB
JavaScript
/* ============================================================================
|
|
* fingerprint.js — stable browser identifier for server-side playlist sync
|
|
*
|
|
* Combines canvas rendering, UA, screen geometry, and hardware hints into a
|
|
* short hex string. The result is stored in localStorage so the same ID
|
|
* survives page reloads, and only regenerated when localStorage is cleared.
|
|
*
|
|
* Not for tracking — used exclusively so the server can associate playlist
|
|
* and history rows with this browser without requiring a login.
|
|
* ========================================================================== */
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// djb2 hash over a string → 32-bit unsigned int
|
|
function djb2(str) {
|
|
let h = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
h = (((h << 5) + h) + str.charCodeAt(i)) >>> 0;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
// Render a small canvas to capture GPU/font rasterisation differences,
|
|
// then hash the pixel data. Falls back to empty string if canvas is blocked.
|
|
function canvasHash() {
|
|
try {
|
|
const c = document.createElement('canvas');
|
|
c.width = 200; c.height = 40;
|
|
const ctx = c.getContext('2d');
|
|
if (!ctx) return '';
|
|
ctx.textBaseline = 'top';
|
|
ctx.font = '14px Arial';
|
|
ctx.fillStyle = '#f60';
|
|
ctx.fillRect(125, 1, 62, 20);
|
|
ctx.fillStyle = '#069';
|
|
ctx.fillText('YTPlayer🎵', 2, 15);
|
|
ctx.fillStyle = 'rgba(102,204,0,0.7)';
|
|
ctx.fillText('YTPlayer🎵', 4, 17);
|
|
return djb2(c.toDataURL()).toString(16);
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function generateFingerprint() {
|
|
const parts = [
|
|
navigator.userAgent || '',
|
|
String(screen.width) + 'x' + String(screen.height),
|
|
String(screen.colorDepth),
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
|
navigator.language || '',
|
|
String(navigator.hardwareConcurrency || 0),
|
|
String(navigator.deviceMemory || 0),
|
|
canvasHash(),
|
|
];
|
|
// Combine all component hashes into one 16-char hex fingerprint
|
|
const combined = parts.reduce((acc, p) => acc + '|' + p, '');
|
|
const h1 = djb2(combined);
|
|
const h2 = djb2(combined.split('').reverse().join(''));
|
|
return h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0');
|
|
}
|
|
|
|
window.getFingerprint = function getFingerprint() {
|
|
try {
|
|
let fp = localStorage.getItem('_ytpfp');
|
|
if (!fp || fp.length < 8) {
|
|
fp = generateFingerprint();
|
|
localStorage.setItem('_ytpfp', fp);
|
|
}
|
|
return fp;
|
|
} catch {
|
|
// localStorage blocked (e.g. private mode on some browsers) — generate
|
|
// ephemeral fingerprint that survives the page session via a closure.
|
|
if (!window._ytpfpEphemeral) {
|
|
window._ytpfpEphemeral = generateFingerprint();
|
|
}
|
|
return window._ytpfpEphemeral;
|
|
}
|
|
};
|
|
}());
|