diff --git a/services/pocketbase/README.md b/services/pocketbase/README.md
index 248a83a5..1967f684 100644
--- a/services/pocketbase/README.md
+++ b/services/pocketbase/README.md
@@ -92,3 +92,15 @@ https://pocketbuilds.com/
how the pros do it
https://github.com/benallfree/pocketpages/blob/5bc48d4f8df75b2f78ca61fa18c792d814b926e8/packages/starters/deploy-pockethost-manual/deploy-pockethost.ts#L5
+
+## Avoid proxying files via pocketbase
+
+https://github.com/pocketbase/pocketbase/discussions/5995
+
+## realtime updates on db changes
+
+https://github.com/pocketbase/pocketbase/discussions/4427#discussioncomment-8585118
+
+## get a random item
+
+https://github.com/pocketbase/pocketbase/discussions/2725
\ No newline at end of file
diff --git a/services/pocketbase/package-lock.json b/services/pocketbase/package-lock.json
index fbe56f47..cd8f5b6c 100644
--- a/services/pocketbase/package-lock.json
+++ b/services/pocketbase/package-lock.json
@@ -1,15 +1,17 @@
{
"name": "futureporn",
- "version": "0.0.3",
+ "version": "3.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "futureporn",
- "version": "0.0.3",
+ "version": "3.0.3",
"license": "Unlicense",
"dependencies": {
"jsonwebtoken": "^9.0.2",
+ "mime": "^4.1.0",
+ "nano-spawn": "^2.0.0",
"pg": "^8.16.3",
"pocketpages": ">=0.22.3",
"pocketpages-plugin-auth": "^0.2.2",
@@ -1771,6 +1773,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mime": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
+ "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
+ "funding": [
+ "https://github.com/sponsors/broofa"
+ ],
+ "license": "MIT",
+ "bin": {
+ "mime": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
@@ -1870,6 +1887,18 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/nano-spawn": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
+ "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
+ }
+ },
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
diff --git a/services/pocketbase/package.json b/services/pocketbase/package.json
index 4a467d06..45d80348 100644
--- a/services/pocketbase/package.json
+++ b/services/pocketbase/package.json
@@ -1,6 +1,6 @@
{
"name": "futureporn",
- "version": "3.0.3",
+ "version": "3.1.0",
"private": true,
"description": "Dedication to the preservation of lewdtuber history",
"license": "Unlicense",
@@ -9,6 +9,8 @@
},
"dependencies": {
"jsonwebtoken": "^9.0.2",
+ "mime": "^4.1.0",
+ "nano-spawn": "^2.0.0",
"pg": "^8.16.3",
"pocketpages": ">=0.22.3",
"pocketpages-plugin-auth": "^0.2.2",
diff --git a/services/pocketbase/pb_hooks/cdn.pb.js b/services/pocketbase/pb_hooks/cdn.pb.js
new file mode 100644
index 00000000..3b3e0f3d
--- /dev/null
+++ b/services/pocketbase/pb_hooks/cdn.pb.js
@@ -0,0 +1,107 @@
+///
+
+
+
+/**
+ * onFileDownloadRequest hook is triggered before each API File download request. Could be used to validate or modify the file response before returning it to the client.
+ * @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
+ *
+ * We use this to return a 302 to the CDN asset instead of having the asset proxied via Pocketbase
+ * @see https://github.com/pocketbase/pocketbase/discussions/5995
+ *
+ */
+onFileDownloadRequest((e) => {
+
+
+
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // console.log('onFileDownloadRequest hook has been triggered ~~~');
+ // e.next()
+
+ // e.app
+ // e.collection
+ // e.record
+ // e.fileField
+ // e.servedPath
+ // e.servedName
+ // and all RequestEvent fields...
+ const securityKey = process.env?.BUNNY_TOKEN_KEY;
+ const baseUrl = process.env?.BUNNY_ZONE_URL;
+
+ console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
+
+ if (!securityKey) {
+ console.error('BUNNY_TOKEN_KEY was missing from env');
+ return e.next();
+ }
+ if (!baseUrl) {
+ console.error('BUNNY_ZONE_URL was missing from env');
+ return e.next();
+ }
+
+
+
+
+ /**
+ * Generates a BunnyCDN-style signed URL using directory tokens.
+ *
+ * We sign URLs to make hotlinking difficult
+ * @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
+ * @see https://github.com/pocketbase/pocketbase/discussions/5983#discussioncomment-11426659 // HMAC in pocketbase
+ * @see https://github.com/pocketbase/pocketbase/discussions/6772 // base64 encode the hex
+ */
+ function signUrl(securityKey, baseUrl, path, expires) {
+
+ if (!path.startsWith('/')) path = '/' + path;
+ if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
+
+
+ const hashableBase = securityKey + path + expires;
+
+ // Generate and encode the token
+ const tokenH = $security.sha256(hashableBase);
+
+ const token = Buffer.from(tokenH, "hex")
+ .toString("base64")
+ .replace(/\n/g, "") // Remove newlines
+ .replace(/\+/g, "-") // Replace + with -
+ .replace(/\//g, "_") // Replace / with _
+ .replace(/=/g, ""); // Remove =
+
+
+ // Generate the URL
+ const signedUrl = baseUrl + path + '?token=' + token + '&expires=' + expires;
+
+ return signedUrl;
+ }
+
+ console.log(`record: ${JSON.stringify(e.record)}`)
+ console.log(`collection: ${JSON.stringify(e.collection)}`)
+ console.log(`app: ${JSON.stringify(e.app)}`)
+ console.log(`fileField: ${JSON.stringify(e.fileField)}`)
+ console.log(`servedPath: ${JSON.stringify(e.servedPath)}`)
+ console.log(`servedName: ${JSON.stringify(e.servedName)}`)
+
+ // Our job here is to take the servedPath, and sign it using bunnycdn method
+ // Then serve a 302 redirect instead of serving the file proxied thru PB
+
+ const path = e.servedPath;
+ const expires = Math.round(Date.now() / 1000) + 3600;
+ const signedUrl = signUrl(securityKey, baseUrl, path, expires);
+ console.log(`signedUrl=${signedUrl}`);
+
+ // This redirect is a tricky thing. We do this to avoid proxying file requests via our pocketbase origin server.
+ // The idea is to reduce load.
+ // HOWEVER, this redirect slows down image loading because it now takes 2 requests per image.
+ e.redirect(302, signedUrl);
+
+ e.next()
+})
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs b/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
index 83cdbe64..1d9b6e6c 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
@@ -53,10 +53,12 @@
+ <%#
+ %>
<%- slots.body || slot%>
diff --git a/services/pocketbase/pb_hooks/pages/(site)/account/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/account/index.ejs
index 76581c37..01f4028f 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/account/index.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/account/index.ejs
@@ -7,6 +7,7 @@
Please note: Patron status updates may take up to one minute to synchronize.
+Patreon ID: <%= auth.get('patreonId') %>
Role:
<% if (auth.get('patron')) { %>
@@ -17,9 +18,8 @@
<% } %>
-Patreon ID: <%= auth.get('patreonId') %>
-
+<% if (auth.get('patron')) { %>
Account Settings
@@ -33,4 +33,5 @@
-
\ No newline at end of file
+
+<% } %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/patrons/+load.js b/services/pocketbase/pb_hooks/pages/(site)/patrons/+load.js
new file mode 100644
index 00000000..94f5450c
--- /dev/null
+++ b/services/pocketbase/pb_hooks/pages/(site)/patrons/+load.js
@@ -0,0 +1,37 @@
+// +load.js
+
+/** @type {import('pocketpages').PageDataLoaderFunc} */
+module.exports = function (api) {
+ const { response } = api;
+
+ try {
+ // Find all users who have both a publicUsername and are patrons
+ const patronsRaw = $app.findRecordsByFilter(
+ 'users',
+ 'publicUsername != "" && patron = true',
+ '-created', // sort (optional)
+ 50, // limit
+ 0, // offset
+ null // params (none used)
+ );
+
+ // Map to plain JSON-safe objects
+ const patrons = patronsRaw.map((p) => ({
+ name: p.get('name'),
+ username: p.get('publicUsername'),
+ }));
+
+ console.log('Patrons:', JSON.stringify(patrons, null, 2));
+
+ return { patrons };
+
+ } catch (e) {
+ console.error('error!', e.message);
+
+ if (e.message.match(/no rows/)) {
+ return response.html(404, 'Patrons not found');
+ } else {
+ return response.html(500, 'Unknown internal error while fetching patrons');
+ }
+ }
+};
diff --git a/services/pocketbase/pb_hooks/pages/(site)/patrons/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/patrons/index.ejs
index f289944d..23a643a1 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/patrons/index.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/patrons/index.ejs
@@ -1,64 +1,19 @@
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vods/+load.js b/services/pocketbase/pb_hooks/pages/(site)/vods/+load.js
index 61079c93..5d242476 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/vods/+load.js
+++ b/services/pocketbase/pb_hooks/pages/(site)/vods/+load.js
@@ -1,40 +1,22 @@
-// +load.js
-
-/** @type {import('pocketpages').PageDataLoaderFunc} */
+/**
+ * @typedef {import('pocketbase').default} PocketBase
+ * @typedef {import('../pb/pocketbase-types').TypedPocketBase} TypedPocketBase
+ * @typedef {import('pocketpages').PageDataLoaderFunc} PageDataLoaderFunc
+ */
module.exports = function (api) {
- const { request, response } = api;
-
+ const { request, response, params, pb } = api;
try {
- // Read query params with PocketPages
- const query = request.url.query || {};
- const page = parseInt(query.page) || 1;
- const limit = parseInt(query.limit) || 200;
- const offset = (page - 1) * limit;
+ const perPage = params.perPage || 25;
+ const page = params.page || 1;
- // Fetch all VODs, sorted by -streamDate
- const allVods = $app.findRecordsByFilter('vods', null, '-streamDate');
+ const client = pb({ request });
+ const vods = client.collection('vods').getList(page, perPage, {
+ expand: 'vtubers',
+ sort: '-streamDate'
+ })
- // Slice according to pagination
- const vods = allVods.slice(offset, offset + limit);
- // Expand related vtubers
- $app.expandRecords(vods, ['vtubers'], null);
-
- // Pagination metadata
- const total = allVods.length;
- const totalPages = Math.ceil(total / limit);
-
- return {
- vods,
- pagination: {
- page,
- limit,
- total,
- totalPages,
- hasNext: page < totalPages,
- hasPrev: page > 1,
- },
- };
+ return { vods };
} catch (e) {
console.error('Error fetching VODs:', e.message);
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vods/[id]/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/vods/[id]/index.ejs
index 0036fdc1..73341fa4 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/vods/[id]/index.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/vods/[id]/index.ejs
@@ -67,6 +67,13 @@
Notes:
<%= data.vod?.get('notes') %>
<% } %>
+
+ <% if (data.vod?.get('thumbnail')) { %>
+ Thumbnail:
+
+
+
+ <% } %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vods/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/vods/index.ejs
index 69d926b9..e60d9fde 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/vods/index.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/vods/index.ejs
@@ -1,72 +1,3 @@
VODs
-<% if (Array.isArray(data.vods) && data.vods.length > 0) { %>
-
-
-
-
- Stream Date
- VTubers
-
-
-
- <% for (const vod of data.vods) { %>
-
-
-
- <%= vod?.get ? vod.get('streamDate') : vod?.streamDate ?? 'Unknown date' %>
-
-
-
- <% const vtubers = vod?.get ? vod.get('expand')?.vtubers ?? [] : vod?.vtubers ?? []; %>
- <% if (vtubers.length > 0) { %>
- <% for (let i = 0; i < vtubers.length; i++) { %>
- <%= vtubers[i]?.get ? vtubers[i].get('displayName') : vtubers[i]?.displayName ?? 'Unknown' %>
- <%= i < vtubers.length - 1 ? ', ' : '' %>
- <% } %>
- <% } else { %>
- None
- <% } %>
-
-
- <% } %>
-
-
-
-
-
-<% if (data.pagination && data.pagination.totalPages > 1) { %>
-
-<% } %>
-
-<% } else { %>
-No VODs available.
-<% } %>
\ No newline at end of file
+<%- include('vod-list.ejs', data) %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+load.js b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+load.js
deleted file mode 100644
index 3194a602..00000000
--- a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+load.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// +load.js
-
-/** @type {import('pocketpages').PageDataLoaderFunc} */
-module.exports = function (api) {
- const { params, response } = api;
- try {
- const vtuber = $app.findFirstRecordByData('vtubers', 'slug', params.slug);
- $app.expandRecord(vtuber, ["vods"], null);
- // console.log(JSON.stringify(vtuber))
- return { vtuber };
- } catch (e) {
- console.error('error!', e.message);
-
- if (e.message.match(/no rows/)) {
- console.log('we are sending 404')
- return response.html(404, 'VTuber not found')
-
- } else {
- console.log('we are sending error 500')
- return response.html(500, 'Unknown internal error while fetching vtuber')
-
- }
- }
-};
-
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+middleware.js b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+middleware.js
new file mode 100644
index 00000000..8e5f3c24
--- /dev/null
+++ b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/+middleware.js
@@ -0,0 +1,40 @@
+// +middleware.js
+
+
+
+
+/** @type {import('pocketpages').PageDataLoaderFunc} */
+module.exports = function (api) {
+ const { params, request, pb } = api;
+
+ const perPage = params.perPage || 25;
+ const page = params.page || 1;
+ const vtuber = $app.findFirstRecordByData('vtubers', 'slug', params.slug);
+ const client = pb({ request });
+ const vods = client.collection('vods').getList(page, perPage, {
+ expand: 'vtubers',
+ sort: '-streamDate',
+ filter: `vtubers.id ?= "${vtuber.id}"`
+ })
+
+
+ // await pb.collection("posts").getList(1, 30, {
+ // filter: "comments_via_post.message ?~ 'hello'"
+ // expand: "comments_via_post.user",
+ // })
+
+ // console.log(JSON.stringify(vods, null, 2));
+
+ // findRecordsByFilter(collectionModelOrIdentifier, filter, sort, limit, offset, ...params): core.Record[]
+ // $app.expandRecord(vtuber, ['vods_via_vtubers'], null);
+
+
+ // $app.expandRecord(vod, ["vtubers"], null);
+ // console.log(eerrs)
+
+
+ return { vods, vtuber };
+
+
+};
+
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/index.ejs
index 528f17d0..d31545f8 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/index.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/index.ejs
@@ -1,3 +1 @@
-<% if (data.vtuber) { %>
-<%- include('vtuber.ejs', { vtuber: data.vtuber }) %>
-<% } %>
\ No newline at end of file
+<%- include('vtuber.ejs', data) %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/vods/index.ejs b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/vods/index.ejs
new file mode 100644
index 00000000..05f762aa
--- /dev/null
+++ b/services/pocketbase/pb_hooks/pages/(site)/vt/[slug]/vods/index.ejs
@@ -0,0 +1,3 @@
+<%= data.vtuber.get('displayName') %> VODs
+
+<%- include('vod-list.ejs', data) %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/(site)/vt/_private/vtuber.ejs b/services/pocketbase/pb_hooks/pages/(site)/vt/_private/vtuber.ejs
new file mode 100644
index 00000000..b5356d60
--- /dev/null
+++ b/services/pocketbase/pb_hooks/pages/(site)/vt/_private/vtuber.ejs
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+ <%# VTuber Name %>
+
+ <%= data.vtuber?.get?.('displayName') || 'Unknown VTuber' %>
+
+
+
+ <%# VTuber Image %>
+
+
+
+
+
+
+
+
Theme Color
+ <% const themeColor = data.vtuber?.get('themeColor') || '#999999' %>
+
+
+
+
<%= themeColor %>
+
+
+
+
+
+
+
+
+
+ <% const socials = {
+ chaturbate: data.vtuber?.get('chaturbate'),
+ twitter: data.vtuber?.get('twitter'),
+ patreon: data.vtuber?.get('patreon'),
+ twitch: data.vtuber?.get('twitch'),
+ tiktok: data.vtuber?.get('tiktok'),
+ onlyfans: data.vtuber?.get('onlyfans'),
+ youtube: data.vtuber?.get('youtube'),
+ linktree: data.vtuber?.get('linktree'),
+ carrd: data.vtuber?.get('carrd'),
+ fansly: data.vtuber?.get('fansly'),
+ pornhub: data.vtuber?.get('pornhub'),
+ discord: data.vtuber?.get('discord'),
+ reddit: data.vtuber?.get('reddit'),
+ throne: data.vtuber?.get('throne'),
+ instagram: data.vtuber?.get('instagram'),
+ facebook: data.vtuber?.get('facebook'),
+ merch: data.vtuber?.get('merch')
+ } %>
+
+ <% const definedSocials = Object.entries(socials).filter(([_, v]) => v) %>
+
+ <% if (definedSocials.length > 0) { %>
+
+ <% } %>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/_private/vod-list.ejs b/services/pocketbase/pb_hooks/pages/_private/vod-list.ejs
new file mode 100644
index 00000000..0944fbf6
--- /dev/null
+++ b/services/pocketbase/pb_hooks/pages/_private/vod-list.ejs
@@ -0,0 +1,79 @@
+<% if (Array.isArray(data.vods.items) && data.vods.items.length > 0) { %>
+
+
+
+<% if (data.vods.totalPages > 1) { %>
+
+<% } %>
+
+<% } else { %>
+No VODs available.
+<% } %>
\ No newline at end of file
diff --git a/services/pocketbase/pb_hooks/pages/_private/vtuber.ejs b/services/pocketbase/pb_hooks/pages/_private/vtuber.ejs
deleted file mode 100644
index 76a9ec50..00000000
--- a/services/pocketbase/pb_hooks/pages/_private/vtuber.ejs
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- <%= data.vtuber?.displayName || data.vtuber?.get?.('displayName') || 'Unknown VTuber' %>
-
-
-
-
-
-
- VODs
-
- <%
- const vods = data.vtuber.get('expand')?.vods || [];
- if (vods.length > 0) {
- %>
-
- <% for (const vod of vods) { %>
-
- <% } %>
-
- <% } else { %>
- No VODs available for this VTuber.
- <% } %>
-
-
-
-
-
\ No newline at end of file
diff --git a/services/pocketbase/pb_migrations/1762408434_updated_vtubers.js b/services/pocketbase/pb_migrations/1762408434_updated_vtubers.js
new file mode 100644
index 00000000..11df968c
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762408434_updated_vtubers.js
@@ -0,0 +1,28 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3872109612")
+
+ // remove field
+ collection.fields.removeById("relation3825607268")
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3872109612")
+
+ // add field
+ collection.fields.addAt(3, new Field({
+ "cascadeDelete": false,
+ "collectionId": "pbc_144770472",
+ "hidden": false,
+ "id": "relation3825607268",
+ "maxSelect": 999,
+ "minSelect": 0,
+ "name": "vods",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "relation"
+ }))
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762500151_updated_vods.js b/services/pocketbase/pb_migrations/1762500151_updated_vods.js
new file mode 100644
index 00000000..7db884eb
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762500151_updated_vods.js
@@ -0,0 +1,48 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // remove field
+ collection.fields.removeById("text3277268710")
+
+ // add field
+ collection.fields.addAt(15, new Field({
+ "hidden": false,
+ "id": "file3277268710",
+ "maxSelect": 1,
+ "maxSize": 0,
+ "mimeTypes": [],
+ "name": "thumbnail",
+ "presentable": false,
+ "protected": false,
+ "required": false,
+ "system": false,
+ "thumbs": [],
+ "type": "file"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // add field
+ collection.fields.addAt(10, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3277268710",
+ "max": 0,
+ "min": 0,
+ "name": "thumbnail",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ // remove field
+ collection.fields.removeById("file3277268710")
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762530625_created_vtuber_vods.js b/services/pocketbase/pb_migrations/1762530625_created_vtuber_vods.js
new file mode 100644
index 00000000..4600a5c5
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762530625_created_vtuber_vods.js
@@ -0,0 +1,49 @@
+///
+migrate((app) => {
+ const collection = new Collection({
+ "createRule": null,
+ "deleteRule": null,
+ "fields": [
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 0,
+ "min": 0,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "_clone_oC8t",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ }
+ ],
+ "id": "pbc_3009055234",
+ "indexes": [],
+ "listRule": null,
+ "name": "vtuber_vods",
+ "system": false,
+ "type": "view",
+ "updateRule": null,
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate\nFROM vods\nLEFT JOIN json_each(vods.vtubers) ON json_each.value = 'el_xox'",
+ "viewRule": null
+ });
+
+ return app.save(collection);
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234");
+
+ return app.delete(collection);
+})
diff --git a/services/pocketbase/pb_migrations/1762530642_updated_vtuber_vods.js b/services/pocketbase/pb_migrations/1762530642_updated_vtuber_vods.js
new file mode 100644
index 00000000..4254d84e
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762530642_updated_vtuber_vods.js
@@ -0,0 +1,52 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate\nFROM vods\nLEFT JOIN json_each(vods.vtubers) ON json_each.value = 'udqmxs649ajf2mk'"
+ }, collection)
+
+ // remove field
+ collection.fields.removeById("_clone_oC8t")
+
+ // add field
+ collection.fields.addAt(1, new Field({
+ "hidden": false,
+ "id": "_clone_1BVl",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate\nFROM vods\nLEFT JOIN json_each(vods.vtubers) ON json_each.value = 'el_xox'"
+ }, collection)
+
+ // add field
+ collection.fields.addAt(1, new Field({
+ "hidden": false,
+ "id": "_clone_oC8t",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ }))
+
+ // remove field
+ collection.fields.removeById("_clone_1BVl")
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762530828_updated_vtuber_vods.js b/services/pocketbase/pb_migrations/1762530828_updated_vtuber_vods.js
new file mode 100644
index 00000000..4be00fd4
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762530828_updated_vtuber_vods.js
@@ -0,0 +1,71 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate,\n vtubers.displayName\nFROM vods\nLEFT JOIN vtubers"
+ }, collection)
+
+ // remove field
+ collection.fields.removeById("_clone_1BVl")
+
+ // add field
+ collection.fields.addAt(1, new Field({
+ "hidden": false,
+ "id": "_clone_h4Aw",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ }))
+
+ // add field
+ collection.fields.addAt(2, new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "_clone_yNn2",
+ "max": 0,
+ "min": 0,
+ "name": "displayName",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234")
+
+ // update collection data
+ unmarshal({
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate\nFROM vods\nLEFT JOIN json_each(vods.vtubers) ON json_each.value = 'udqmxs649ajf2mk'"
+ }, collection)
+
+ // add field
+ collection.fields.addAt(1, new Field({
+ "hidden": false,
+ "id": "_clone_1BVl",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ }))
+
+ // remove field
+ collection.fields.removeById("_clone_h4Aw")
+
+ // remove field
+ collection.fields.removeById("_clone_yNn2")
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762531591_deleted_vtuber_vods.js b/services/pocketbase/pb_migrations/1762531591_deleted_vtuber_vods.js
new file mode 100644
index 00000000..5c43d9a4
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762531591_deleted_vtuber_vods.js
@@ -0,0 +1,63 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3009055234");
+
+ return app.delete(collection);
+}, (app) => {
+ const collection = new Collection({
+ "createRule": null,
+ "deleteRule": null,
+ "fields": [
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 0,
+ "min": 0,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "_clone_h4Aw",
+ "max": "",
+ "min": "",
+ "name": "streamDate",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "_clone_yNn2",
+ "max": 0,
+ "min": 0,
+ "name": "displayName",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }
+ ],
+ "id": "pbc_3009055234",
+ "indexes": [],
+ "listRule": null,
+ "name": "vtuber_vods",
+ "system": false,
+ "type": "view",
+ "updateRule": null,
+ "viewQuery": "SELECT\n vods.id,\n vods.streamDate,\n vtubers.displayName\nFROM vods\nLEFT JOIN vtubers",
+ "viewRule": null
+ });
+
+ return app.save(collection);
+})
diff --git a/services/pocketbase/pb_migrations/1762622017_updated_vods.js b/services/pocketbase/pb_migrations/1762622017_updated_vods.js
new file mode 100644
index 00000000..92fb8ef1
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762622017_updated_vods.js
@@ -0,0 +1,20 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "viewRule": ""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762622042_updated_vods.js b/services/pocketbase/pb_migrations/1762622042_updated_vods.js
new file mode 100644
index 00000000..8cea92be
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762622042_updated_vods.js
@@ -0,0 +1,20 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": ""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762622148_updated_vods.js b/services/pocketbase/pb_migrations/1762622148_updated_vods.js
new file mode 100644
index 00000000..a6b129b1
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762622148_updated_vods.js
@@ -0,0 +1,22 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": null,
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": "",
+ "viewRule": ""
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762622255_updated_vods.js b/services/pocketbase/pb_migrations/1762622255_updated_vods.js
new file mode 100644
index 00000000..b61b310d
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762622255_updated_vods.js
@@ -0,0 +1,22 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": "",
+ "viewRule": ""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_144770472")
+
+ // update collection data
+ unmarshal({
+ "listRule": null,
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/pb_migrations/1762622465_updated_vtubers.js b/services/pocketbase/pb_migrations/1762622465_updated_vtubers.js
new file mode 100644
index 00000000..595db17f
--- /dev/null
+++ b/services/pocketbase/pb_migrations/1762622465_updated_vtubers.js
@@ -0,0 +1,22 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3872109612")
+
+ // update collection data
+ unmarshal({
+ "listRule": "",
+ "viewRule": ""
+ }, collection)
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_3872109612")
+
+ // update collection data
+ unmarshal({
+ "listRule": null,
+ "viewRule": null
+ }, collection)
+
+ return app.save(collection)
+})
diff --git a/services/pocketbase/utils/data_migrations/2025-11-05-populate-vtuber-field.js b/services/pocketbase/utils/data_migrations/2025-11-05-populate-vtuber-field.js
new file mode 100644
index 00000000..b160ac77
--- /dev/null
+++ b/services/pocketbase/utils/data_migrations/2025-11-05-populate-vtuber-field.js
@@ -0,0 +1,64 @@
+/**
+ * Migration Script: Assign missing VOD vtubers to "projektmelody"
+ * ---------------------------------------------------------------
+ * This script connects to a PocketBase instance and updates all VOD records
+ * that do not have a `vtuber` set, assigning them to the vtuber with slug "projektmelody".
+ *
+ * Environment variables:
+ * PB_URL Base URL of your PocketBase instance (e.g. "http://127.0.0.1:8090")
+ * PB_ADMIN_EMAIL PocketBase admin email
+ * PB_ADMIN_PASS PocketBase admin password
+ *
+ * Usage:
+ * $ npx @dotenvx/dotenvx run -f .env.local -- node ./2025-11-05-fix-vod-vtuber.js
+ */
+
+import PocketBase from 'pocketbase';
+
+const pb = new PocketBase(process.env.PB_URL || 'http://127.0.0.1:8090');
+
+async function main() {
+ console.log('Authenticating with PocketBase...');
+ await pb
+ .collection("_superusers")
+ .authWithPassword(process.env.PB_USERNAME, process.env.PB_PASSWORD);
+
+
+ console.log('Fetching vtuber "projektmelody"...');
+ const projekt = await pb.collection('vtubers').getFirstListItem('slug="projektmelody"');
+ if (!projekt) {
+ throw new Error('Could not find vtuber with slug "projektelody"');
+ }
+
+ console.log('Fetching VODs...');
+ let page = 1;
+ const perPage = 50;
+ let updatedCount = 0;
+
+ while (true) {
+ const vods = await pb.collection('vods').getList(page, perPage);
+ if (vods.items.length === 0) break;
+
+ for (const vod of vods.items) {
+ // Only update if vtuber is missing or empty
+ if (!vod.vtubers || vod.vtubers.length === 0) {
+ await pb.collection('vods').update(vod.id, {
+ vtubers: [projekt.id],
+ });
+ console.log(`✅ Updated VOD ${vod.id} → projektmelody`);
+ updatedCount++;
+ }
+ }
+
+ if (vods.items.length < perPage) break;
+ page++;
+ }
+
+ console.log(`Done! Updated ${updatedCount} VODs that had no vtuber set.`);
+ pb.authStore.clear();
+}
+
+main().catch((err) => {
+ console.error('Migration failed:', err);
+ process.exit(1);
+});
diff --git a/services/pocketbase/utils/data_migrations/2025-11-05-see-image-internal.js b/services/pocketbase/utils/data_migrations/2025-11-05-see-image-internal.js
new file mode 100644
index 00000000..98244926
--- /dev/null
+++ b/services/pocketbase/utils/data_migrations/2025-11-05-see-image-internal.js
@@ -0,0 +1,21 @@
+import PocketBase from 'pocketbase';
+
+const pb = new PocketBase(process.env.PB_URL || 'http://127.0.0.1:8090');
+
+async function main() {
+ console.log('Authenticating with PocketBase...');
+ await pb
+ .collection("_superusers")
+ .authWithPassword(process.env.PB_USERNAME, process.env.PB_PASSWORD);
+
+ const name = 'mirakink'
+ const vt = await pb.collection('vtubers').getFirstListItem(`slug="${name}"`);
+ if (!vt) {
+ throw new Error(`Could not find vtuber with slug "${name}"`);
+ }
+
+ console.log(`image internals as follows`)
+ console.log(vt)
+}
+
+main()
\ No newline at end of file
diff --git a/services/pocketbase/utils/data_migrations/2025-11-07-import-thumbnails.js b/services/pocketbase/utils/data_migrations/2025-11-07-import-thumbnails.js
new file mode 100644
index 00000000..2f7e6ff4
--- /dev/null
+++ b/services/pocketbase/utils/data_migrations/2025-11-07-import-thumbnails.js
@@ -0,0 +1,118 @@
+import PocketBase from 'pocketbase';
+import { readFileSync } from 'node:fs';
+import { basename, join } from 'node:path';
+import spawn from 'nano-spawn';
+import { tmpdir } from 'node:os';
+import mime from 'mime';
+
+const pb = new PocketBase(process.env.PB_URL || 'http://127.0.0.1:8090');
+if (!process.env.PB_USERNAME) throw new Error('PB_USERNAME missing');
+if (!process.env.PB_PASSWORD) throw new Error('PB_PASSWORD missing');
+if (!process.env.B2_BUCKET_FROM) throw new Error('B2_BUCKET_FROM missing');
+if (!process.env.V1_DATA_FILE) throw new Error('V1_DATA_FILE missing');
+
+const vodsCollectionName = 'pbc_144770472';
+// pbc_144770472 is the vods collection
+// cv6m31vj98gmtsx is a sample vod id
+
+
+async function main() {
+ console.log('Authenticating with PocketBase...');
+ await pb
+ .collection("_superusers")
+ .authWithPassword(process.env.PB_USERNAME, process.env.PB_PASSWORD);
+
+
+ // Use vod.streamDate to find the correct entry
+ // load v1 datafile
+ const v1VodDataRaw = readFileSync(process.env.V1_DATA_FILE, { encoding: 'utf-8' });
+ const v1VodData = JSON.parse(v1VodDataRaw);
+ // console.log('v1VodData sample', v1VodData[0])
+
+
+ // # BUILD A MANIFEST
+ // For each vod
+ // Get the expected pocketbase s3 key format, ex: `pbc_144770472/cv6m31vj98gmtsx/projektmelody-fansly-2025-09-17-thumb.png`
+ // Get the v1 thumbnail s3 key from the v1 export json
+ let manifest = [];
+ const vods = await pb.collection('vods').getFullList();
+ for (const vod of vods) {
+ // console.log('v2VodData sample', vod)
+ // console.log(`for this vod`, vod)
+ const v1Vod = v1VodData.find((vod1) => new Date(vod1.streamDate).getTime() === new Date(vod.streamDate).getTime());
+ console.log(`v1vod`, v1Vod)
+ if (!v1Vod) throw new Error(`failed to find matching v1 data vod for vod`);
+
+ // skip if there is no thumbnail in the v1 vod
+ if (!v1Vod.thumbnail) continue;
+ const inputS3Key = v1Vod.thumbnail.replace('content/', '');
+ const outputFile = join(tmpdir(), basename(inputS3Key));
+ const entry = {
+ vodId: vod.id,
+ inputS3Key,
+ outputFile,
+ };
+ manifest.push(entry);
+ }
+
+ const invalidVods = manifest.filter((m) => m.inputS3Key.includes('mp4'))
+ if (invalidVods.length > 0) {
+ console.error('invalid thumbnails found', invalidVods)
+ throw new Error('invalid. mp4s found in thumbnails');
+ }
+
+
+ console.log('manifest', manifest);
+
+ // # ACTUAL WORK
+ // Copy the file from B2_BUCKET_FROM to tmp file
+ // Update the pocketbase vod.thumbnail entry
+
+ function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ async function retry(fn, retries = 6, delayMs = 500) {
+ let lastError;
+ for (let attempt = 1; attempt <= retries; attempt++) {
+ try {
+ return await fn();
+ } catch (err) {
+ lastError = err;
+ console.warn(`Attempt ${attempt} failed: ${err}. Retrying in ${delayMs}ms...`);
+ if (attempt < retries) await sleep(delayMs);
+ }
+ }
+ throw lastError;
+ }
+
+ for (const [i, m] of manifest.entries()) {
+ var from = "b2://" + process.env.B2_BUCKET_FROM + "/" + m.inputS3Key;
+ var to = m.outputFile;
+ var vodId = m.vodId;
+
+ console.log("processing thumbnail " + i + ". " + from + " -> " + to + " (vodId=" + vodId + ")");
+
+ // Retry the download
+ await retry(function () {
+ return spawn('b2', ['file', 'download', from, to]);
+ });
+
+ // Retry the PocketBase upload
+ await retry(async function () {
+ var fileData = readFileSync(m.outputFile);
+ var mimetype = mime.getType(m.outputFile) || undefined;
+ var file = new File([fileData], basename(m.outputFile), { type: mimetype });
+ var form = new FormData();
+ form.set("thumbnail", file);
+ return pb.collection('vods').update(vodId, form);
+ });
+ }
+
+ console.log("All done.");
+
+
+
+}
+
+main()
\ No newline at end of file
diff --git a/services/pocketbase/utils/data_migrations/vods.json b/services/pocketbase/utils/data_migrations/vods.json
index 87fcdb83..196bd68e 100644
--- a/services/pocketbase/utils/data_migrations/vods.json
+++ b/services/pocketbase/utils/data_migrations/vods.json
@@ -143,7 +143,7 @@
"streamDate": "2025-10-29T08:16:00.000Z",
"notes": "Recorded using ffmpeg. Recorded via Chaturbate. \n\nIPFS CID bafybeicc3vkvyptu522gt4lgr65xadk2l6y7irwkqepsggzmuci64xxlvu\n\nMagnet link magnet:?xt=urn:btih:b4ce3c94cd90e620025e1710ad7f4e47cbba1e4e&dn=projektmelody-chaturbate-2025-10-29.mp4",
"sourceVideo": "content/projektmelody-chaturbate-2025-10-29.mp4",
- "thumbnail": "content/projektmelody-chaturbate-2025-10-29.mp4",
+ "thumbnail": "content/js.png",
"ipfsCid": null,
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2025-10-29.mp4",
"muxAssetId": "ET8FXN00guyrpugug00dSMU00m4odQayzhVT3zAHpAMWo00",
@@ -183,7 +183,7 @@
"streamDate": "2023-01-02T07:03:49.000Z",
"notes": "This VOD is not original HD quality. We are searching for a better version.",
"sourceVideo": "content/projektmelody-chaturbate-2023-01-01.mp4",
- "thumbnail": "content/projektmelody-chaturbate-2023-01-01.mp4",
+ "thumbnail": "content/js.png",
"ipfsCid": "bafybeie3oynhomwdwvkdwgef2viii7kccpegnk6zvfprl4fyz6p52qwh6u",
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2023-01-01.mp4",
"muxAssetId": null,
diff --git a/services/pocketbase/utils/sign-url.js b/services/pocketbase/utils/sign-url.js
new file mode 100644
index 00000000..e574a7d2
--- /dev/null
+++ b/services/pocketbase/utils/sign-url.js
@@ -0,0 +1,73 @@
+var queryString = require("querystring");
+var crypto = require("crypto");
+
+function addCountries(url, a, b) {
+ var tempUrl = url;
+ if (a != null) {
+ var tempUrlOne = new URL(tempUrl);
+ tempUrl += ((tempUrlOne.search == "") ? "?" : "&") + "token_countries=" + a;
+ }
+ if (b != null) {
+ var tempUrlTwo = new URL(tempUrl);
+ tempUrl += ((tempUrlTwo.search == "") ? "?" : "&") + "token_countries_blocked=" + b;
+ }
+ return tempUrl;
+}
+
+function signUrl(url, securityKey, expirationTime = 103600, userIp, isDirectory = false, pathAllowed, countriesAllowed, countriesBlocked) {
+ /*
+ url: CDN URL w/o the trailing '/' - exp. http://test.b-cdn.net/file.png
+ securityKey: Security token found in your pull zone
+ expirationTime: Authentication validity (default. 86400 sec/24 hrs)
+ userIp: Optional parameter if you have the User IP feature enabled
+ isDirectory: Optional parameter - "true" returns a URL separated by forward slashes (exp. (domain)/bcdn_token=...)
+ pathAllowed: Directory to authenticate (exp. /path/to/images)
+ countriesAllowed: List of countries allowed (exp. CA, US, TH)
+ countriesBlocked: List of countries blocked (exp. CA, US, TH)
+ */
+ var parameterData = "", parameterDataUrl = "", signaturePath = "", hashableBase = "", token = "";
+ // var expires = Math.floor(new Date() / 1000) + expirationTime;
+ const expires = expirationTime;
+ var url = addCountries(url, countriesAllowed, countriesBlocked);
+ var parsedUrl = new URL(url);
+ var parameters = (new URL(url)).searchParams;
+ if (pathAllowed != "") {
+ signaturePath = pathAllowed;
+ parameters.set("token_path", signaturePath);
+ } else {
+ signaturePath = decodeURIComponent(parsedUrl.pathname);
+ }
+ parameters.sort();
+ if (Array.from(parameters).length > 0) {
+ parameters.forEach(function (value, key) {
+ if (value == "") {
+ return;
+ }
+ if (parameterData.length > 0) {
+ parameterData += "&";
+ }
+ parameterData += key + "=" + value;
+ parameterDataUrl += "&" + key + "=" + queryString.escape(value);
+
+ });
+ }
+ hashableBase = securityKey + signaturePath + expires + ((userIp != null) ? userIp : "") + parameterData;
+ console.log(`hashableBase=${hashableBase}`)
+ token = Buffer.from(crypto.createHash("sha256").update(hashableBase).digest()).toString("base64");
+ token = token.replace(/\n/g, "").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+ if (isDirectory) {
+ return parsedUrl.protocol + "//" + parsedUrl.host + "/bcdn_token=" + token + parameterDataUrl + "&expires=" + expires + parsedUrl.pathname;
+ } else {
+ return parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname + "?token=" + token + parameterDataUrl + "&expires=" + expires;
+ }
+}
+
+const securityKey = 'd5814175-cc56-4098-ae63-1096301fb3c1';
+const sampleUrl = 'https://fppbdev.b-cdn.net/pbc_3872109612/z0bpy5cwxi1uksv/g4ot5_omb_qaei_o7_y_tuzdlcn1se.jpeg';
+// const expires = Math.round(Date.now() / 1000) + 3600;
+const expires = 3524904301;
+const signedUrl = signUrl(sampleUrl, securityKey, expires, null, false, '');
+
+console.log(`signedUrl=${signedUrl}`);
+
+module.exports = { signUrl };
\ No newline at end of file