{ "version": 3, "sources": ["../../../../server/chat-plugins/the-studio.ts"], "sourcesContent": ["/**\n * The Studio room chat-plugin.\n * Supports scrobbling and searching for music from last.fm.\n * Also supports storing and suggesting recommendations.\n * Written by Kris, loosely based on the concept from bumbadadabum.\n * @author Kris\n */\n\nimport {FS, Net, Utils} from '../../lib';\nimport {YouTube, VideoData} from './youtube';\n\nconst LASTFM_DB = 'config/chat-plugins/lastfm.json';\nconst RECOMMENDATIONS = 'config/chat-plugins/the-studio.json';\nconst API_ROOT = 'http://ws.audioscrobbler.com/2.0/';\nconst DEFAULT_IMAGES = [\n\t'https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png',\n\t'https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png',\n\t'https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png',\n\t'https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png',\n];\n\ninterface Recommendation {\n\tartist: string;\n\ttitle: string;\n\turl: string;\n\tvideoInfo: VideoData | null;\n\tdescription: string;\n\ttags: string[];\n\tuserData: {\n\t\tname: string,\n\t\tavatar?: string,\n\t};\n\tlikes: number;\n\tliked?: {\n\t\tips: string[],\n\t\tuserids: string[],\n\t};\n}\n\ninterface Recommendations {\n\tsuggested: Recommendation[];\n\tsaved: Recommendation[];\n}\n\nconst lastfm: {[userid: string]: string} = JSON.parse(FS(LASTFM_DB).readIfExistsSync() || \"{}\");\nconst recommendations: Recommendations = JSON.parse(FS(RECOMMENDATIONS).readIfExistsSync() || \"{}\");\n\nif (!recommendations.saved) recommendations.saved = [];\nif (!recommendations.suggested) recommendations.suggested = [];\nsaveRecommendations();\n\nfunction updateRecTags() {\n\tfor (const rec of recommendations.saved) {\n\t\tif (!rec.tags.map(toID).includes(toID(rec.artist))) rec.tags.push(rec.artist);\n\t\tif (!rec.tags.map(toID).includes(toID(rec.userData.name))) rec.tags.push(rec.userData.name);\n\t}\n\tfor (const rec of recommendations.suggested) {\n\t\tif (!rec.tags.map(toID).includes(toID(rec.artist))) rec.tags.push(rec.artist);\n\t\tif (!rec.tags.map(toID).includes(toID(rec.userData.name))) rec.tags.push(rec.userData.name);\n\t}\n\tsaveRecommendations();\n}\n\nupdateRecTags();\n\nfunction saveLastFM() {\n\tFS(LASTFM_DB).writeUpdate(() => JSON.stringify(lastfm));\n}\nfunction saveRecommendations() {\n\tFS(RECOMMENDATIONS).writeUpdate(() => JSON.stringify(recommendations));\n}\n\nexport class LastFMInterface {\n\tasync getScrobbleData(username: string, displayName?: string) {\n\t\tthis.checkHasKey();\n\t\tconst accountName = this.getAccountName(username);\n\t\tlet raw;\n\t\ttry {\n\t\t\traw = await Net(API_ROOT).get({\n\t\t\t\tquery: {\n\t\t\t\t\tmethod: 'user.getRecentTracks', user: accountName,\n\t\t\t\t\tlimit: 1, api_key: Config.lastfmkey, format: 'json',\n\t\t\t\t},\n\t\t\t});\n\t\t} catch {\n\t\t\tthrow new Chat.ErrorMessage(`No scrobble data found.`);\n\t\t}\n\t\tconst res = JSON.parse(raw);\n\t\tif (res.error) {\n\t\t\tthrow new Chat.ErrorMessage(`${res.message}.`);\n\t\t}\n\t\tif (!res?.recenttracks?.track?.length) throw new Chat.ErrorMessage(`last.fm account not found.`);\n\t\tconst track = res.recenttracks.track[0];\n\t\tlet buf = `
| ${Utils.escapeHTML(displayName || accountName)}`;\n\t\t\tif (track['@attr']?.nowplaying) {\n\t\t\t\tbuf += ` is currently listening to:`;\n\t\t\t} else {\n\t\t\t\tbuf += ` was last seen listening to:`;\n\t\t\t}\n\t\t\tbuf += ` `;\n\t\t\tconst trackName = `${track.artist?.['#text'] ? `${track.artist['#text']} - ` : ''}${track.name}`;\n\t\t\tlet videoIDs: string[] | undefined;\n\t\t\ttry {\n\t\t\t\tvideoIDs = await YouTube.searchVideo(trackName, 1);\n\t\t\t} catch (e: any) {\n\t\t\t\tthrow new Chat.ErrorMessage(`Error while fetching video data: ${e.message}`);\n\t\t\t}\n\t\t\tif (!videoIDs?.length) {\n\t\t\t\tthrow new Chat.ErrorMessage(`Something went wrong with the YouTube API.`);\n\t\t\t}\n\t\t\tbuf += `${Utils.escapeHTML(trackName)}`;\n\t\t\tbuf += ` |
| `;\n\t\t\tconst obj = req.results.trackmatches.track[0];\n\t\t\tconst trackName = obj.name || \"Untitled\";\n\t\t\tconst artistName = obj.artist || \"Unknown Artist\";\n\t\t\tconst searchName = `${artistName} - ${trackName}`;\n\t\t\tif (obj.image?.length) {\n\t\t\t\tconst img = obj.image;\n\t\t\t\tconst imageIndex = img.length >= 3 ? 2 : img.length - 1;\n\t\t\t\tif (img[imageIndex]['#text'] && !DEFAULT_IMAGES.includes(img[imageIndex]['#text'])) {\n\t\t\t\t\tbuf += ` | `;\n\t\t\tconst artistUrl = obj.url.split('_/')[0];\n\t\t\tbuf += `${artistName} - ${trackName} `;\n\t\t\tlet videoIDs: string[] | undefined;\n\t\t\ttry {\n\t\t\t\tvideoIDs = await YouTube.searchVideo(searchName, 1);\n\t\t\t} catch (e: any) {\n\t\t\t\tthrow new Chat.ErrorMessage(`Error while fetching video data: ${e.message}`);\n\t\t\t}\n\t\t\tif (!videoIDs?.length) {\n\t\t\t\tbuf += searchName;\n\t\t\t} else {\n\t\t\t\tbuf += `YouTube link`;\n\t\t\t}\n\t\t\tbuf += ` |
`;\n\t\t\tbuf += `${!suggested ? `${Chat.count(rec.likes, \"points\")} | ` : ``}${rec.videoInfo.views} views | `;\n\t\t}\n\t\tbuf += Utils.html`${rec.artist} - ${rec.title}`;\n\t\tconst tags = rec.tags.map(x => Utils.escapeHTML(x))\n\t\t\t.filter(x => toID(x) !== toID(rec.userData.name) && toID(x) !== toID(rec.artist));\n\t\tif (tags.length) {\n\t\t\tbuf += ` Tags: ${tags.join(', ')}`;\n\t\t}\n\t\tif (rec.description) {\n\t\t\tbuf += ` Description: ${Utils.escapeHTML(rec.description)}`;\n\t\t}\n\t\tif (!rec.videoInfo && !suggested) {\n\t\t\tbuf += ` Score: ${Chat.count(rec.likes, \"points\")}`;\n\t\t}\n\t\tif (!rec.userData.avatar) {\n\t\t\tbuf += ` Recommended by: ${rec.userData.name}`;\n\t\t}\n\t\tbuf += ` `;\n\t\tif (suggested) {\n\t\t\tbuf += Utils.html` | `;\n\t\t\tbuf += Utils.html``;\n\t\t} else {\n\t\t\tbuf += Utils.html``;\n\t\t}\n\t\tbuf += ` | `;\n\t\tif (rec.userData.avatar) {\n\t\t\tbuf += ``;\n\t\t\tconst isCustom = rec.userData.avatar.startsWith('#');\n\t\t\tbuf += ` Recommended by:`;\n\t\t\tbuf += ` ${rec.userData.name} | `;\n\t\t} else {\n\t\t\tbuf += `Recommended by: ${rec.userData.name} | `;\n\t\t}\n\t\tbuf += `