bunny.ts: smart generateSignedUrl checks S3 migration status (folxvideos vs Bunny fallback)

This commit is contained in:
Sebastjan 2026-06-07 15:36:07 +02:00
parent 26de8896c5
commit 55df1e27f0
2 changed files with 65 additions and 2 deletions

View File

@ -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<string> = 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<void> {
const ageMs = Date.now() - this.migratedGuidsRefreshedAt;
if (!force && ageMs < 60_000 && this.migratedGuids.size > 0) return;
const newSet = new Set<string>();
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
// 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}`;
}
}

View File

@ -23,6 +23,9 @@ class VideoSyncService {
}
private async getAllVideos(): Promise<any[]> {
// 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