mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 21:02:28 +00:00
fix(ci): fix netlify deployment by building to dist
- Move from root publishing to clean `dist/` directory - Create `scripts/build-dist.mjs` to orchestrate build and copy assets - Update `package.json` build script - Update `netlify.toml` to publish `dist/` - Ignore `dist/` in `.gitignore` Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
ccb60c3a7e
commit
0bfa3b08e8
638 files changed
+193490
-2
No files matched your search
+371
@@ -0,0 +1,371 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { normalizePointerInput } from "../nostr/watchHistory.js";
|
||||
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
|
||||
const DEFAULT_IDF_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_HISTORY_REFRESH_INTERVAL_MS = 60 * 1000;
|
||||
const DEFAULT_REFRESH_DEBOUNCE_MS = 200;
|
||||
|
||||
function normalizeAddressKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function collectVideoTags(video) {
|
||||
const tags = new Set();
|
||||
|
||||
if (!video || typeof video !== "object") {
|
||||
return tags;
|
||||
}
|
||||
|
||||
if (Array.isArray(video.tags)) {
|
||||
for (const tag of video.tags) {
|
||||
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
|
||||
const normalized = normalizeHashtag(tag[1]);
|
||||
if (normalized) {
|
||||
tags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(video.nip71?.hashtags)) {
|
||||
for (const tag of video.nip71.hashtags) {
|
||||
if (typeof tag !== "string") {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (normalized) {
|
||||
tags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function buildWatchHistoryTagCounts({
|
||||
watchHistoryService,
|
||||
nostrService,
|
||||
actor,
|
||||
} = {}) {
|
||||
if (!watchHistoryService || typeof watchHistoryService.loadLatest !== "function") {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
let items = [];
|
||||
try {
|
||||
items = await Promise.resolve(
|
||||
watchHistoryService.loadLatest(actor, { allowStale: true })
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[exploreData] Failed to load watch history entries:", error);
|
||||
items = [];
|
||||
}
|
||||
|
||||
const videosMap =
|
||||
nostrService && typeof nostrService.getVideosMap === "function"
|
||||
? nostrService.getVideosMap()
|
||||
: new Map();
|
||||
|
||||
const needsAddressIndex = Array.isArray(items)
|
||||
? items.some((item) => {
|
||||
const pointer = normalizePointerInput(item?.pointer || item);
|
||||
return pointer?.type === "a";
|
||||
})
|
||||
: false;
|
||||
|
||||
let addressIndex = null;
|
||||
if (needsAddressIndex) {
|
||||
addressIndex = new Map();
|
||||
for (const video of videosMap.values()) {
|
||||
const address = buildVideoAddressPointer(video);
|
||||
const key = normalizeAddressKey(address);
|
||||
if (key) {
|
||||
addressIndex.set(key, video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const counts = new Map();
|
||||
if (!Array.isArray(items)) {
|
||||
return counts;
|
||||
}
|
||||
|
||||
for (const entry of items) {
|
||||
const pointer = normalizePointerInput(entry?.pointer || entry);
|
||||
let video = entry?.video || entry?.metadata?.video || null;
|
||||
|
||||
if (pointer?.type === "e" && pointer.value) {
|
||||
const eventId = typeof pointer.value === "string" ? pointer.value.trim() : "";
|
||||
if (eventId) {
|
||||
video = videosMap.get(eventId) || videosMap.get(eventId.toLowerCase()) || video;
|
||||
}
|
||||
} else if (pointer?.type === "a" && pointer.value && addressIndex) {
|
||||
const key = normalizeAddressKey(pointer.value);
|
||||
if (key && addressIndex.has(key)) {
|
||||
video = addressIndex.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (!video) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tags = collectVideoTags(video);
|
||||
for (const tag of tags) {
|
||||
counts.set(tag, (counts.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function buildTagIdf({ videos } = {}) {
|
||||
const idf = new Map();
|
||||
const list = Array.isArray(videos) ? videos : [];
|
||||
if (!list.length) {
|
||||
return idf;
|
||||
}
|
||||
|
||||
const docFrequency = new Map();
|
||||
const totalDocs = list.length;
|
||||
|
||||
for (const video of list) {
|
||||
const tags = collectVideoTags(video);
|
||||
if (!tags.size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tag, df] of docFrequency.entries()) {
|
||||
const ratio = (totalDocs + 1) / (df + 1);
|
||||
const value = 1 + Math.log(ratio);
|
||||
if (Number.isFinite(value) && value > 0) {
|
||||
idf.set(tag, value);
|
||||
}
|
||||
}
|
||||
|
||||
return idf;
|
||||
}
|
||||
|
||||
export default class ExploreDataService {
|
||||
constructor({
|
||||
watchHistoryService,
|
||||
nostrService,
|
||||
getActiveActor,
|
||||
logger = devLogger,
|
||||
idfRefreshIntervalMs = DEFAULT_IDF_REFRESH_INTERVAL_MS,
|
||||
historyRefreshIntervalMs = DEFAULT_HISTORY_REFRESH_INTERVAL_MS,
|
||||
refreshDebounceMs = DEFAULT_REFRESH_DEBOUNCE_MS,
|
||||
} = {}) {
|
||||
this.watchHistoryService = watchHistoryService;
|
||||
this.nostrService = nostrService;
|
||||
this.getActiveActor =
|
||||
typeof getActiveActor === "function" ? getActiveActor : () => "";
|
||||
this.logger = logger && typeof logger.warn === "function" ? logger : devLogger;
|
||||
this.idfRefreshIntervalMs = idfRefreshIntervalMs;
|
||||
this.historyRefreshIntervalMs = historyRefreshIntervalMs;
|
||||
this.refreshDebounceMs = refreshDebounceMs;
|
||||
|
||||
this.watchHistoryTagCounts = new Map();
|
||||
this.watchHistoryTagCountsUpdatedAt = 0;
|
||||
this.tagIdf = new Map();
|
||||
this.tagIdfUpdatedAt = 0;
|
||||
|
||||
this.watchHistoryRefreshHandle = null;
|
||||
this.tagIdfRefreshHandle = null;
|
||||
this.watchHistoryInterval = null;
|
||||
this.tagIdfInterval = null;
|
||||
this.unsubscribeHandlers = [];
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.refreshWatchHistoryTagCounts({ force: true, reason: "init" });
|
||||
this.refreshTagIdf({ force: true, reason: "init" });
|
||||
this.subscribeToUpdates();
|
||||
this.startIntervals();
|
||||
}
|
||||
|
||||
startIntervals() {
|
||||
this.clearIntervals();
|
||||
if (Number.isFinite(this.historyRefreshIntervalMs) && this.historyRefreshIntervalMs > 0) {
|
||||
this.watchHistoryInterval = setInterval(() => {
|
||||
this.refreshWatchHistoryTagCounts({ reason: "interval" });
|
||||
}, this.historyRefreshIntervalMs);
|
||||
}
|
||||
if (Number.isFinite(this.idfRefreshIntervalMs) && this.idfRefreshIntervalMs > 0) {
|
||||
this.tagIdfInterval = setInterval(() => {
|
||||
this.refreshTagIdf({ reason: "interval" });
|
||||
}, this.idfRefreshIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
clearIntervals() {
|
||||
if (this.watchHistoryInterval) {
|
||||
clearInterval(this.watchHistoryInterval);
|
||||
this.watchHistoryInterval = null;
|
||||
}
|
||||
if (this.tagIdfInterval) {
|
||||
clearInterval(this.tagIdfInterval);
|
||||
this.tagIdfInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToUpdates() {
|
||||
this.unsubscribeHandlers.forEach((unsubscribe) => {
|
||||
if (typeof unsubscribe === "function") {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
this.unsubscribeHandlers = [];
|
||||
|
||||
if (this.watchHistoryService && typeof this.watchHistoryService.subscribe === "function") {
|
||||
const unsubscribe = this.watchHistoryService.subscribe("fingerprint", () => {
|
||||
this.queueWatchHistoryRefresh("watch-history");
|
||||
});
|
||||
this.unsubscribeHandlers.push(unsubscribe);
|
||||
}
|
||||
|
||||
if (this.nostrService && typeof this.nostrService.on === "function") {
|
||||
const events = [
|
||||
"videos:updated",
|
||||
"videos:fetched",
|
||||
"videos:published",
|
||||
"videos:edited",
|
||||
"videos:deleted",
|
||||
"videos:cache",
|
||||
];
|
||||
events.forEach((eventName) => {
|
||||
const unsubscribe = this.nostrService.on(eventName, () => {
|
||||
this.queueTagIdfRefresh(eventName);
|
||||
});
|
||||
this.unsubscribeHandlers.push(unsubscribe);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
queueWatchHistoryRefresh(reason) {
|
||||
if (this.watchHistoryRefreshHandle) {
|
||||
return;
|
||||
}
|
||||
this.watchHistoryRefreshHandle = setTimeout(() => {
|
||||
this.watchHistoryRefreshHandle = null;
|
||||
this.refreshWatchHistoryTagCounts({ reason });
|
||||
}, this.refreshDebounceMs);
|
||||
}
|
||||
|
||||
queueTagIdfRefresh(reason) {
|
||||
if (this.tagIdfRefreshHandle) {
|
||||
return;
|
||||
}
|
||||
this.tagIdfRefreshHandle = setTimeout(() => {
|
||||
this.tagIdfRefreshHandle = null;
|
||||
this.refreshTagIdf({ reason });
|
||||
}, this.refreshDebounceMs);
|
||||
}
|
||||
|
||||
async refreshWatchHistoryTagCounts({ force = false, reason } = {}) {
|
||||
const now = Date.now();
|
||||
if (!force && this.watchHistoryTagCountsUpdatedAt) {
|
||||
const elapsed = now - this.watchHistoryTagCountsUpdatedAt;
|
||||
if (elapsed < this.historyRefreshIntervalMs) {
|
||||
return this.watchHistoryTagCounts;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const actorRaw = this.getActiveActor();
|
||||
const actor = typeof actorRaw === "string" && actorRaw.trim() ? actorRaw.trim() : undefined;
|
||||
const counts = await buildWatchHistoryTagCounts({
|
||||
watchHistoryService: this.watchHistoryService,
|
||||
nostrService: this.nostrService,
|
||||
actor,
|
||||
});
|
||||
this.watchHistoryTagCounts = counts instanceof Map ? counts : new Map();
|
||||
this.watchHistoryTagCountsUpdatedAt = now;
|
||||
return this.watchHistoryTagCounts;
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn(
|
||||
"[exploreData] Failed to refresh watch history tag counts:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (reason) {
|
||||
return this.watchHistoryTagCounts;
|
||||
}
|
||||
return this.watchHistoryTagCounts;
|
||||
}
|
||||
}
|
||||
|
||||
refreshTagIdf({ force = false, videos, reason } = {}) {
|
||||
const now = Date.now();
|
||||
if (!force && this.tagIdfUpdatedAt) {
|
||||
const elapsed = now - this.tagIdfUpdatedAt;
|
||||
if (elapsed < this.idfRefreshIntervalMs) {
|
||||
return this.tagIdf;
|
||||
}
|
||||
}
|
||||
|
||||
const sourceVideos = Array.isArray(videos)
|
||||
? videos
|
||||
: this.nostrService && typeof this.nostrService.getFilteredActiveVideos === "function"
|
||||
? this.nostrService.getFilteredActiveVideos()
|
||||
: [];
|
||||
|
||||
try {
|
||||
this.tagIdf = buildTagIdf({ videos: sourceVideos });
|
||||
this.tagIdfUpdatedAt = now;
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn("[exploreData] Failed to refresh tag IDF:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (reason) {
|
||||
return this.tagIdf;
|
||||
}
|
||||
|
||||
return this.tagIdf;
|
||||
}
|
||||
|
||||
getWatchHistoryTagCounts() {
|
||||
return this.watchHistoryTagCounts;
|
||||
}
|
||||
|
||||
getTagIdf() {
|
||||
return this.tagIdf;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clearIntervals();
|
||||
|
||||
if (this.watchHistoryRefreshHandle) {
|
||||
clearTimeout(this.watchHistoryRefreshHandle);
|
||||
this.watchHistoryRefreshHandle = null;
|
||||
}
|
||||
if (this.tagIdfRefreshHandle) {
|
||||
clearTimeout(this.tagIdfRefreshHandle);
|
||||
this.tagIdfRefreshHandle = null;
|
||||
}
|
||||
|
||||
this.unsubscribeHandlers.forEach((unsubscribe) => {
|
||||
if (typeof unsubscribe === "function") {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
this.unsubscribeHandlers = [];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user