mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-03-11 21:40:48 +00:00
293 lines
7.0 KiB
JavaScript
293 lines
7.0 KiB
JavaScript
// js/videoEventUtils.js
|
|
|
|
/**
|
|
* Extracts normalized fields from a Bitvid video event while
|
|
* tolerating legacy payloads that may omit version >= 2 metadata.
|
|
*/
|
|
const MAGNET_URI_PATTERN = /^magnet:\?/i;
|
|
const HEX_INFO_HASH_PATTERN = /\b[0-9a-f]{40}\b/gi;
|
|
const LEGACY_MAGNET_PATTERN = /magnet:\?xt=urn:btih:[0-9a-z]{40,}[^\s"'<>]*/i;
|
|
|
|
function safeTrim(value) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function pickFirstString(values = []) {
|
|
if (!Array.isArray(values)) {
|
|
return "";
|
|
}
|
|
|
|
for (const value of values) {
|
|
const trimmed = safeTrim(value);
|
|
if (trimmed) {
|
|
return trimmed;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
export function deriveTitleFromEvent({
|
|
parsedContent = {},
|
|
tags = [],
|
|
primaryTitle = "",
|
|
} = {}) {
|
|
const initial = safeTrim(primaryTitle);
|
|
if (initial) {
|
|
return initial;
|
|
}
|
|
|
|
const directCandidates = pickFirstString([
|
|
parsedContent?.title,
|
|
parsedContent?.name,
|
|
parsedContent?.filename,
|
|
parsedContent?.fileName,
|
|
parsedContent?.subject,
|
|
parsedContent?.caption,
|
|
]);
|
|
if (directCandidates) {
|
|
return directCandidates;
|
|
}
|
|
|
|
const metaTitle = safeTrim(parsedContent?.meta?.title);
|
|
if (metaTitle) {
|
|
return metaTitle;
|
|
}
|
|
|
|
if (Array.isArray(tags)) {
|
|
for (const tag of tags) {
|
|
if (!Array.isArray(tag) || tag.length < 2) {
|
|
continue;
|
|
}
|
|
const key = safeTrim(tag[0]).toLowerCase();
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
if (["title", "subject", "caption", "name"].includes(key)) {
|
|
const candidate = safeTrim(tag[1]);
|
|
if (candidate) {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function extractInfoHashesFromString(value, pushInfoHash) {
|
|
if (typeof value !== "string") {
|
|
return;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
|
|
const matches = trimmed.match(HEX_INFO_HASH_PATTERN);
|
|
if (!matches) {
|
|
return;
|
|
}
|
|
|
|
for (const match of matches) {
|
|
const normalized = match.toLowerCase();
|
|
pushInfoHash(normalized);
|
|
}
|
|
}
|
|
|
|
function traverseForInfoHashes(value, pushInfoHash) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
if (typeof value === "string") {
|
|
extractInfoHashesFromString(value, pushInfoHash);
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
traverseForInfoHashes(item, pushInfoHash);
|
|
}
|
|
return;
|
|
}
|
|
if (typeof value === "object") {
|
|
for (const nested of Object.values(value)) {
|
|
traverseForInfoHashes(nested, pushInfoHash);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parseVideoEventPayload(event = {}) {
|
|
const rawContent = typeof event.content === "string" ? event.content : "";
|
|
|
|
let parsedContent = {};
|
|
let parseError = null;
|
|
if (rawContent) {
|
|
try {
|
|
const parsed = JSON.parse(rawContent);
|
|
if (parsed && typeof parsed === "object") {
|
|
parsedContent = parsed;
|
|
}
|
|
} catch (err) {
|
|
parseError = err;
|
|
parsedContent = {};
|
|
}
|
|
}
|
|
|
|
const title = typeof parsedContent.title === "string"
|
|
? parsedContent.title.trim()
|
|
: "";
|
|
const thumbnail = typeof parsedContent.thumbnail === "string"
|
|
? parsedContent.thumbnail.trim()
|
|
: "";
|
|
|
|
const magnetCandidates = [];
|
|
const infoHashCandidates = [];
|
|
const urlCandidates = [];
|
|
|
|
const pushUnique = (arr, value) => {
|
|
if (!value || typeof value !== "string") return;
|
|
const trimmed = value.trim();
|
|
if (!trimmed || arr.includes(trimmed)) return;
|
|
arr.push(trimmed);
|
|
};
|
|
|
|
const pushInfoHash = (candidate) => {
|
|
if (!candidate) return;
|
|
const normalized = candidate.toLowerCase();
|
|
if (!/^[0-9a-f]{40}$/.test(normalized)) {
|
|
return;
|
|
}
|
|
if (infoHashCandidates.includes(normalized)) {
|
|
return;
|
|
}
|
|
infoHashCandidates.push(normalized);
|
|
};
|
|
|
|
const collectMagnetOrInfoHash = (value) => {
|
|
if (typeof value !== "string") {
|
|
return;
|
|
}
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
|
|
if (MAGNET_URI_PATTERN.test(trimmed)) {
|
|
pushUnique(magnetCandidates, trimmed);
|
|
}
|
|
|
|
extractInfoHashesFromString(trimmed, pushInfoHash);
|
|
|
|
const urnMatch = trimmed.match(/^urn:btih:([0-9a-z]+)$/i);
|
|
if (urnMatch) {
|
|
pushInfoHash(urnMatch[1]);
|
|
}
|
|
};
|
|
|
|
if (typeof parsedContent.magnet === "string") {
|
|
collectMagnetOrInfoHash(parsedContent.magnet);
|
|
}
|
|
if (typeof parsedContent.url === "string") {
|
|
const parsedUrl = parsedContent.url.trim();
|
|
if (parsedUrl && parsedUrl !== thumbnail) {
|
|
// Keep this guard so we don't duplicate thumbnail URLs in the playable URL list.
|
|
pushUnique(urlCandidates, parsedUrl);
|
|
}
|
|
// Legacy Bitvid events sometimes embedded magnets/info-hashes in the URL field, so treat it accordingly.
|
|
collectMagnetOrInfoHash(parsedUrl);
|
|
}
|
|
|
|
const tags = Array.isArray(event.tags) ? event.tags : [];
|
|
const urlTagKeys = new Set(["r", "url", "u"]);
|
|
for (const tag of tags) {
|
|
if (!Array.isArray(tag) || tag.length < 2) continue;
|
|
const key = typeof tag[0] === "string" ? tag[0] : "";
|
|
const value = typeof tag[1] === "string" ? tag[1] : "";
|
|
if (!value) continue;
|
|
|
|
if (value.toLowerCase().startsWith("magnet:")) {
|
|
collectMagnetOrInfoHash(value);
|
|
continue;
|
|
}
|
|
|
|
if (urlTagKeys.has(key) && /^https?:\/\//i.test(value)) {
|
|
pushUnique(urlCandidates, value);
|
|
}
|
|
|
|
collectMagnetOrInfoHash(value);
|
|
}
|
|
|
|
const magnetMatch = rawContent.match(/magnet:\?xt=urn:[^"'\s<>]+/i);
|
|
if (magnetMatch) {
|
|
collectMagnetOrInfoHash(magnetMatch[0]);
|
|
}
|
|
|
|
extractInfoHashesFromString(rawContent, pushInfoHash);
|
|
traverseForInfoHashes(parsedContent, pushInfoHash);
|
|
|
|
const magnet = magnetCandidates.find(Boolean) || "";
|
|
const infoHash = infoHashCandidates.find(Boolean) || "";
|
|
const url =
|
|
urlCandidates.find(
|
|
(candidate) => candidate && !candidate.toLowerCase().startsWith("magnet:")
|
|
) || "";
|
|
|
|
const rawVersion = parsedContent.version;
|
|
let version = 0;
|
|
if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) {
|
|
version = rawVersion;
|
|
} else if (typeof rawVersion === "string") {
|
|
const parsedVersion = Number(rawVersion);
|
|
if (!Number.isNaN(parsedVersion)) {
|
|
version = parsedVersion;
|
|
}
|
|
}
|
|
|
|
return {
|
|
parsedContent,
|
|
parseError,
|
|
title,
|
|
url,
|
|
magnet,
|
|
infoHash,
|
|
version,
|
|
};
|
|
}
|
|
|
|
export function magnetFromText(value) {
|
|
if (typeof value !== "string") {
|
|
return "";
|
|
}
|
|
const match = value.match(LEGACY_MAGNET_PATTERN);
|
|
return match ? match[0] : "";
|
|
}
|
|
|
|
export function findLegacyMagnetInEvent(event = {}) {
|
|
const fromContent = magnetFromText(event?.content);
|
|
if (fromContent) {
|
|
return fromContent.trim();
|
|
}
|
|
|
|
const tags = Array.isArray(event?.tags) ? event.tags : [];
|
|
for (const tag of tags) {
|
|
if (!Array.isArray(tag) || tag.length < 2) {
|
|
continue;
|
|
}
|
|
const key = typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : "";
|
|
const value = typeof tag[1] === "string" ? tag[1].trim() : "";
|
|
if (!value) {
|
|
continue;
|
|
}
|
|
if (key === "magnet" && value) {
|
|
return value;
|
|
}
|
|
const fromTag = magnetFromText(value);
|
|
if (fromTag) {
|
|
return fromTag.trim();
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|