From 76ba7507084d68fcc3496fb4339c7afa93f2a284 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:04:16 -0400 Subject: [PATCH] refactor(playback): extract playback helpers to shrink playbackService (1/10) File-size decomposition (no behavior change). Move the self-contained SimpleEventEmitter and extractWebSeedsFromMagnet (+ its WEBSEED_PARAM_KEYS and the now-sole safeDecodeURIComponent consumer) out of playbackService.js into a new js/services/playbackHelpers.js and import them back. playbackService.js: 1350 -> 1274 lines (now under its original 1289 baseline). Re-recorded the smaller baseline in check-file-size.mjs to lock the reduction. Build + full unit suite green. Co-Authored-By: Claude Opus 4.8 --- js/services/playbackHelpers.js | 98 ++++++++++++++++++++++++++++++++++ js/services/playbackService.js | 83 ++-------------------------- scripts/check-file-size.mjs | 2 +- 3 files changed, 103 insertions(+), 80 deletions(-) create mode 100644 js/services/playbackHelpers.js diff --git a/js/services/playbackHelpers.js b/js/services/playbackHelpers.js new file mode 100644 index 00000000..161e4e09 --- /dev/null +++ b/js/services/playbackHelpers.js @@ -0,0 +1,98 @@ +// js/services/playbackHelpers.js +// +// Small, self-contained helpers extracted from playbackService.js to keep that +// orchestrator under the file-size budget. Pure logic with no dependency on the +// playback session state. + +import { safeDecodeURIComponent } from "../utils/safeDecode.js"; + +/** + * Minimal synchronous event emitter used by the playback session to surface + * status/fallback/sourcechange events to the coordinator. Errors thrown by a + * listener are swallowed (and optionally logged) so one bad listener can't + * abort the rest of the playback pipeline. + */ +export class SimpleEventEmitter { + constructor(logger = null) { + this.logger = typeof logger === "function" ? logger : null; + this.listeners = new Map(); + } + + on(eventName, handler) { + if (typeof handler !== "function") { + return () => {}; + } + if (!this.listeners.has(eventName)) { + this.listeners.set(eventName, new Set()); + } + const handlers = this.listeners.get(eventName); + handlers.add(handler); + return () => { + handlers.delete(handler); + if (!handlers.size) { + this.listeners.delete(eventName); + } + }; + } + + emit(eventName, detail) { + const handlers = this.listeners.get(eventName); + if (!handlers || !handlers.size) { + return; + } + for (const handler of Array.from(handlers)) { + try { + handler(detail); + } catch (err) { + if (this.logger) { + this.logger("[PlaybackService] Listener error", err); + } + } + } + } +} + +const WEBSEED_PARAM_KEYS = new Set(["ws", "webseed"]); + +/** + * Extract every web seed URL (`ws=`/`webseed=`) from a magnet's query string, + * in order, decoded. Used to seed WebTorrent's urlList from the magnet. + * + * @param {string} magnetUri + * @returns {string[]} + */ +export const extractWebSeedsFromMagnet = (magnetUri) => { + if (typeof magnetUri !== "string") { + return []; + } + const trimmed = magnetUri.trim(); + if (!trimmed) { + return []; + } + const [withoutFragment] = trimmed.split("#", 1); + const [, query = ""] = withoutFragment.split("?", 2); + if (!query) { + return []; + } + + const webSeeds = []; + for (const segment of query.split("&")) { + if (!segment) { + continue; + } + const [rawKey, rawValue = ""] = segment.split("=", 2); + if (!rawKey) { + continue; + } + const key = rawKey.trim().toLowerCase(); + if (!WEBSEED_PARAM_KEYS.has(key)) { + continue; + } + const decodedValue = safeDecodeURIComponent(rawValue.replace(/\+/g, "%20")); + const candidate = decodedValue.trim(); + if (candidate) { + webSeeds.push(candidate); + } + } + return webSeeds; +}; diff --git a/js/services/playbackService.js b/js/services/playbackService.js index 1828b788..d94449ab 100644 --- a/js/services/playbackService.js +++ b/js/services/playbackService.js @@ -1,7 +1,10 @@ import { userLogger } from "../utils/logger.js"; -import { safeDecodeURIComponent } from "../utils/safeDecode.js"; import { PLAYBACK_START_TIMEOUT } from "../constants.js"; import { getCurrentVideo, setCurrentVideo } from "../state/appState.js"; +import { + SimpleEventEmitter, + extractWebSeedsFromMagnet, +} from "./playbackHelpers.js"; // js/services/playbackService.js /** @@ -21,46 +24,6 @@ import { getCurrentVideo, setCurrentVideo } from "../state/appState.js"; * old session is cancelled and doesn't overwrite the new one. */ -class SimpleEventEmitter { - constructor(logger = null) { - this.logger = typeof logger === "function" ? logger : null; - this.listeners = new Map(); - } - - on(eventName, handler) { - if (typeof handler !== "function") { - return () => {}; - } - if (!this.listeners.has(eventName)) { - this.listeners.set(eventName, new Set()); - } - const handlers = this.listeners.get(eventName); - handlers.add(handler); - return () => { - handlers.delete(handler); - if (!handlers.size) { - this.listeners.delete(eventName); - } - }; - } - - emit(eventName, detail) { - const handlers = this.listeners.get(eventName); - if (!handlers || !handlers.size) { - return; - } - for (const handler of Array.from(handlers)) { - try { - handler(detail); - } catch (err) { - if (this.logger) { - this.logger("[PlaybackService] Listener error", err); - } - } - } - } -} - const HOSTED_URL_SUCCESS_MESSAGE = "✅ Streaming from hosted URL"; const DEFAULT_UNSUPPORTED_BTITH_MESSAGE = "This magnet link is missing a compatible BitTorrent v1 info hash."; @@ -68,44 +31,6 @@ const AUTH_STATUS_CODES = new Set([401, 403]); const SSL_ERROR_PATTERN = /(ssl|cert|certificate)/i; const CORS_ERROR_PATTERN = /cors/i; const PROBE_CACHE_TTL_MS = 45000; -const WEBSEED_PARAM_KEYS = new Set(["ws", "webseed"]); - -const extractWebSeedsFromMagnet = (magnetUri) => { - if (typeof magnetUri !== "string") { - return []; - } - const trimmed = magnetUri.trim(); - if (!trimmed) { - return []; - } - const [withoutFragment] = trimmed.split("#", 1); - const [, query = ""] = withoutFragment.split("?", 2); - if (!query) { - return []; - } - - const webSeeds = []; - for (const segment of query.split("&")) { - if (!segment) { - continue; - } - const [rawKey, rawValue = ""] = segment.split("=", 2); - if (!rawKey) { - continue; - } - const key = rawKey.trim().toLowerCase(); - if (!WEBSEED_PARAM_KEYS.has(key)) { - continue; - } - const decodedValue = safeDecodeURIComponent(rawValue.replace(/\+/g, "%20")); - const candidate = decodedValue.trim(); - if (candidate) { - webSeeds.push(candidate); - } - } - return webSeeds; -}; - const getHostedUrlFailureDetails = (probeResult = {}) => { const outcome = probeResult?.outcome || "error"; const status = Number.isFinite(probeResult?.status) diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs index 51ecc7ae..267ffaf0 100644 --- a/scripts/check-file-size.mjs +++ b/scripts/check-file-size.mjs @@ -58,7 +58,7 @@ const GRANDFATHERED = { "js/ui/views/VideoListView.js": 1359, "js/ui/components/UploadModal.js": 1319, "js/ui/components/SimilarContentCard.js": 1303, - "js/services/playbackService.js": 1289, + "js/services/playbackService.js": 1274, "js/services/authService.js": 1335, "js/ui/zapController.js": 1206, "js/services/r2Service.js": 1194,