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