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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-19 11:04:16 -04:00
1 parent c62114557c
commit 76ba750708
3 files changed
+103 -80

No files matched your search

+98
View File
@@ -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;
};
+4 -79
View File
@@ -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)
+1 -1
View File
@@ -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,