add /vods and /vt/:slug/vods
This commit is contained in:
parent
fe1f318424
commit
6caf2dbcc3
@ -92,3 +92,15 @@ https://pocketbuilds.com/
|
|||||||
how the pros do it
|
how the pros do it
|
||||||
|
|
||||||
https://github.com/benallfree/pocketpages/blob/5bc48d4f8df75b2f78ca61fa18c792d814b926e8/packages/starters/deploy-pockethost-manual/deploy-pockethost.ts#L5
|
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
|
||||||
33
services/pocketbase/package-lock.json
generated
33
services/pocketbase/package-lock.json
generated
@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "futureporn",
|
"name": "futureporn",
|
||||||
"version": "0.0.3",
|
"version": "3.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "futureporn",
|
"name": "futureporn",
|
||||||
"version": "0.0.3",
|
"version": "3.0.3",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mime": "^4.1.0",
|
||||||
|
"nano-spawn": "^2.0.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"pocketpages": ">=0.22.3",
|
"pocketpages": ">=0.22.3",
|
||||||
"pocketpages-plugin-auth": "^0.2.2",
|
"pocketpages-plugin-auth": "^0.2.2",
|
||||||
@ -1771,6 +1773,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/mimic-function": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
"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": "^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": {
|
"node_modules/onetime": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "futureporn",
|
"name": "futureporn",
|
||||||
"version": "3.0.3",
|
"version": "3.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Dedication to the preservation of lewdtuber history",
|
"description": "Dedication to the preservation of lewdtuber history",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
@ -9,6 +9,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mime": "^4.1.0",
|
||||||
|
"nano-spawn": "^2.0.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"pocketpages": ">=0.22.3",
|
"pocketpages": ">=0.22.3",
|
||||||
"pocketpages-plugin-auth": "^0.2.2",
|
"pocketpages-plugin-auth": "^0.2.2",
|
||||||
|
|||||||
107
services/pocketbase/pb_hooks/cdn.pb.js
Normal file
107
services/pocketbase/pb_hooks/cdn.pb.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
})
|
||||||
@ -53,10 +53,12 @@
|
|||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<%#
|
||||||
<div class="notification is-info" data-signals='{"showNotif": false}' data-show="$showNotif">
|
<div class="notification is-info" data-signals='{"showNotif": false}' data-show="$showNotif">
|
||||||
<button class="delete" data-on-click="$showNotif = false"></button>
|
<button class="delete" data-on-click="$showNotif = false"></button>
|
||||||
<span data-text="$message"></span>
|
<span data-text="$message"></span>
|
||||||
</div>
|
</div>
|
||||||
|
%>
|
||||||
|
|
||||||
|
|
||||||
<%- slots.body || slot%>
|
<%- slots.body || slot%>
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
Please note: Patron status updates may take up to one minute to synchronize.
|
Please note: Patron status updates may take up to one minute to synchronize.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Patreon ID:</strong> <%= auth.get('patreonId') %></p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Role:</strong>
|
<strong>Role:</strong>
|
||||||
<% if (auth.get('patron')) { %>
|
<% if (auth.get('patron')) { %>
|
||||||
@ -17,9 +18,8 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><strong>Patreon ID:</strong> <%= auth.get('patreonId') %></p>
|
|
||||||
|
|
||||||
|
|
||||||
|
<% if (auth.get('patron')) { %>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<h3 class="title is-3">Account Settings</h3>
|
<h3 class="title is-3">Account Settings</h3>
|
||||||
|
|
||||||
@ -33,4 +33,5 @@
|
|||||||
<!-- <br />
|
<!-- <br />
|
||||||
<input class="button" type="submit" value="Save" /> -->
|
<input class="button" type="submit" value="Save" /> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
37
services/pocketbase/pb_hooks/pages/(site)/patrons/+load.js
Normal file
37
services/pocketbase/pb_hooks/pages/(site)/patrons/+load.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,64 +1,19 @@
|
|||||||
<p></p>
|
<p></p>
|
||||||
|
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<p class="subtitle">Thank you to our wonderful patrons who keep this site running</p>
|
<p class="subtitle">
|
||||||
|
Thank you to our wonderful patrons who keep this site running 💖
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if (Array.isArray(data.patrons) && data.patrons.length > 0) { %>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>Tyrs</li>
|
<% for (const patron of data.patrons) { %>
|
||||||
<li>StubbstheZombie</li>
|
<li>
|
||||||
<li>Delques1843</li>
|
<%= patron.name %>
|
||||||
<li>Cray Shay</li>
|
</li>
|
||||||
<li>Jay black</li>
|
<% } %>
|
||||||
<li>E T 4321</li>
|
|
||||||
<li>ProfessionalUwU</li>
|
|
||||||
<li>LzyAsn</li>
|
|
||||||
<li>RettichDerGeile</li>
|
|
||||||
<li>Vladislav Šlechta</li>
|
|
||||||
<li>FluffyPenguins527</li>
|
|
||||||
<li>Enderwolf</li>
|
|
||||||
<li>Kieren</li>
|
|
||||||
<li>Captain Highlighter</li>
|
|
||||||
<li>Jonas andreas Gulbrandsen-Efteland</li>
|
|
||||||
<li>Drefsab</li>
|
|
||||||
<li>Randernizer</li>
|
|
||||||
<li>sheldmaster</li>
|
|
||||||
<li>Joe Thelizard</li>
|
|
||||||
<li>IshikawaTXC</li>
|
|
||||||
<li>Alex</li>
|
|
||||||
<li>BioAct1ve</li>
|
|
||||||
<li>AverageYuriEnjoyer</li>
|
|
||||||
<li>Virted</li>
|
|
||||||
<li>MechiPlat</li>
|
|
||||||
<li>JustSleep</li>
|
|
||||||
<li>Cjyoubusta</li>
|
|
||||||
<li>Bravo The Chonk</li>
|
|
||||||
<li>LaoPao</li>
|
|
||||||
<li>Toffse</li>
|
|
||||||
<li>Rubén Ruiz</li>
|
|
||||||
<li>Brewhλwk</li>
|
|
||||||
<li>BusterMachine7</li>
|
|
||||||
<li>Lucas</li>
|
|
||||||
<li>Jogabbagabba</li>
|
|
||||||
<li>Tony</li>
|
|
||||||
<li>Mydus</li>
|
|
||||||
<li>Wubb wubb</li>
|
|
||||||
<li>François d'Aruna</li>
|
|
||||||
<li>Cheese And Crackers</li>
|
|
||||||
<li>Shift</li>
|
|
||||||
<li>Addable Gravy61</li>
|
|
||||||
<li>Heiko7761</li>
|
|
||||||
<li>DustyGreenTea</li>
|
|
||||||
<li>Lolithia</li>
|
|
||||||
<li>Aicome 2000</li>
|
|
||||||
<li>The Gameinator</li>
|
|
||||||
<li>Dice Loynes</li>
|
|
||||||
<li>MilkMoblin</li>
|
|
||||||
<li>Turnip Toss</li>
|
|
||||||
<li>C4425</li>
|
|
||||||
<li>TheSlimiestKing</li>
|
|
||||||
<li>R4z0rSh4rP</li>
|
|
||||||
<li>Lumbar247</li>
|
|
||||||
<li>Vilkki</li>
|
|
||||||
<li>Chiko</li>
|
|
||||||
<li>Djinn</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<% } else { %>
|
||||||
|
<p class="has-text-grey-light">Patron names are private by default. Become the first supporter and <a href="/account#settings">opt-in</a> to have your name shown!</p>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
@ -1,40 +1,22 @@
|
|||||||
// +load.js
|
/**
|
||||||
|
* @typedef {import('pocketbase').default} PocketBase
|
||||||
/** @type {import('pocketpages').PageDataLoaderFunc} */
|
* @typedef {import('../pb/pocketbase-types').TypedPocketBase} TypedPocketBase
|
||||||
|
* @typedef {import('pocketpages').PageDataLoaderFunc} PageDataLoaderFunc
|
||||||
|
*/
|
||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
const { request, response } = api;
|
const { request, response, params, pb } = api;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read query params with PocketPages
|
const perPage = params.perPage || 25;
|
||||||
const query = request.url.query || {};
|
const page = params.page || 1;
|
||||||
const page = parseInt(query.page) || 1;
|
|
||||||
const limit = parseInt(query.limit) || 200;
|
|
||||||
const offset = (page - 1) * limit;
|
|
||||||
|
|
||||||
// Fetch all VODs, sorted by -streamDate
|
const client = pb({ request });
|
||||||
const allVods = $app.findRecordsByFilter('vods', null, '-streamDate');
|
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
|
return { vods };
|
||||||
$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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching VODs:', e.message);
|
console.error('Error fetching VODs:', e.message);
|
||||||
|
|||||||
@ -67,6 +67,13 @@
|
|||||||
<p><b>Notes:</b></p>
|
<p><b>Notes:</b></p>
|
||||||
<pre class="p-2"><%= data.vod?.get('notes') %></pre>
|
<pre class="p-2"><%= data.vod?.get('notes') %></pre>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (data.vod?.get('thumbnail')) { %>
|
||||||
|
<p><b>Thumbnail:</b></p>
|
||||||
|
<figure class="image">
|
||||||
|
<img src="/api/files/vods/<%= data.vod?.get('id') %>/<%= data.vod?.get('thumbnail') %>" />
|
||||||
|
</figure>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5"></div>
|
<div class="mb-5"></div>
|
||||||
@ -1,72 +1,3 @@
|
|||||||
<h2 class="title is-2">VODs</h2>
|
<h2 class="title is-2">VODs</h2>
|
||||||
|
|
||||||
<% if (Array.isArray(data.vods) && data.vods.length > 0) { %>
|
<%- include('vod-list.ejs', data) %>
|
||||||
<div class="table-container">
|
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Stream Date</th>
|
|
||||||
<th>VTubers</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% for (const vod of data.vods) { %>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="/vods/<%= vod?.id %>" class="is-small is-link">
|
|
||||||
<%= vod?.get ? vod.get('streamDate') : vod?.streamDate ?? 'Unknown date' %>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<% 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
|
|
||||||
<% } %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% } %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
<% if (data.pagination && data.pagination.totalPages > 1) { %>
|
|
||||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
|
||||||
<% const currentPage = data.pagination.page; %>
|
|
||||||
<% const totalPages = data.pagination.totalPages; %>
|
|
||||||
|
|
||||||
<!-- Previous Page -->
|
|
||||||
<% if (data.pagination.hasPrev) { %>
|
|
||||||
<a class="pagination-previous" href="?page=<%= currentPage - 1 %>&limit=<%= data.pagination.limit %>">Previous</a>
|
|
||||||
<% } else { %>
|
|
||||||
<a class="pagination-previous" disabled>Previous</a>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- Next Page -->
|
|
||||||
<% if (data.pagination.hasNext) { %>
|
|
||||||
<a class="pagination-next" href="?page=<%= currentPage + 1 %>&limit=<%= data.pagination.limit %>">Next</a>
|
|
||||||
<% } else { %>
|
|
||||||
<a class="pagination-next" disabled>Next</a>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- Page Numbers -->
|
|
||||||
<ul class="pagination-list">
|
|
||||||
<% for (let i = 1; i <= totalPages; i++) { %>
|
|
||||||
<li>
|
|
||||||
<a class="pagination-link <%= i === currentPage ? 'is-current' : '' %>" href="?page=<%= i %>&limit=<%= data.pagination.limit %>">
|
|
||||||
<%= i %>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<% } %>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% } else { %>
|
|
||||||
<p>No VODs available.</p>
|
|
||||||
<% } %>
|
|
||||||
@ -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')
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -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 };
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,3 +1 @@
|
|||||||
<% if (data.vtuber) { %>
|
<%- include('vtuber.ejs', data) %>
|
||||||
<%- include('vtuber.ejs', { vtuber: data.vtuber }) %>
|
|
||||||
<% } %>
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<h2 class="title is-2"><%= data.vtuber.get('displayName') %> VODs</h2>
|
||||||
|
|
||||||
|
<%- include('vod-list.ejs', data) %>
|
||||||
129
services/pocketbase/pb_hooks/pages/(site)/vt/_private/vtuber.ejs
Normal file
129
services/pocketbase/pb_hooks/pages/(site)/vt/_private/vtuber.ejs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<div id="vtuber" class="">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<%# VTuber Name %>
|
||||||
|
<h2 class="title is-2">
|
||||||
|
<%= data.vtuber?.get?.('displayName') || 'Unknown VTuber' %>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<%# VTuber Image %>
|
||||||
|
<figure class="image is-128x128">
|
||||||
|
<img src="/api/files/vtubers/<%= data.vtuber?.id %>/<%= data.vtuber?.get('image') %>?thumb=128x128" alt="<%= data.vtuber?.get?.('displayName') || 'VTuber' %>" />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
|
||||||
|
|
||||||
|
<h3 class="title is-3">Theme Color</h3>
|
||||||
|
<% const themeColor = data.vtuber?.get('themeColor') || '#999999' %>
|
||||||
|
|
||||||
|
<div class="theme-color" style="
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: <%= themeColor %>
|
||||||
|
" title="<%= themeColor %>"></div>
|
||||||
|
|
||||||
|
<p class="has-text-grey is-size-7 mb-5"><%= themeColor %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<% 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) { %>
|
||||||
|
<section class="section">
|
||||||
|
<h3 class="title is-3">Socials</h3>
|
||||||
|
<div class="tags are-medium">
|
||||||
|
<% for (const [name, url] of definedSocials) { %>
|
||||||
|
<a href="<%= url %>" class="tag is-link is-light" target="_blank" rel="noopener">
|
||||||
|
<%= name.charAt(0).toUpperCase() + name.slice(1) %>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
|
||||||
|
<h3 class="title is-3">VODs</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<%
|
||||||
|
const vods = data.vods.items
|
||||||
|
if (vods.length > 0) {
|
||||||
|
%>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<% for (const vod of vods) { %>
|
||||||
|
<div class="column is-one-quarter-desktop is-half-tablet is-full-mobile">
|
||||||
|
<a href="/vods/<%= vod.id %>" class="box has-text-centered">
|
||||||
|
|
||||||
|
<!-- Thumbnail -->
|
||||||
|
<% if (vod.thumbnail) { %>
|
||||||
|
<figure class="image is-16by9 mb-2">
|
||||||
|
<img src="/api/files/vods/<%= vod.id %>/<%= vod.thumbnail %>?thumb=400x225" alt="VOD thumbnail for <%= vod.id %>" />
|
||||||
|
</figure>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="has-background-grey-lighter py-6 mb-2">
|
||||||
|
<span class="has-text-grey">No thumbnail</span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Title / ID -->
|
||||||
|
<p class="is-size-6 has-text-weight-semibold">
|
||||||
|
<%= vod.title || vod.id %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
|
<% if (vod.streamDate) { %>
|
||||||
|
<p class="is-size-7 has-text-grey">
|
||||||
|
<%= new Date(vod.streamDate).toLocaleDateString() %>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<a href="<%= request.url.pathname.replace(/\/$/, '') %>/vods">See all <%= data.vtuber?.get?.('displayName') %> vods</a>
|
||||||
|
<% } else { %>
|
||||||
|
<p>No VODs available for this VTuber.</p>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
79
services/pocketbase/pb_hooks/pages/_private/vod-list.ejs
Normal file
79
services/pocketbase/pb_hooks/pages/_private/vod-list.ejs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<% if (Array.isArray(data.vods.items) && data.vods.items.length > 0) { %>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Stream Date</th>
|
||||||
|
<th>VTuber</th>
|
||||||
|
<th>Thumbnail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% for (const vod of data.vods.items) { %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/vods/<%= vod.id %>" class="is-small is-link">
|
||||||
|
<%= vod.streamDate ? new Date(vod.streamDate).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' }) : 'Unknown date' %>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% const vtubers = vod.expand?.vtubers || []; %>
|
||||||
|
<% if (vtubers.length) { %>
|
||||||
|
<% vtubers.forEach(function(v, i){ %>
|
||||||
|
<a href="/vt/<%= v.slug %>" class="is-small"><%= v.displayName %></a><%= (i === vtubers.length - 2 ? (vtubers.length > 2 ? ', and ' : ' and ') : (i === vtubers.length - 1 ? '' : ', ')) %>
|
||||||
|
<% }) %>
|
||||||
|
<% } else { %>
|
||||||
|
<span>Unknown</span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td style="width: 160px;">
|
||||||
|
<% if (vod.thumbnail) { %>
|
||||||
|
<figure class="image is-3by2">
|
||||||
|
<img src="/api/files/<%= vod.collectionId %>/<%= vod.id %>/<%= vod.thumbnail %>" alt="Thumbnail" style="width: 120px; border-radius: 8px;">
|
||||||
|
</figure>
|
||||||
|
<% } else { %>
|
||||||
|
<span>No thumbnail</span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<% if (data.vods.totalPages > 1) { %>
|
||||||
|
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||||
|
<% const currentPage = data.vods.page; %>
|
||||||
|
<% const totalPages = data.vods.totalPages; %>
|
||||||
|
|
||||||
|
<!-- Previous Page -->
|
||||||
|
<% if (currentPage > 1) { %>
|
||||||
|
<a class="pagination-previous" href="?page=<%= currentPage - 1 %>&perPage=<%= data.vods.perPage %>">Previous</a>
|
||||||
|
<% } else { %>
|
||||||
|
<a class="pagination-previous" disabled>Previous</a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Next Page -->
|
||||||
|
<% if (currentPage < totalPages) { %>
|
||||||
|
<a class="pagination-next" href="?page=<%= currentPage + 1 %>&perPage=<%= data.vods.perPage %>">Next</a>
|
||||||
|
<% } else { %>
|
||||||
|
<a class="pagination-next" disabled>Next</a>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Page Numbers -->
|
||||||
|
<ul class="pagination-list">
|
||||||
|
<% for (let i = 1; i <= totalPages; i++) { %>
|
||||||
|
<li>
|
||||||
|
<a class="pagination-link <%= i === currentPage ? 'is-current' : '' %>" href="?page=<%= i %>&perPage=<%= data.vods.perPage %>">
|
||||||
|
<%= i %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } else { %>
|
||||||
|
<p>No VODs available.</p>
|
||||||
|
<% } %>
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<div id="vtuber" class="">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
|
|
||||||
<!-- VTuber Image -->
|
|
||||||
<figure class="image is-128x128">
|
|
||||||
<img src="/api/files/vtubers/<%= data.vtuber?.id %>/<%= data.vtuber?.image %>?thumb=128x128" alt="<%= data.vtuber?.get?.('displayName') || 'VTuber' %>" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<!-- VTuber Name -->
|
|
||||||
<span class="title is-6">
|
|
||||||
<%= data.vtuber?.displayName || data.vtuber?.get?.('displayName') || 'Unknown VTuber' %>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h3 class="title is-4 mb-4">VODs</h3>
|
|
||||||
|
|
||||||
<%
|
|
||||||
const vods = data.vtuber.get('expand')?.vods || [];
|
|
||||||
if (vods.length > 0) {
|
|
||||||
%>
|
|
||||||
<div class="columns is-multiline">
|
|
||||||
<% for (const vod of vods) { %>
|
|
||||||
<div class="column is-one-quarter-desktop is-half-tablet is-full-mobile">
|
|
||||||
<a href="/vods/<%= vod.get('id') %>" class="box has-text-centered">
|
|
||||||
|
|
||||||
<!-- Thumbnail -->
|
|
||||||
<% if (vod.get('thumbnail')) { %>
|
|
||||||
<figure class="image is-16by9 mb-2">
|
|
||||||
<img src="/api/files/vods/<%= vod.get('id') %>/<%= vod.get('thumbnail') %>?thumb=400x225" alt="VOD thumbnail for <%= vod.get('id') %>" />
|
|
||||||
</figure>
|
|
||||||
<% } else { %>
|
|
||||||
<div class="has-background-grey-lighter py-6 mb-2">
|
|
||||||
<span class="has-text-grey">No thumbnail</span>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- Title / ID -->
|
|
||||||
<p class="is-size-6 has-text-weight-semibold">
|
|
||||||
<%= vod.get('title') || vod.get('id') %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Date -->
|
|
||||||
<% if (vod.get('streamDate')) { %>
|
|
||||||
<p class="is-size-7 has-text-grey">
|
|
||||||
<%= new Date(vod.get('streamDate')).toLocaleDateString() %>
|
|
||||||
</p>
|
|
||||||
<% } %>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
<% } else { %>
|
|
||||||
<p>No VODs available for this VTuber.</p>
|
|
||||||
<% } %>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
48
services/pocketbase/pb_migrations/1762500151_updated_vods.js
Normal file
48
services/pocketbase/pb_migrations/1762500151_updated_vods.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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);
|
||||||
|
})
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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);
|
||||||
|
})
|
||||||
20
services/pocketbase/pb_migrations/1762622017_updated_vods.js
Normal file
20
services/pocketbase/pb_migrations/1762622017_updated_vods.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
20
services/pocketbase/pb_migrations/1762622042_updated_vods.js
Normal file
20
services/pocketbase/pb_migrations/1762622042_updated_vods.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
22
services/pocketbase/pb_migrations/1762622148_updated_vods.js
Normal file
22
services/pocketbase/pb_migrations/1762622148_updated_vods.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
22
services/pocketbase/pb_migrations/1762622255_updated_vods.js
Normal file
22
services/pocketbase/pb_migrations/1762622255_updated_vods.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -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);
|
||||||
|
});
|
||||||
@ -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()
|
||||||
@ -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()
|
||||||
@ -143,7 +143,7 @@
|
|||||||
"streamDate": "2025-10-29T08:16:00.000Z",
|
"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",
|
"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",
|
"sourceVideo": "content/projektmelody-chaturbate-2025-10-29.mp4",
|
||||||
"thumbnail": "content/projektmelody-chaturbate-2025-10-29.mp4",
|
"thumbnail": "content/js.png",
|
||||||
"ipfsCid": null,
|
"ipfsCid": null,
|
||||||
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2025-10-29.mp4",
|
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2025-10-29.mp4",
|
||||||
"muxAssetId": "ET8FXN00guyrpugug00dSMU00m4odQayzhVT3zAHpAMWo00",
|
"muxAssetId": "ET8FXN00guyrpugug00dSMU00m4odQayzhVT3zAHpAMWo00",
|
||||||
@ -183,7 +183,7 @@
|
|||||||
"streamDate": "2023-01-02T07:03:49.000Z",
|
"streamDate": "2023-01-02T07:03:49.000Z",
|
||||||
"notes": "This VOD is not original HD quality. We are searching for a better version.",
|
"notes": "This VOD is not original HD quality. We are searching for a better version.",
|
||||||
"sourceVideo": "content/projektmelody-chaturbate-2023-01-01.mp4",
|
"sourceVideo": "content/projektmelody-chaturbate-2023-01-01.mp4",
|
||||||
"thumbnail": "content/projektmelody-chaturbate-2023-01-01.mp4",
|
"thumbnail": "content/js.png",
|
||||||
"ipfsCid": "bafybeie3oynhomwdwvkdwgef2viii7kccpegnk6zvfprl4fyz6p52qwh6u",
|
"ipfsCid": "bafybeie3oynhomwdwvkdwgef2viii7kccpegnk6zvfprl4fyz6p52qwh6u",
|
||||||
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2023-01-01.mp4",
|
"videoSrcB2": "https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2023-01-01.mp4",
|
||||||
"muxAssetId": null,
|
"muxAssetId": null,
|
||||||
|
|||||||
73
services/pocketbase/utils/sign-url.js
Normal file
73
services/pocketbase/utils/sign-url.js
Normal file
@ -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 };
|
||||||
Loading…
x
Reference in New Issue
Block a user