Compare commits

..

2 Commits

Author SHA1 Message Date
ff6ff97889 use mqdefault thumbnail 2024-06-17 15:37:16 -07:00
1e743aa59a potpack 2024-06-17 15:33:03 -07:00
5 changed files with 66 additions and 93 deletions

View File

@ -1,6 +1,6 @@
import { searchYouTubeVideos, continueYouTubeVideoSearch, getYouTubePlaylist, continueYouTubePlaylist } from "./simpleYoutubeSearch.js"; import { searchYouTubeVideos, continueYouTubeVideoSearch, getYouTubePlaylist, continueYouTubePlaylist } from "./simpleYoutubeSearch.js";
import { putVrcUrl } from "./vrcurl.js"; import { putVrcUrl } from "./vrcurl.js";
import { makeImageSheetVrcUrl, iconWidth, iconHeight } from "./imagesheet.js"; import { makeImageSheetVrcUrl } from "./imagesheet.js";
import { getTrending } from "./trending.js"; import { getTrending } from "./trending.js";
var cache = {}; var cache = {};
@ -52,27 +52,25 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
var {videos, continuationData} = playlistId ? await getYouTubePlaylist(playlistId) : await searchYouTubeVideos(query); var {videos, continuationData} = playlistId ? await getYouTubePlaylist(playlistId) : await searchYouTubeVideos(query);
} }
var images = [];
if (options.thumbnails) { if (options.thumbnails) {
var thumbnailUrls = videos.map(video => video.thumbnail.url); videos.forEach(video => video.thumbnail.url && images.push(video.thumbnail));
var smallestThumbnail = videos.map(video => video.thumbnail).reduce((smallest, selected) => selected.height < smallest.height ? selected : smallest);
} }
if (options.icons) { if (options.icons) {
var iconUrls = new Set(); let iconUrls = new Set();
for (let video of videos) { videos.forEach(video => video.channel.iconUrl && iconUrls.add(video.channel.iconUrl));
iconUrls.add(video.channel.iconUrl); iconUrls.forEach(url => images.push({
} width: 68,//todo pass from yt data not hardcode
iconUrls = [...iconUrls]; height: 68,
url
}));
} }
if (thumbnailUrls?.length || iconUrls?.length) { if (images.length) {
try { try {
var {vrcurl: imagesheet_vrcurl, thumbnails, icons} = await makeImageSheetVrcUrl(pool, { var {vrcurl: imagesheet_vrcurl} = await makeImageSheetVrcUrl(pool, images);
thumbnailUrls,
iconUrls,
thumbnailWidth: smallestThumbnail.width,
thumbnailHeight: smallestThumbnail.height
});
data.imagesheet_vrcurl = imagesheet_vrcurl; data.imagesheet_vrcurl = imagesheet_vrcurl;
} catch (error) { } catch (error) {
console.error(error.stack); console.error(error.stack);
@ -81,24 +79,20 @@ async function VRCYoutubeSearch(pool, query, options = {}) {
for (let video of videos) { for (let video of videos) {
video.vrcurl = await putVrcUrl(pool, {type: "redirect", url: `https://www.youtube.com/watch?v=${video.id}`}); video.vrcurl = await putVrcUrl(pool, {type: "redirect", url: `https://www.youtube.com/watch?v=${video.id}`});
if (thumbnails?.length) { let thumbnail = images.find(image => image.url == video.thumbnail.url);
let thumbnail = thumbnails.find(x => x.url == video.thumbnail.url); video.thumbnail = thumbnail ? {
video.thumbnail = {
x: thumbnail?.x, x: thumbnail?.x,
y: thumbnail?.y, y: thumbnail?.y,
width: smallestThumbnail?.width, width: thumbnail?.width,
height: smallestThumbnail?.height height: thumbnail?.height
}; } : undefined;
} let icon = images.find(image => image.url == video.channel.iconUrl);
if (icons?.length) { video.channel.icon = icon ? {
let icon = icons.find(x => x.url == video.channel.iconUrl);
video.channel.icon = {
x: icon?.x, x: icon?.x,
y: icon?.y, y: icon?.y,
width: iconWidth, width: icon?.width,
height: iconHeight height: icon?.height
}; } : undefined;
}
if (options.captions) { if (options.captions) {
video.captions_vrcurl = await putVrcUrl(pool, {type: "captions", videoId: video.id}); video.captions_vrcurl = await putVrcUrl(pool, {type: "captions", videoId: video.id});
} }

View File

@ -1,75 +1,42 @@
import { createCanvas, loadImage } from 'canvas'; import { createCanvas, loadImage } from 'canvas';
import potpack from 'potpack';
import { putVrcUrl } from './vrcurl.js'; import { putVrcUrl } from './vrcurl.js';
var store = {}; var store = {};
async function createImageSheet(images /*[{width, height, url}]*/) {
const maxSheetWidth = 2048; images.forEach(image => {
const maxSheetHeight = 2048; image.w = image.width;
export const iconWidth = 68; image.h = image.height;
export const iconHeight = 68; });
//const maxIconRowLen = Math.floor(maxSheetWidth / iconWidth); var {w, h, fill} = potpack(images);
const maxIconRowLen = 3; if (w > 2048) {
//const maxIconColLen = Math.floor(maxSheetHeight / iconHeight); console.warn("Imagesheet exceeded max width");
w = 2048;
async function createImageSheet({thumbnailUrls = [], iconUrls = [], thumbnailWidth = 360, thumbnailHeight = 202}) { }
if (h > 2048) {
const maxThumbnailRowLen = Math.floor(maxSheetWidth / thumbnailWidth); console.warn("Imagesheet exceeded max height");
//const maxThumbnailColLen = Math.floor(maxSheetHeight / thumbnailHeight); h = 2048;
}
var thumbnails = thumbnailUrls.map((url, index) => ({ var canvas = createCanvas(w, h);
x: index % maxThumbnailRowLen * thumbnailWidth,
y: Math.floor(index / maxThumbnailRowLen) * thumbnailHeight,
url
}));
const iconStartX = thumbnailWidth * Math.min(maxThumbnailRowLen, thumbnails.length);
var icons = iconUrls.map((url, index) => ({
x: iconStartX + index % maxIconRowLen * iconWidth,
y: Math.floor(index / maxIconRowLen) * iconHeight,
url
}));
const canvasWidth = Math.max(
Math.min(thumbnails.length, maxThumbnailRowLen) * thumbnailWidth,
iconStartX + Math.min(icons.length, maxIconRowLen) * iconWidth
);
const canvasHeight = Math.max(thumbnails.length ? thumbnails.at(-1).y + thumbnailHeight : 0, icons.length ? icons.at(-1)?.y + iconHeight : 0);
var canvas = createCanvas(Math.min(maxSheetWidth, canvasWidth), Math.min(maxSheetHeight, canvasHeight));
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
var promises = []; await Promise.all(images.map(({x, y, w, h, url}) => (async function(){
if (thumbnails.length) {
promises = promises.concat(thumbnails.map(({x, y, url}) => (async function(){
if (!url) return; if (!url) return;
var image = await loadImage(url); var image = await loadImage(url);
ctx.drawImage(image, x, y, thumbnailWidth, thumbnailHeight); ctx.drawImage(image, x, y, w, h);
})().catch(error => console.error(error.stack)))); })().catch(error => console.error(error.stack))));
}
if (icons.length) {
promises = promises.concat(icons.map(({x, y, url}) => (async function(){
if (!url) return;
var image = await loadImage(url);
ctx.drawImage(image, x, y, iconWidth, iconHeight);
})().catch(error => console.error(error.stack))));
}
await Promise.all(promises);
return { return {
imagesheet: canvas.toBuffer("image/png"), imagesheet: canvas.toBuffer("image/png"),
thumbnails, icons images
}; };
} }
export async function makeImageSheetVrcUrl(pool, images) {
export async function makeImageSheetVrcUrl(pool, opts) {
var num = await putVrcUrl(pool, {type: "imagesheet"}); var num = await putVrcUrl(pool, {type: "imagesheet"});
var key = `${pool}:${num}`; var key = `${pool}:${num}`;
var promise = createImageSheet(opts); var promise = createImageSheet(images);
store[key] = promise; store[key] = promise;
promise.then(() => { promise.then(() => {
setTimeout(() => { setTimeout(() => {

8
package-lock.json generated
View File

@ -11,7 +11,8 @@
"fast-xml-parser": "^4.3.4", "fast-xml-parser": "^4.3.4",
"keyv": "^4.5.4", "keyv": "^4.5.4",
"koa": "^2.14.2", "koa": "^2.14.2",
"koa-send": "^5.0.1" "koa-send": "^5.0.1",
"potpack": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@ -1262,6 +1263,11 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
}, },
"node_modules/potpack": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
},
"node_modules/promise-inflight": { "node_modules/promise-inflight": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",

View File

@ -6,7 +6,8 @@
"fast-xml-parser": "^4.3.4", "fast-xml-parser": "^4.3.4",
"keyv": "^4.5.4", "keyv": "^4.5.4",
"koa": "^2.14.2", "koa": "^2.14.2",
"koa-send": "^5.0.1" "koa-send": "^5.0.1",
"potpack": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"

View File

@ -40,7 +40,12 @@ export function parseVideoRendererData(data) {
description: data.detailedMetadataSnippets?.[0]?.snippetText?.runs?.concatRunsText() description: data.detailedMetadataSnippets?.[0]?.snippetText?.runs?.concatRunsText()
|| data.descriptionSnippet?.runs?.concatRunsText(), || data.descriptionSnippet?.runs?.concatRunsText(),
//thumbnailUrl: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138))?.url || data.thumbnail?.thumbnails?.[0]?.url, //thumbnailUrl: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138))?.url || data.thumbnail?.thumbnails?.[0]?.url,
thumbnail: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138)) || data.thumbnail?.thumbnails?.[0], //thumbnail: data.thumbnail?.thumbnails?.find(x => (x.width == 360 && x.height == 202) || (x.width == 246 && x.height == 138)) || data.thumbnail?.thumbnails?.[0],
thumbnail: {
url: `https://i.ytimg.com/vi/${data.videoId}/mqdefault.jpg`,
width: 320,
height: 180
},
uploaded: data.publishedTimeText?.simpleText || data.videoInfo?.runs?.[2]?.text, uploaded: data.publishedTimeText?.simpleText || data.videoInfo?.runs?.[2]?.text,
lengthText: data.lengthText?.simpleText, lengthText: data.lengthText?.simpleText,
longLengthText: data.lengthText?.accessibility?.accessibilityData?.label, longLengthText: data.lengthText?.accessibility?.accessibilityData?.label,