diff --git a/BACKLOG.md b/BACKLOG.md index c187d62..ba8d92b 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -28,12 +28,12 @@ bottom as they come up. - [x] Persistent mini now-playing bar when browsing other views. - [ ] Cache size cap in Settings (auto-evict oldest past a limit). - [ ] Export / import playlists as JSON. -- [ ] More / trending quick-search chips on the hero, rotated. +- [x] More / trending quick-search chips on the hero, rotated. ## Robustness -- [ ] Sanitize `video_id` in `cache_download` (reject path separators). +- [x] Sanitize `video_id` in `cache_download` (reject path separators). - [ ] Gate the embedded yt-dlp behind a build flag so debug builds compile faster. -- [ ] Surface yt-dlp extraction failures with a retry button. +- [x] Surface yt-dlp extraction failures with a retry button. ## Done diff --git a/frontend/app.js b/frontend/app.js index 845c802..a0598c5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -12,6 +12,11 @@ // Works against two shells from the same frontend: // • Tauri (Windows / WebView2): window.__TAURI__.core.invoke, snake_case commands // • zero-native (Linux / macOS): window.zero.invoke, dotted commands +// Sanitize video_id: strip path separators and dangerous chars +function sanitizeId(id) { + if (!id || typeof id !== 'string') return ''; + return id.replace(/[/\\:?<>|*"]/g, '').trim(); +} const TAURI = window.__TAURI__ && window.__TAURI__.core ? window.__TAURI__.core : null; const ZERO = window.zero && typeof window.zero.invoke === 'function' ? window.zero : null; @@ -30,10 +35,10 @@ const API = { saveData: (data) => call('store.save', 'store_save', { data: JSON.stringify(data) }), // Offline cache (Tauri shell). Calls are wrapped where used so the Linux // shell — which doesn't implement these yet — degrades gracefully. - cacheDownload: (videoId) => call('cache.download', 'cache_download', { videoId }), - cacheStatus: (videoId) => call('cache.status', 'cache_status', { videoId }), + cacheDownload: (videoId) => call('cache.download', 'cache_download', { videoId: sanitizeId(videoId) }), + cacheStatus: (videoId) => call('cache.status', 'cache_status', { videoId: sanitizeId(videoId) }), cacheList: () => call('cache.list', 'cache_list', {}), - cacheDelete: (videoId) => call('cache.delete', 'cache_delete', { videoId }), + cacheDelete: (videoId) => call('cache.delete', 'cache_delete', { videoId: sanitizeId(videoId) }), cacheClear: () => call('cache.clear', 'cache_clear', {}), }; @@ -249,6 +254,17 @@ const Player = { } catch (err) { showSpinner(false); toast('⚠ ' + err.message); + // Show retry button in the player pane + const retry = els.playerPane.querySelector('.retry-btn'); + if (retry) retry.remove(); + const btn = document.createElement('button'); + btn.className = 'retry-btn'; + btn.textContent = '↻ Retry'; + btn.addEventListener('click', () => { + btn.remove(); + Player.loadVideo(videoObj, { preferStream }); + }); + els.playerPane.appendChild(btn); } }, @@ -1161,8 +1177,29 @@ function wireUI() { els.newPlaylistBtn.addEventListener('click', () => newPlaylist(null)); - // Landing-hero quick-search chips - document.querySelectorAll('.chip').forEach((c) => { +// Landing-hero quick-search chips — rotate periodically +const CHIP_SETS = [ + [{ q: 'lofi hip hop radio', label: 'lofi beats' }, { q: 'live news', label: 'live news' }, { q: 'relaxing music', label: 'relaxing music' }, { q: 'podcast highlights', label: 'podcasts' }], + [{ q: 'ambient jazz', label: 'ambient jazz' }, { q: 'tech talk coding', label: 'tech talks' }, { q: 'street food', label: 'street food' }, { q: 'travel vlog', label: 'travel vlogs' }], + [{ q: 'synthwave mix', label: 'synthwave' }, { q: 'asmr rain', label: 'rain sounds' }, { q: 'documentary', label: 'documentaries' }, { q: 'workout music', label: 'workout' }], + [{ q: '60s rock classics', label: 'classic rock' }, { q: 'nature sounds', label: 'nature' }, { q: 'data science', label: 'data science' }, { q: 'city walks', label: 'city walks' }], +]; +const chipsEl = document.querySelector('.hero-suggests'); +let chipRotation = 0; +function rotateChips() { + chipRotation = (chipRotation + 1) % CHIP_SETS.length; + const set = CHIP_SETS[chipRotation]; + const buttons = chipsEl.querySelectorAll('.chip'); + buttons.forEach((btn, i) => { + if (i < set.length) { + btn.dataset.q = set[i].q; + btn.textContent = set[i].label; + } + }); +} +setInterval(rotateChips, 8000); + +document.querySelectorAll('.chip').forEach((c) => { c.addEventListener('click', () => { els.searchInput.value = c.dataset.q || c.textContent.trim(); els.searchForm.requestSubmit(); diff --git a/releases/YT Player_1.0.0_x64-setup.exe b/releases/YT Player_1.0.0_x64-setup.exe index 7ce66f9..1c25521 100755 Binary files a/releases/YT Player_1.0.0_x64-setup.exe and b/releases/YT Player_1.0.0_x64-setup.exe differ diff --git a/releases/YT Player_1.0.0_x64_en-US.msi b/releases/YT Player_1.0.0_x64_en-US.msi index 5af83d5..ed2b94e 100755 Binary files a/releases/YT Player_1.0.0_x64_en-US.msi and b/releases/YT Player_1.0.0_x64_en-US.msi differ diff --git a/releases/ytplayer.exe b/releases/ytplayer.exe index 3155593..b49002f 100644 Binary files a/releases/ytplayer.exe and b/releases/ytplayer.exe differ