diff --git a/server/chat-plugins/the-studio.ts b/server/chat-plugins/the-studio.ts index 590db058ab830..4620e1c86687a 100644 --- a/server/chat-plugins/the-studio.ts +++ b/server/chat-plugins/the-studio.ts @@ -9,7 +9,7 @@ import {FS} from '../../lib/fs'; import {Net} from '../../lib/net'; import {Utils} from '../../lib/utils'; -import {YouTube} from './youtube'; +import {YouTube, VideoData} from './youtube'; const LASTFM_DB = 'config/chat-plugins/lastfm.json'; const RECOMMENDATIONS = 'config/chat-plugins/the-studio.json'; @@ -25,6 +25,7 @@ interface Recommendation { artist: string; title: string; url: string; + videoInfo: VideoData | null; description: string; tags: string[]; userData: { @@ -223,7 +224,7 @@ class RecommendationsInterface { } } - add( + async add( artist: string, title: string, url: string, description: string, username: string, tags: string[], avatar?: string ) { @@ -237,10 +238,11 @@ class RecommendationsInterface { throw new Chat.ErrorMessage(`Please provide a valid YouTube link.`); } url = url.split('&')[0]; + const videoInfo = await YouTube.getVideoData(url); this.checkTags(tags); // JUST in case if (!recommendations.saved) recommendations.saved = []; - const rec: Recommendation = {artist, title, url, description, tags, userData: {name: username}, likes: 0}; + const rec: Recommendation = {artist, title, videoInfo, url, description, tags, userData: {name: username}, likes: 0}; if (!rec.tags.map(toID).includes(toID(username))) rec.tags.push(username); if (!rec.tags.map(toID).includes(toID(artist))) rec.tags.push(artist); if (avatar) rec.userData.avatar = avatar; @@ -262,7 +264,7 @@ class RecommendationsInterface { saveRecommendations(); } - suggest( + async suggest( artist: string, title: string, url: string, description: string, username: string, tags: string[], avatar?: string ) { @@ -279,8 +281,9 @@ class RecommendationsInterface { throw new Chat.ErrorMessage(`Please provide a valid YouTube link.`); } url = url.split('&')[0]; + const videoInfo = await YouTube.getVideoData(url); this.checkTags(tags); - const rec: Recommendation = {artist, title, url, description, tags, userData: {name: username}, likes: 0}; + const rec: Recommendation = {artist, title, videoInfo, url, description, tags, userData: {name: username}, likes: 0}; if (!rec.tags.map(toID).includes(toID(username))) rec.tags.push(username); if (!rec.tags.map(toID).includes(toID(artist))) rec.tags.push(artist); if (avatar) rec.userData.avatar = avatar; @@ -313,18 +316,16 @@ class RecommendationsInterface { } async render(rec: Recommendation, suggested = false) { - let videoInfo = null; - try { - videoInfo = await YouTube.getVideoData(rec.url); - } catch (e) { - throw new Chat.ErrorMessage(`Error while fetching recommendation URL: ${e.message}`); - } let buf = ``; buf += `
`; buf += ``; - if (videoInfo) { - buf += ``; + if (rec.videoInfo === undefined) { + rec.videoInfo = await YouTube.getVideoData(rec.videoInfo); + saveRecommendations(); + } + if (rec.videoInfo) { + buf += ``; } buf += Utils.html`

`; - buf += `${!suggested ? `${Chat.count(rec.likes, "points")} | ` : ``}${videoInfo.views} views

`; + buf += `${!suggested ? `${Chat.count(rec.likes, "points")} | ` : ``}${rec.videoInfo.views} views
${rec.artist} - ${rec.title}`; const tags = rec.tags.map(x => Utils.escapeHTML(x)) @@ -335,7 +336,7 @@ class RecommendationsInterface { if (rec.description) { buf += `
Description: ${Utils.escapeHTML(rec.description)}`; } - if (!videoInfo && !suggested) { + if (!rec.videoInfo && !suggested) { buf += `
Score: ${Chat.count(rec.likes, "points")}`; } if (!rec.userData.avatar) { @@ -434,6 +435,8 @@ export const commands: ChatCommands = { ], async lastfm(target, room, user) { + this.checkChat(); + if (!user.autoconfirmed) return this.errorReply(`You cannot use this command while not autoconfirmed.`); this.runBroadcast(true); this.splitTarget(target, true); const username = LastFM.getAccountName(target ? target : user.name); @@ -448,7 +451,8 @@ export const commands: ChatCommands = { async track(target, room, user) { if (!target) return this.parse('/help track'); - this.checkChat(target); + this.checkChat(); + if (!user.autoconfirmed) return this.errorReply(`You cannot use this command while not autoconfirmed.`); const [track, artist] = this.splitOne(target); if (!track) return this.parse('/help track'); this.runBroadcast(true); @@ -459,7 +463,7 @@ export const commands: ChatCommands = { ], addrec: 'addrecommendation', - addrecommendation(target, room, user) { + async addrecommendation(target, room, user) { room = this.requireRoom('thestudio' as RoomID); this.checkCan('show', null, room); const [artist, title, url, description, ...tags] = target.split('|').map(x => x.trim()); @@ -468,7 +472,7 @@ export const commands: ChatCommands = { } const cleansedTags = tags.map(x => x.trim()); - Recs.add(artist, title, url, description, user.name, cleansedTags, String(user.avatar)); + await Recs.add(artist, title, url, description, user.name, cleansedTags, String(user.avatar)); this.privateModAction(`${user.name} added a recommendation for '${title}' by ${artist}.`); this.modlog(`RECOMMENDATION`, null, `add: '${toID(title)}' by ${toID(artist)}`); @@ -507,18 +511,19 @@ export const commands: ChatCommands = { return this.parse('/help suggestrecommendation'); } this.checkChat(target); + if (!user.autoconfirmed) return this.errorReply(`You cannot use this command while not autoconfirmed.`); const [artist, title, url, description, ...tags] = target.split('|').map(x => x.trim()); if (!(artist && title && url && description && tags?.length)) { return this.parse(`/help suggestrecommendation`); } const cleansedTags = tags.map(x => x.trim()); - Recs.suggest(artist, title, url, description, user.name, cleansedTags, String(user.avatar)); + await Recs.suggest(artist, title, url, description, user.name, cleansedTags, String(user.avatar)); this.sendReply(`Your suggestion for '${title}' by ${artist} has been submitted.`); const html = await Recs.render({ artist, title, url, description, userData: {name: user.name, avatar: String(user.avatar)}, - tags: cleansedTags, likes: 0, + tags: cleansedTags, likes: 0, videoInfo: null, }, true); room.sendRankedUsers(`|html|${html}`, '%'); }, diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 5cab33e8a9c89..4addaf4014172 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -14,6 +14,7 @@ const ROOT = 'https://www.googleapis.com/youtube/v3/'; const STORAGE_PATH = 'config/chat-plugins/youtube.json'; export const videoDataCache: Map = Chat.oldPlugins.youtube?.videoDataCache || new Map(); +export const searchDataCache: Map = Chat.oldPlugins.youtube?.searchDataCache || new Map(); interface ChannelEntry { name: string; @@ -27,7 +28,7 @@ interface ChannelEntry { category?: string; } -interface VideoData { +export interface VideoData { id: string; title: string; date: string; @@ -240,14 +241,20 @@ export class YoutubeInterface { return FS(STORAGE_PATH).writeUpdate(() => JSON.stringify(this.data)); } async searchVideo(name: string, limit?: number): Promise { + const cached = searchDataCache.get(toID(name)); + if (cached) { + return cached.slice(0, limit); + } const raw = await Net(`${ROOT}search`).get({ query: { part: 'snippet', q: name, - key: Config.youtubeKey, order: 'relevance', maxResults: limit || 10, + key: Config.youtubeKey, order: 'relevance', }, }); const result = JSON.parse(raw); - return result.items?.map((item: AnyObject) => item?.id?.videoId).filter(Boolean); + const resultArray = result.items?.map((item: AnyObject) => item?.id?.videoId).filter(Boolean); + searchDataCache.set(toID(name), resultArray); + return resultArray.slice(0, limit); } async searchChannel(name: string, limit = 10): Promise { const raw = await Net(`${ROOT}search`).get({