From 55df1e27f025ee22c7b4ec8097e222180daf1118 Mon Sep 17 00:00:00 2001 From: Sebastjan Date: Sun, 7 Jun 2026 15:36:07 +0200 Subject: [PATCH] bunny.ts: smart generateSignedUrl checks S3 migration status (folxvideos vs Bunny fallback) --- server/bunny.ts | 64 +++++++++++++++++++++++++++++++++++++++++++-- server/videoSync.ts | 3 +++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/server/bunny.ts b/server/bunny.ts index 9189acc..8182e79 100644 --- a/server/bunny.ts +++ b/server/bunny.ts @@ -1,4 +1,18 @@ import { type Video, type InsertVideo } from "@shared/schema"; +import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3"; + +const s3 = new S3Client({ + region: process.env.S3_REGION || "fsn1", + endpoint: process.env.S3_ENDPOINT || "https://fsn1.your-objectstorage.com", + forcePathStyle: true, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY_ID || "", + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "", + }, +}); +const S3_BUCKET = process.env.S3_BUCKET || "folxspeed"; +const S3_FOLX_PREFIX = "folx-tv/"; + interface BunnyVideo { guid: string; @@ -37,6 +51,45 @@ interface BunnyLibraryResponse { } export class BunnyService { + private migratedGuids: Set = new Set(); + private migratedGuidsRefreshedAt: number = 0; + + // Refresh list of GUIDs that exist in S3 (folx-tv/*/master.m3u8). Cached for 60s. + async refreshMigratedGuids(force: boolean = false): Promise { + const ageMs = Date.now() - this.migratedGuidsRefreshedAt; + if (!force && ageMs < 60_000 && this.migratedGuids.size > 0) return; + const newSet = new Set(); + let continuationToken: string | undefined = undefined; + try { + do { + const cmd: any = new ListObjectsV2Command({ + Bucket: S3_BUCKET, + Prefix: S3_FOLX_PREFIX, + MaxKeys: 1000, + ContinuationToken: continuationToken, + }); + const res: any = await s3.send(cmd); + for (const obj of res.Contents || []) { + const k = obj.Key || ""; + if (k.endsWith("/master.m3u8")) { + const guid = k.slice(S3_FOLX_PREFIX.length, -"/master.m3u8".length); + if (guid) newSet.add(guid); + } + } + continuationToken = res.IsTruncated ? res.NextContinuationToken : undefined; + } while (continuationToken); + this.migratedGuids = newSet; + this.migratedGuidsRefreshedAt = Date.now(); + console.log(`📦 Refreshed migrated GUIDs: ${newSet.size} videos available in S3`); + } catch (e) { + console.error("Failed to refresh migrated GUIDs:", e); + } + } + + isMigrated(videoId: string): boolean { + return this.migratedGuids.has(videoId); + } + private apiKey: string; private libraryId: string; private hostname: string; @@ -254,7 +307,14 @@ export class BunnyService { // Generate signed URL for private video access generateSignedUrl(videoId: string, expirationTime: number = 3600): string { - // Switched to folxvideos.b-cdn.net (S3-folxspeed origin) - public, no token required - return `https://folxvideos.b-cdn.net/folx-tv/${videoId}/master.m3u8`; + // If video is migrated to S3, use folxvideos.b-cdn.net (no token). + // Otherwise fall back to legacy Bunny Stream URL with token. + if (this.migratedGuids.has(videoId)) { + return `https://folxvideos.b-cdn.net/folx-tv/${videoId}/master.m3u8`; + } + const baseUrl = `https://${this.hostname}/${videoId}/playlist.m3u8`; + const expires = Math.floor(Date.now() / 1000) + expirationTime; + const token = Buffer.from(`${videoId}:${expires}:${this.apiKey.substring(0, 8)}`).toString("base64"); + return `${baseUrl}?token=${token}&expires=${expires}`; } } \ No newline at end of file diff --git a/server/videoSync.ts b/server/videoSync.ts index 17491f5..343489f 100644 --- a/server/videoSync.ts +++ b/server/videoSync.ts @@ -23,6 +23,9 @@ class VideoSyncService { } private async getAllVideos(): Promise { + // Refresh list of migrated GUIDs from S3 before fetching, so generateSignedUrl picks correct URL + try { await this.bunnyService.refreshMigratedGuids(); } catch {} + let allVideos: any[] = []; let page = 1; const itemsPerPage = 100; // Maximum per request