stations.js 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  1. 'use strict';
  2. const async = require('async'),
  3. request = require('request'),
  4. config = require('config'),
  5. _ = require('underscore')._;
  6. const hooks = require('./hooks');
  7. const moduleManager = require("../../index");
  8. const db = moduleManager.modules["db"];
  9. const cache = moduleManager.modules["cache"];
  10. const notifications = moduleManager.modules["notifications"];
  11. const utils = moduleManager.modules["utils"];
  12. const logger = moduleManager.modules["logger"];
  13. const stations = moduleManager.modules["stations"];
  14. const songs = moduleManager.modules["songs"];
  15. let userList = {};
  16. let usersPerStation = {};
  17. let usersPerStationCount = {};
  18. setInterval(() => {
  19. let stationsCountUpdated = [];
  20. let stationsUpdated = [];
  21. let oldUsersPerStation = usersPerStation;
  22. usersPerStation = {};
  23. let oldUsersPerStationCount = usersPerStationCount;
  24. usersPerStationCount = {};
  25. async.each(Object.keys(userList), function(socketId, next) {
  26. utils.socketFromSession(socketId).then((socket) => {
  27. let stationId = userList[socketId];
  28. if (!socket || Object.keys(socket.rooms).indexOf(`station.${stationId}`) === -1) {
  29. if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
  30. if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
  31. delete userList[socketId];
  32. return next();
  33. }
  34. if (!usersPerStationCount[stationId]) usersPerStationCount[stationId] = 0;
  35. usersPerStationCount[stationId]++;
  36. if (!usersPerStation[stationId]) usersPerStation[stationId] = [];
  37. async.waterfall([
  38. (next) => {
  39. if (!socket.session || !socket.session.sessionId) return next('No session found.');
  40. cache.hget('sessions', socket.session.sessionId, next);
  41. },
  42. (session, next) => {
  43. if (!session) return next('Session not found.');
  44. db.models.user.findOne({_id: session.userId}, next);
  45. },
  46. (user, next) => {
  47. if (!user) return next('User not found.');
  48. if (usersPerStation[stationId].indexOf(user.username) !== -1) return next('User already in the list.');
  49. next(null, user.username);
  50. }
  51. ], (err, username) => {
  52. if (!err) {
  53. usersPerStation[stationId].push(username);
  54. }
  55. next();
  56. });
  57. });
  58. //TODO Code to show users
  59. }, (err) => {
  60. for (let stationId in usersPerStationCount) {
  61. if (oldUsersPerStationCount[stationId] !== usersPerStationCount[stationId]) {
  62. if (stationsCountUpdated.indexOf(stationId) === -1) stationsCountUpdated.push(stationId);
  63. }
  64. }
  65. for (let stationId in usersPerStation) {
  66. if (_.difference(usersPerStation[stationId], oldUsersPerStation[stationId]).length > 0 || _.difference(oldUsersPerStation[stationId], usersPerStation[stationId]).length > 0) {
  67. if (stationsUpdated.indexOf(stationId) === -1) stationsUpdated.push(stationId);
  68. }
  69. }
  70. stationsCountUpdated.forEach((stationId) => {
  71. //logger.info("UPDATE_STATION_USER_COUNT", `Updating user count of ${stationId}.`);
  72. cache.pub('station.updateUserCount', stationId);
  73. });
  74. stationsUpdated.forEach((stationId) => {
  75. //logger.info("UPDATE_STATION_USER_LIST", `Updating user list of ${stationId}.`);
  76. cache.pub('station.updateUsers', stationId);
  77. });
  78. //console.log("Userlist", usersPerStation);
  79. });
  80. }, 3000);
  81. cache.sub('station.updateUsers', stationId => {
  82. let list = usersPerStation[stationId] || [];
  83. utils.emitToRoom(`station.${stationId}`, "event:users.updated", list);
  84. });
  85. cache.sub('station.updateUserCount', stationId => {
  86. let count = usersPerStationCount[stationId] || 0;
  87. utils.emitToRoom(`station.${stationId}`, "event:userCount.updated", count);
  88. stations.getStation(stationId, async (err, station) => {
  89. if (station.privacy === 'public') utils.emitToRoom('home', "event:userCount.updated", stationId, count);
  90. else {
  91. let sockets = await utils.getRoomSockets('home');
  92. for (let socketId in sockets) {
  93. let socket = sockets[socketId];
  94. let session = sockets[socketId].session;
  95. if (session.sessionId) {
  96. cache.hget('sessions', session.sessionId, (err, session) => {
  97. if (!err && session) {
  98. db.models.user.findOne({_id: session.userId}, (err, user) => {
  99. if (user.role === 'admin') socket.emit("event:userCount.updated", stationId, count);
  100. else if (station.type === "community" && station.owner === session.userId) socket.emit("event:userCount.updated", stationId, count);
  101. });
  102. }
  103. });
  104. }
  105. }
  106. }
  107. })
  108. });
  109. cache.sub('station.queueLockToggled', data => {
  110. utils.emitToRoom(`station.${data.stationId}`, "event:queueLockToggled", data.locked)
  111. });
  112. cache.sub('station.updatePartyMode', data => {
  113. utils.emitToRoom(`station.${data.stationId}`, "event:partyMode.updated", data.partyMode);
  114. });
  115. cache.sub('privatePlaylist.selected', data => {
  116. utils.emitToRoom(`station.${data.stationId}`, "event:privatePlaylist.selected", data.playlistId);
  117. });
  118. cache.sub('station.pause', stationId => {
  119. stations.getStation(stationId, (err, station) => {
  120. utils.emitToRoom(`station.${stationId}`, "event:stations.pause", { pausedAt: station.pausedAt });
  121. });
  122. });
  123. cache.sub('station.resume', stationId => {
  124. stations.getStation(stationId, (err, station) => {
  125. utils.emitToRoom(`station.${stationId}`, "event:stations.resume", { timePaused: station.timePaused });
  126. });
  127. });
  128. cache.sub('station.queueUpdate', stationId => {
  129. stations.getStation(stationId, (err, station) => {
  130. if (!err) utils.emitToRoom(`station.${stationId}`, "event:queue.update", station.queue);
  131. });
  132. });
  133. cache.sub('station.voteSkipSong', stationId => {
  134. utils.emitToRoom(`station.${stationId}`, "event:song.voteSkipSong");
  135. });
  136. cache.sub('station.remove', stationId => {
  137. utils.emitToRoom(`station.${stationId}`, 'event:stations.remove');
  138. utils.emitToRoom('admin.stations', 'event:admin.station.removed', stationId);
  139. });
  140. cache.sub('station.create', stationId => {
  141. stations.initializeStation(stationId, async (err, station) => {
  142. station.userCount = usersPerStationCount[stationId] || 0;
  143. if (err) console.error(err);
  144. utils.emitToRoom('admin.stations', 'event:admin.station.added', station);
  145. // TODO If community, check if on whitelist
  146. if (station.privacy === 'public') utils.emitToRoom('home', "event:stations.created", station);
  147. else {
  148. let sockets = await utils.getRoomSockets('home');
  149. for (let socketId in sockets) {
  150. let socket = sockets[socketId];
  151. let session = sockets[socketId].session;
  152. if (session.sessionId) {
  153. cache.hget('sessions', session.sessionId, (err, session) => {
  154. if (!err && session) {
  155. db.models.user.findOne({_id: session.userId}, (err, user) => {
  156. if (user.role === 'admin') socket.emit("event:stations.created", station);
  157. else if (station.type === "community" && station.owner === session.userId) socket.emit("event:stations.created", station);
  158. });
  159. }
  160. });
  161. }
  162. }
  163. }
  164. });
  165. });
  166. module.exports = {
  167. /**
  168. * Get a list of all the stations
  169. *
  170. * @param session
  171. * @param cb
  172. * @return {{ status: String, stations: Array }}
  173. */
  174. index: (session, cb) => {
  175. async.waterfall([
  176. (next) => {
  177. cache.hgetall('stations', next);
  178. },
  179. (stations, next) => {
  180. let resultStations = [];
  181. for (let id in stations) {
  182. resultStations.push(stations[id]);
  183. }
  184. next(null, stations);
  185. },
  186. (stationsArray, next) => {
  187. let resultStations = [];
  188. async.each(stationsArray, (station, next) => {
  189. async.waterfall([
  190. (next) => {
  191. stations.canUserViewStation(station, session.userId, (err, exists) => {
  192. next(err, exists);
  193. });
  194. }
  195. ], (err, exists) => {
  196. station.userCount = usersPerStationCount[station._id] || 0;
  197. if (exists) resultStations.push(station);
  198. next();
  199. });
  200. }, () => {
  201. next(null, resultStations);
  202. });
  203. }
  204. ], async (err, stations) => {
  205. if (err) {
  206. err = await utils.getError(err);
  207. logger.error("STATIONS_INDEX", `Indexing stations failed. "${err}"`);
  208. return cb({'status': 'failure', 'message': err});
  209. }
  210. logger.success("STATIONS_INDEX", `Indexing stations successful.`, false);
  211. return cb({'status': 'success', 'stations': stations});
  212. });
  213. },
  214. /**
  215. * Verifies that a station exists
  216. *
  217. * @param session
  218. * @param stationName - the station name
  219. * @param cb
  220. */
  221. existsByName: (session, stationName, cb) => {
  222. async.waterfall([
  223. (next) => {
  224. stations.getStationByName(stationName, next);
  225. },
  226. (station, next) => {
  227. if (!station) return next(null, false);
  228. stations.canUserViewStation(station, session.userId, (err, exists) => {
  229. next(err, exists);
  230. });
  231. }
  232. ], async (err, exists) => {
  233. if (err) {
  234. err = await utils.getError(err);
  235. logger.error("STATION_EXISTS_BY_NAME", `Checking if station "${stationName}" exists failed. "${err}"`);
  236. return cb({'status': 'failure', 'message': err});
  237. }
  238. logger.success("STATION_EXISTS_BY_NAME", `Station "${stationName}" exists successfully.`/*, false*/);
  239. cb({status: 'success', exists});
  240. });
  241. },
  242. /**
  243. * Gets the official playlist for a station
  244. *
  245. * @param session
  246. * @param stationId - the station id
  247. * @param cb
  248. */
  249. getPlaylist: (session, stationId, cb) => {
  250. async.waterfall([
  251. (next) => {
  252. stations.getStation(stationId, next);
  253. },
  254. (station, next) => {
  255. stations.canUserViewStation(station, session.userId, (err, canView) => {
  256. if (err) return next(err);
  257. if (canView) return next(null, station);
  258. return next('Insufficient permissions.');
  259. });
  260. },
  261. (station, next) => {
  262. if (!station) return next('Station not found.');
  263. else if (station.type !== 'official') return next('This is not an official station.');
  264. else next();
  265. },
  266. (next) => {
  267. cache.hget('officialPlaylists', stationId, next);
  268. },
  269. (playlist, next) => {
  270. if (!playlist) return next('Playlist not found.');
  271. next(null, playlist);
  272. }
  273. ], async (err, playlist) => {
  274. if (err) {
  275. err = await utils.getError(err);
  276. logger.error("STATIONS_GET_PLAYLIST", `Getting playlist for station "${stationId}" failed. "${err}"`);
  277. return cb({ status: 'failure', message: err });
  278. } else {
  279. logger.success("STATIONS_GET_PLAYLIST", `Got playlist for station "${stationId}" successfully.`, false);
  280. cb({ status: 'success', data: playlist.songs });
  281. }
  282. });
  283. },
  284. /**
  285. * Joins the station by its name
  286. *
  287. * @param session
  288. * @param stationName - the station name
  289. * @param cb
  290. * @return {{ status: String, userCount: Integer }}
  291. */
  292. join: (session, stationName, cb) => {
  293. async.waterfall([
  294. (next) => {
  295. stations.getStationByName(stationName, next);
  296. },
  297. (station, next) => {
  298. if (!station) return next('Station not found.');
  299. stations.canUserViewStation(station, session.userId, (err, canView) => {
  300. if (err) return next(err);
  301. if (!canView) next("Not allowed to join station.");
  302. else next(null, station);
  303. });
  304. },
  305. (station, next) => {
  306. utils.socketJoinRoom(session.socketId, `station.${station._id}`);
  307. let data = {
  308. _id: station._id,
  309. type: station.type,
  310. currentSong: station.currentSong,
  311. startedAt: station.startedAt,
  312. paused: station.paused,
  313. timePaused: station.timePaused,
  314. pausedAt: station.pausedAt,
  315. description: station.description,
  316. displayName: station.displayName,
  317. privacy: station.privacy,
  318. locked: station.locked,
  319. partyMode: station.partyMode,
  320. owner: station.owner,
  321. privatePlaylist: station.privatePlaylist,
  322. theme: station.theme
  323. };
  324. userList[session.socketId] = station._id;
  325. next(null, data);
  326. },
  327. (data, next) => {
  328. data = JSON.parse(JSON.stringify(data));
  329. data.userCount = usersPerStationCount[data._id] || 0;
  330. data.users = usersPerStation[data._id] || [];
  331. if (!data.currentSong || !data.currentSong.title) return next(null, data);
  332. utils.socketJoinSongRoom(session.socketId, `song.${data.currentSong.songId}`);
  333. data.currentSong.skipVotes = data.currentSong.skipVotes.length;
  334. songs.getSongFromId(data.currentSong.songId, (err, song) => {
  335. if (!err && song) {
  336. data.currentSong.likes = song.likes;
  337. data.currentSong.dislikes = song.dislikes;
  338. } else {
  339. data.currentSong.likes = -1;
  340. data.currentSong.dislikes = -1;
  341. }
  342. next(null, data);
  343. });
  344. }
  345. ], async (err, data) => {
  346. if (err) {
  347. err = await utils.getError(err);
  348. logger.error("STATIONS_JOIN", `Joining station "${stationName}" failed. "${err}"`);
  349. return cb({'status': 'failure', 'message': err});
  350. }
  351. logger.success("STATIONS_JOIN", `Joined station "${data._id}" successfully.`);
  352. cb({status: 'success', data});
  353. });
  354. },
  355. /**
  356. * Toggles if a station is locked
  357. *
  358. * @param session
  359. * @param stationId - the station id
  360. * @param cb
  361. */
  362. toggleLock: hooks.ownerRequired((session, stationId, cb) => {
  363. async.waterfall([
  364. (next) => {
  365. stations.getStation(stationId, next);
  366. },
  367. (station, next) => {
  368. db.models.station.updateOne({ _id: stationId }, { $set: { locked: !station.locked} }, next);
  369. },
  370. (res, next) => {
  371. stations.updateStation(stationId, next);
  372. }
  373. ], async (err, station) => {
  374. if (err) {
  375. err = await utils.getError(err);
  376. logger.error("STATIONS_UPDATE_LOCKED_STATUS", `Toggling the queue lock for station "${stationId}" failed. "${err}"`);
  377. return cb({ status: 'failure', message: err });
  378. } else {
  379. logger.success("STATIONS_UPDATE_LOCKED_STATUS", `Toggled the queue lock for station "${stationId}" successfully to "${station.locked}".`);
  380. cache.pub('station.queueLockToggled', {stationId, locked: station.locked});
  381. return cb({ status: 'success', data: station.locked });
  382. }
  383. });
  384. }),
  385. /**
  386. * Votes to skip a station
  387. *
  388. * @param session
  389. * @param stationId - the station id
  390. * @param cb
  391. */
  392. voteSkip: hooks.loginRequired((session, stationId, cb) => {
  393. let skipVotes = 0;
  394. let shouldSkip = false;
  395. async.waterfall([
  396. (next) => {
  397. stations.getStation(stationId, next);
  398. },
  399. (station, next) => {
  400. if (!station) return next('Station not found.');
  401. stations.canUserViewStation(station, session.userId, (err, canView) => {
  402. if (err) return next(err);
  403. if (canView) return next(null, station);
  404. return next('Insufficient permissions.');
  405. });
  406. },
  407. (station, next) => {
  408. if (!station.currentSong) return next('There is currently no song to skip.');
  409. if (station.currentSong.skipVotes.indexOf(session.userId) !== -1) return next('You have already voted to skip this song.');
  410. next(null, station);
  411. },
  412. (station, next) => {
  413. db.models.station.updateOne({_id: stationId}, {$push: {"currentSong.skipVotes": session.userId}}, next)
  414. },
  415. (res, next) => {
  416. stations.updateStation(stationId, next);
  417. },
  418. (station, next) => {
  419. if (!station) return next('Station not found.');
  420. next(null, station);
  421. },
  422. (station, next) => {
  423. skipVotes = station.currentSong.skipVotes.length;
  424. utils.getRoomSockets(`station.${stationId}`).then(sockets => {
  425. next(null, sockets);
  426. }).catch(next);
  427. },
  428. (sockets, next) => {
  429. if (sockets.length <= skipVotes) shouldSkip = true;
  430. next();
  431. }
  432. ], async (err, station) => {
  433. if (err) {
  434. err = await utils.getError(err);
  435. logger.error("STATIONS_VOTE_SKIP", `Vote skipping station "${stationId}" failed. "${err}"`);
  436. return cb({'status': 'failure', 'message': err});
  437. }
  438. logger.success("STATIONS_VOTE_SKIP", `Vote skipping "${stationId}" successful.`);
  439. cache.pub('station.voteSkipSong', stationId);
  440. cb({ status: 'success', message: 'Successfully voted to skip the song.' });
  441. if (shouldSkip) stations.skipStation(stationId)();
  442. });
  443. }),
  444. /**
  445. * Force skips a station
  446. *
  447. * @param session
  448. * @param stationId - the station id
  449. * @param cb
  450. */
  451. forceSkip: hooks.ownerRequired((session, stationId, cb) => {
  452. async.waterfall([
  453. (next) => {
  454. stations.getStation(stationId, next);
  455. },
  456. (station, next) => {
  457. if (!station) return next('Station not found.');
  458. next();
  459. }
  460. ], async (err) => {
  461. if (err) {
  462. err = await utils.getError(err);
  463. logger.error("STATIONS_FORCE_SKIP", `Force skipping station "${stationId}" failed. "${err}"`);
  464. return cb({'status': 'failure', 'message': err});
  465. }
  466. notifications.unschedule(`stations.nextSong?id=${stationId}`);
  467. stations.skipStation(stationId)();
  468. logger.success("STATIONS_FORCE_SKIP", `Force skipped station "${stationId}" successfully.`);
  469. return cb({'status': 'success', 'message': 'Successfully skipped station.'});
  470. });
  471. }),
  472. /**
  473. * Leaves the user's current station
  474. *
  475. * @param session
  476. * @param stationId
  477. * @param cb
  478. * @return {{ status: String, userCount: Integer }}
  479. */
  480. leave: (session, stationId, cb) => {
  481. async.waterfall([
  482. (next) => {
  483. stations.getStation(stationId, next);
  484. },
  485. (station, next) => {
  486. if (!station) return next('Station not found.');
  487. next();
  488. }
  489. ], async (err, userCount) => {
  490. if (err) {
  491. err = await utils.getError(err);
  492. logger.error("STATIONS_LEAVE", `Leaving station "${stationId}" failed. "${err}"`);
  493. return cb({'status': 'failure', 'message': err});
  494. }
  495. logger.success("STATIONS_LEAVE", `Left station "${stationId}" successfully.`);
  496. utils.socketLeaveRooms(session);
  497. delete userList[session.socketId];
  498. return cb({'status': 'success', 'message': 'Successfully left station.', userCount});
  499. });
  500. },
  501. /**
  502. * Updates a station's name
  503. *
  504. * @param session
  505. * @param stationId - the station id
  506. * @param newName - the new station name
  507. * @param cb
  508. */
  509. updateName: hooks.ownerRequired((session, stationId, newName, cb) => {
  510. async.waterfall([
  511. (next) => {
  512. db.models.station.updateOne({_id: stationId}, {$set: {name: newName}}, {runValidators: true}, next);
  513. },
  514. (res, next) => {
  515. stations.updateStation(stationId, next);
  516. }
  517. ], async (err) => {
  518. if (err) {
  519. err = await utils.getError(err);
  520. logger.error("STATIONS_UPDATE_NAME", `Updating station "${stationId}" name to "${newName}" failed. "${err}"`);
  521. return cb({'status': 'failure', 'message': err});
  522. }
  523. logger.success("STATIONS_UPDATE_NAME", `Updated station "${stationId}" name to "${newName}" successfully.`);
  524. return cb({'status': 'success', 'message': 'Successfully updated the name.'});
  525. });
  526. }),
  527. /**
  528. * Updates a station's display name
  529. *
  530. * @param session
  531. * @param stationId - the station id
  532. * @param newDisplayName - the new station display name
  533. * @param cb
  534. */
  535. updateDisplayName: hooks.ownerRequired((session, stationId, newDisplayName, cb) => {
  536. async.waterfall([
  537. (next) => {
  538. db.models.station.updateOne({_id: stationId}, {$set: {displayName: newDisplayName}}, {runValidators: true}, next);
  539. },
  540. (res, next) => {
  541. stations.updateStation(stationId, next);
  542. }
  543. ], async (err) => {
  544. if (err) {
  545. err = await utils.getError(err);
  546. logger.error("STATIONS_UPDATE_DISPLAY_NAME", `Updating station "${stationId}" displayName to "${newDisplayName}" failed. "${err}"`);
  547. return cb({'status': 'failure', 'message': err});
  548. }
  549. logger.success("STATIONS_UPDATE_DISPLAY_NAME", `Updated station "${stationId}" displayName to "${newDisplayName}" successfully.`);
  550. return cb({'status': 'success', 'message': 'Successfully updated the display name.'});
  551. });
  552. }),
  553. /**
  554. * Updates a station's description
  555. *
  556. * @param session
  557. * @param stationId - the station id
  558. * @param newDescription - the new station description
  559. * @param cb
  560. */
  561. updateDescription: hooks.ownerRequired((session, stationId, newDescription, cb) => {
  562. async.waterfall([
  563. (next) => {
  564. db.models.station.updateOne({_id: stationId}, {$set: {description: newDescription}}, {runValidators: true}, next);
  565. },
  566. (res, next) => {
  567. stations.updateStation(stationId, next);
  568. }
  569. ], async (err) => {
  570. if (err) {
  571. err = await utils.getError(err);
  572. logger.error("STATIONS_UPDATE_DESCRIPTION", `Updating station "${stationId}" description to "${newDescription}" failed. "${err}"`);
  573. return cb({'status': 'failure', 'message': err});
  574. }
  575. logger.success("STATIONS_UPDATE_DESCRIPTION", `Updated station "${stationId}" description to "${newDescription}" successfully.`);
  576. return cb({'status': 'success', 'message': 'Successfully updated the description.'});
  577. });
  578. }),
  579. /**
  580. * Updates a station's privacy
  581. *
  582. * @param session
  583. * @param stationId - the station id
  584. * @param newPrivacy - the new station privacy
  585. * @param cb
  586. */
  587. updatePrivacy: hooks.ownerRequired((session, stationId, newPrivacy, cb) => {
  588. async.waterfall([
  589. (next) => {
  590. db.models.station.updateOne({_id: stationId}, {$set: {privacy: newPrivacy}}, {runValidators: true}, next);
  591. },
  592. (res, next) => {
  593. stations.updateStation(stationId, next);
  594. }
  595. ], async (err) => {
  596. if (err) {
  597. err = await utils.getError(err);
  598. logger.error("STATIONS_UPDATE_PRIVACY", `Updating station "${stationId}" privacy to "${newPrivacy}" failed. "${err}"`);
  599. return cb({'status': 'failure', 'message': err});
  600. }
  601. logger.success("STATIONS_UPDATE_PRIVACY", `Updated station "${stationId}" privacy to "${newPrivacy}" successfully.`);
  602. return cb({'status': 'success', 'message': 'Successfully updated the privacy.'});
  603. });
  604. }),
  605. /**
  606. * Updates a station's genres
  607. *
  608. * @param session
  609. * @param stationId - the station id
  610. * @param newGenres - the new station genres
  611. * @param cb
  612. */
  613. updateGenres: hooks.ownerRequired((session, stationId, newGenres, cb) => {
  614. async.waterfall([
  615. (next) => {
  616. db.models.station.updateOne({_id: stationId}, {$set: {genres: newGenres}}, {runValidators: true}, next);
  617. },
  618. (res, next) => {
  619. stations.updateStation(stationId, next);
  620. }
  621. ], async (err) => {
  622. if (err) {
  623. err = await utils.getError(err);
  624. logger.error("STATIONS_UPDATE_GENRES", `Updating station "${stationId}" genres to "${newGenres}" failed. "${err}"`);
  625. return cb({'status': 'failure', 'message': err});
  626. }
  627. logger.success("STATIONS_UPDATE_GENRES", `Updated station "${stationId}" genres to "${newGenres}" successfully.`);
  628. return cb({'status': 'success', 'message': 'Successfully updated the genres.'});
  629. });
  630. }),
  631. /**
  632. * Updates a station's blacklisted genres
  633. *
  634. * @param session
  635. * @param stationId - the station id
  636. * @param newBlacklistedGenres - the new station blacklisted genres
  637. * @param cb
  638. */
  639. updateBlacklistedGenres: hooks.ownerRequired((session, stationId, newBlacklistedGenres, cb) => {
  640. async.waterfall([
  641. (next) => {
  642. db.models.station.updateOne({_id: stationId}, {$set: {blacklistedGenres: newBlacklistedGenres}}, {runValidators: true}, next);
  643. },
  644. (res, next) => {
  645. stations.updateStation(stationId, next);
  646. }
  647. ], async (err) => {
  648. if (err) {
  649. err = await utils.getError(err);
  650. logger.error("STATIONS_UPDATE_BLACKLISTED_GENRES", `Updating station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" failed. "${err}"`);
  651. return cb({'status': 'failure', 'message': err});
  652. }
  653. logger.success("STATIONS_UPDATE_BLACKLISTED_GENRES", `Updated station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" successfully.`);
  654. return cb({'status': 'success', 'message': 'Successfully updated the blacklisted genres.'});
  655. });
  656. }),
  657. /**
  658. * Updates a station's party mode
  659. *
  660. * @param session
  661. * @param stationId - the station id
  662. * @param newPartyMode - the new station party mode
  663. * @param cb
  664. */
  665. updatePartyMode: hooks.ownerRequired((session, stationId, newPartyMode, cb) => {
  666. async.waterfall([
  667. (next) => {
  668. stations.getStation(stationId, next);
  669. },
  670. (station, next) => {
  671. if (!station) return next('Station not found.');
  672. if (station.partyMode === newPartyMode) return next('The party mode was already ' + ((newPartyMode) ? 'enabled.' : 'disabled.'));
  673. db.models.station.updateOne({_id: stationId}, {$set: {partyMode: newPartyMode}}, {runValidators: true}, next);
  674. },
  675. (res, next) => {
  676. stations.updateStation(stationId, next);
  677. }
  678. ], async (err) => {
  679. if (err) {
  680. err = await utils.getError(err);
  681. logger.error("STATIONS_UPDATE_PARTY_MODE", `Updating station "${stationId}" party mode to "${newPartyMode}" failed. "${err}"`);
  682. return cb({'status': 'failure', 'message': err});
  683. }
  684. logger.success("STATIONS_UPDATE_PARTY_MODE", `Updated station "${stationId}" party mode to "${newPartyMode}" successfully.`);
  685. cache.pub('station.updatePartyMode', {stationId: stationId, partyMode: newPartyMode});
  686. stations.skipStation(stationId)();
  687. return cb({'status': 'success', 'message': 'Successfully updated the party mode.'});
  688. });
  689. }),
  690. /**
  691. * Pauses a station
  692. *
  693. * @param session
  694. * @param stationId - the station id
  695. * @param cb
  696. */
  697. pause: hooks.ownerRequired((session, stationId, cb) => {
  698. async.waterfall([
  699. (next) => {
  700. stations.getStation(stationId, next);
  701. },
  702. (station, next) => {
  703. if (!station) return next('Station not found.');
  704. if (station.paused) return next('That station was already paused.');
  705. db.models.station.updateOne({_id: stationId}, {$set: {paused: true, pausedAt: Date.now()}}, next);
  706. },
  707. (res, next) => {
  708. stations.updateStation(stationId, next);
  709. }
  710. ], async (err) => {
  711. if (err) {
  712. err = await utils.getError(err);
  713. logger.error("STATIONS_PAUSE", `Pausing station "${stationId}" failed. "${err}"`);
  714. return cb({'status': 'failure', 'message': err});
  715. }
  716. logger.success("STATIONS_PAUSE", `Paused station "${stationId}" successfully.`);
  717. cache.pub('station.pause', stationId);
  718. notifications.unschedule(`stations.nextSong?id=${stationId}`);
  719. return cb({'status': 'success', 'message': 'Successfully paused.'});
  720. });
  721. }),
  722. /**
  723. * Resumes a station
  724. *
  725. * @param session
  726. * @param stationId - the station id
  727. * @param cb
  728. */
  729. resume: hooks.ownerRequired((session, stationId, cb) => {
  730. async.waterfall([
  731. (next) => {
  732. stations.getStation(stationId, next);
  733. },
  734. (station, next) => {
  735. if (!station) return next('Station not found.');
  736. if (!station.paused) return next('That station is not paused.');
  737. station.timePaused += (Date.now() - station.pausedAt);
  738. db.models.station.updateOne({_id: stationId}, {$set: {paused: false}, $inc: {timePaused: Date.now() - station.pausedAt}}, next);
  739. },
  740. (res, next) => {
  741. stations.updateStation(stationId, next);
  742. }
  743. ], async (err) => {
  744. if (err) {
  745. err = await utils.getError(err);
  746. logger.error("STATIONS_RESUME", `Resuming station "${stationId}" failed. "${err}"`);
  747. return cb({'status': 'failure', 'message': err});
  748. }
  749. logger.success("STATIONS_RESUME", `Resuming station "${stationId}" successfully.`);
  750. cache.pub('station.resume', stationId);
  751. return cb({'status': 'success', 'message': 'Successfully resumed.'});
  752. });
  753. }),
  754. /**
  755. * Removes a station
  756. *
  757. * @param session
  758. * @param stationId - the station id
  759. * @param cb
  760. */
  761. remove: hooks.ownerRequired((session, stationId, cb) => {
  762. async.waterfall([
  763. (next) => {
  764. db.models.station.deleteOne({ _id: stationId }, err => next(err));
  765. },
  766. (next) => {
  767. cache.hdel('stations', stationId, err => next(err));
  768. }
  769. ], async (err) => {
  770. if (err) {
  771. err = await utils.getError(err);
  772. logger.error("STATIONS_REMOVE", `Removing station "${stationId}" failed. "${err}"`);
  773. return cb({ 'status': 'failure', 'message': err });
  774. }
  775. logger.success("STATIONS_REMOVE", `Removing station "${stationId}" successfully.`);
  776. cache.pub('station.remove', stationId);
  777. return cb({ 'status': 'success', 'message': 'Successfully removed.' });
  778. });
  779. }),
  780. /**
  781. * Create a station
  782. *
  783. * @param session
  784. * @param data - the station data
  785. * @param cb
  786. */
  787. create: hooks.loginRequired((session, data, cb) => {
  788. data.name = data.name.toLowerCase();
  789. let blacklist = ["country", "edm", "musare", "hip-hop", "rap", "top-hits", "todays-hits", "old-school", "christmas", "about", "support", "staff", "help", "news", "terms", "privacy", "profile", "c", "community", "tos", "login", "register", "p", "official", "o", "trap", "faq", "team", "donate", "buy", "shop", "forums", "explore", "settings", "admin", "auth", "reset_password"];
  790. async.waterfall([
  791. (next) => {
  792. if (!data) return next('Invalid data.');
  793. next();
  794. },
  795. (next) => {
  796. db.models.station.findOne({ $or: [{name: data.name}, {displayName: new RegExp(`^${data.displayName}$`, 'i')}] }, next);
  797. },
  798. (station, next) => {
  799. if (station) return next('A station with that name or display name already exists.');
  800. const { name, displayName, description, genres, playlist, type, blacklistedGenres } = data;
  801. if (type === 'official') {
  802. db.models.user.findOne({_id: session.userId}, (err, user) => {
  803. if (err) return next(err);
  804. if (!user) return next('User not found.');
  805. if (user.role !== 'admin') return next('Admin required.');
  806. db.models.station.create({
  807. name,
  808. displayName,
  809. description,
  810. type,
  811. privacy: 'private',
  812. playlist,
  813. genres,
  814. blacklistedGenres,
  815. currentSong: stations.defaultSong,
  816. theme: 'blue'
  817. }, next);
  818. });
  819. } else if (type === 'community') {
  820. if (blacklist.indexOf(name) !== -1) return next('That name is blacklisted. Please use a different name.');
  821. db.models.station.create({
  822. name,
  823. displayName,
  824. description,
  825. type,
  826. privacy: 'private',
  827. owner: session.userId,
  828. queue: [],
  829. currentSong: null,
  830. theme: 'blue'
  831. }, next);
  832. }
  833. }
  834. ], async (err, station) => {
  835. if (err) {
  836. err = await utils.getError(err);
  837. logger.error("STATIONS_CREATE", `Creating station failed. "${err}"`);
  838. return cb({'status': 'failure', 'message': err});
  839. }
  840. logger.success("STATIONS_CREATE", `Created station "${station._id}" successfully.`);
  841. cache.pub('station.create', station._id);
  842. return cb({'status': 'success', 'message': 'Successfully created station.'});
  843. });
  844. }),
  845. /**
  846. * Adds song to station queue
  847. *
  848. * @param session
  849. * @param stationId - the station id
  850. * @param songId - the song id
  851. * @param cb
  852. */
  853. addToQueue: hooks.loginRequired((session, stationId, songId, cb) => {
  854. async.waterfall([
  855. (next) => {
  856. stations.getStation(stationId, next);
  857. },
  858. (station, next) => {
  859. if (!station) return next('Station not found.');
  860. if (station.locked) {
  861. db.models.user.findOne({ _id: session.userId }, (err, user) => {
  862. if (user.role !== 'admin' && station.owner !== session.userId) return next('Only owners and admins can add songs to a locked queue.');
  863. else return next(null, station);
  864. });
  865. } else {
  866. return next(null, station);
  867. }
  868. },
  869. (station, next) => {
  870. if (station.type !== 'community') return next('That station is not a community station.');
  871. stations.canUserViewStation(station, session.userId, (err, canView) => {
  872. if (err) return next(err);
  873. if (canView) return next(null, station);
  874. return next('Insufficient permissions.');
  875. });
  876. },
  877. (station, next) => {
  878. if (station.currentSong && station.currentSong.songId === songId) return next('That song is currently playing.');
  879. async.each(station.queue, (queueSong, next) => {
  880. if (queueSong.songId === songId) return next('That song is already in the queue.');
  881. next();
  882. }, (err) => {
  883. next(err, station);
  884. });
  885. },
  886. (station, next) => {
  887. songs.getSong(songId, (err, song) => {
  888. if (!err && song) return next(null, song, station);
  889. utils.getSongFromYouTube(songId, (song) => {
  890. song.artists = [];
  891. song.skipDuration = 0;
  892. song.likes = -1;
  893. song.dislikes = -1;
  894. song.thumbnail = "empty";
  895. song.explicit = false;
  896. next(null, song, station);
  897. });
  898. });
  899. },
  900. (song, station, next) => {
  901. let queue = station.queue;
  902. song.requestedBy = session.userId;
  903. queue.push(song);
  904. let totalDuration = 0;
  905. queue.forEach((song) => {
  906. totalDuration += song.duration;
  907. });
  908. if (totalDuration >= 3600 * 3) return next('The max length of the queue is 3 hours.');
  909. next(null, song, station);
  910. },
  911. (song, station, next) => {
  912. let queue = station.queue;
  913. if (queue.length === 0) return next(null, song, station);
  914. let totalDuration = 0;
  915. const userId = queue[queue.length - 1].requestedBy;
  916. station.queue.forEach((song) => {
  917. if (userId === song.requestedBy) {
  918. totalDuration += song.duration;
  919. }
  920. });
  921. if(totalDuration >= 900) return next('The max length of songs per user is 15 minutes.');
  922. next(null, song, station);
  923. },
  924. (song, station, next) => {
  925. let queue = station.queue;
  926. if (queue.length === 0) return next(null, song);
  927. let totalSongs = 0;
  928. const userId = queue[queue.length - 1].requestedBy;
  929. queue.forEach((song) => {
  930. if (userId === song.requestedBy) {
  931. totalSongs++;
  932. }
  933. });
  934. if (totalSongs <= 2) return next(null, song);
  935. if (totalSongs > 3) return next('The max amount of songs per user is 3, and only 2 in a row is allowed.');
  936. if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId) return next('The max amount of songs per user is 3, and only 2 in a row is allowed.');
  937. next(null, song);
  938. },
  939. (song, next) => {
  940. db.models.station.updateOne({_id: stationId}, {$push: {queue: song}}, {runValidators: true}, next);
  941. },
  942. (res, next) => {
  943. stations.updateStation(stationId, next);
  944. }
  945. ], async (err, station) => {
  946. if (err) {
  947. err = await utils.getError(err);
  948. logger.error("STATIONS_ADD_SONG_TO_QUEUE", `Adding song "${songId}" to station "${stationId}" queue failed. "${err}"`);
  949. return cb({'status': 'failure', 'message': err});
  950. }
  951. logger.success("STATIONS_ADD_SONG_TO_QUEUE", `Added song "${songId}" to station "${stationId}" successfully.`);
  952. cache.pub('station.queueUpdate', stationId);
  953. return cb({'status': 'success', 'message': 'Successfully added song to queue.'});
  954. });
  955. }),
  956. /**
  957. * Removes song from station queue
  958. *
  959. * @param session
  960. * @param stationId - the station id
  961. * @param songId - the song id
  962. * @param cb
  963. */
  964. removeFromQueue: hooks.ownerRequired((session, stationId, songId, cb) => {
  965. async.waterfall([
  966. (next) => {
  967. if (!songId) return next('Invalid song id.');
  968. stations.getStation(stationId, next);
  969. },
  970. (station, next) => {
  971. if (!station) return next('Station not found.');
  972. if (station.type !== 'community') return next('Station is not a community station.');
  973. async.each(station.queue, (queueSong, next) => {
  974. if (queueSong.songId === songId) return next(true);
  975. next();
  976. }, (err) => {
  977. if (err === true) return next();
  978. next('Song is not currently in the queue.');
  979. });
  980. },
  981. (next) => {
  982. db.models.station.updateOne({_id: stationId}, {$pull: {queue: {songId: songId}}}, next);
  983. },
  984. (res, next) => {
  985. stations.updateStation(stationId, next);
  986. }
  987. ], async (err, station) => {
  988. if (err) {
  989. err = await utils.getError(err);
  990. logger.error("STATIONS_REMOVE_SONG_TO_QUEUE", `Removing song "${songId}" from station "${stationId}" queue failed. "${err}"`);
  991. return cb({'status': 'failure', 'message': err});
  992. }
  993. logger.success("STATIONS_REMOVE_SONG_TO_QUEUE", `Removed song "${songId}" from station "${stationId}" successfully.`);
  994. cache.pub('station.queueUpdate', stationId);
  995. return cb({'status': 'success', 'message': 'Successfully removed song from queue.'});
  996. });
  997. }),
  998. /**
  999. * Gets the queue from a station
  1000. *
  1001. * @param session
  1002. * @param stationId - the station id
  1003. * @param cb
  1004. */
  1005. getQueue: (session, stationId, cb) => {
  1006. async.waterfall([
  1007. (next) => {
  1008. stations.getStation(stationId, next);
  1009. },
  1010. (station, next) => {
  1011. if (!station) return next('Station not found.');
  1012. if (station.type !== 'community') return next('Station is not a community station.');
  1013. next(null, station);
  1014. },
  1015. (station, next) => {
  1016. stations.canUserViewStation(station, session.userId, (err, canView) => {
  1017. if (err) return next(err);
  1018. if (canView) return next(null, station);
  1019. return next('Insufficient permissions.');
  1020. });
  1021. }
  1022. ], async (err, station) => {
  1023. if (err) {
  1024. err = await utils.getError(err);
  1025. logger.error("STATIONS_GET_QUEUE", `Getting queue for station "${stationId}" failed. "${err}"`);
  1026. return cb({'status': 'failure', 'message': err});
  1027. }
  1028. logger.success("STATIONS_GET_QUEUE", `Got queue for station "${stationId}" successfully.`);
  1029. return cb({'status': 'success', 'message': 'Successfully got queue.', queue: station.queue});
  1030. });
  1031. },
  1032. /**
  1033. * Selects a private playlist for a station
  1034. *
  1035. * @param session
  1036. * @param stationId - the station id
  1037. * @param playlistId - the private playlist id
  1038. * @param cb
  1039. */
  1040. selectPrivatePlaylist: hooks.ownerRequired((session, stationId, playlistId, cb) => {
  1041. async.waterfall([
  1042. (next) => {
  1043. stations.getStation(stationId, next);
  1044. },
  1045. (station, next) => {
  1046. if (!station) return next('Station not found.');
  1047. if (station.type !== 'community') return next('Station is not a community station.');
  1048. if (station.privatePlaylist === playlistId) return next('That private playlist is already selected.');
  1049. db.models.playlist.findOne({_id: playlistId}, next);
  1050. },
  1051. (playlist, next) => {
  1052. if (!playlist) return next('Playlist not found.');
  1053. let currentSongIndex = (playlist.songs.length > 0) ? playlist.songs.length - 1 : 0;
  1054. db.models.station.updateOne({_id: stationId}, {$set: {privatePlaylist: playlistId, currentSongIndex: currentSongIndex}}, {runValidators: true}, next);
  1055. },
  1056. (res, next) => {
  1057. stations.updateStation(stationId, next);
  1058. }
  1059. ], async (err, station) => {
  1060. if (err) {
  1061. err = await utils.getError(err);
  1062. logger.error("STATIONS_SELECT_PRIVATE_PLAYLIST", `Selecting private playlist "${playlistId}" for station "${stationId}" failed. "${err}"`);
  1063. return cb({'status': 'failure', 'message': err});
  1064. }
  1065. logger.success("STATIONS_SELECT_PRIVATE_PLAYLIST", `Selected private playlist "${playlistId}" for station "${stationId}" successfully.`);
  1066. notifications.unschedule(`stations.nextSong?id${stationId}`);
  1067. if (!station.partyMode) stations.skipStation(stationId)();
  1068. cache.pub('privatePlaylist.selected', {playlistId, stationId});
  1069. return cb({'status': 'success', 'message': 'Successfully selected playlist.'});
  1070. });
  1071. }),
  1072. favoriteStation: hooks.loginRequired((session, stationId, cb) => {
  1073. async.waterfall([
  1074. (next) => {
  1075. stations.getStation(stationId, next);
  1076. },
  1077. (station, next) => {
  1078. if (!station) return next('Station not found.');
  1079. stations.canUserViewStation(station, session.userId, (err, canView) => {
  1080. if (err) return next(err);
  1081. if (canView) return next();
  1082. return next('Insufficient permissions.');
  1083. });
  1084. },
  1085. (next) => {
  1086. db.models.user.updateOne({ _id: session.userId }, { $addToSet: { favoriteStations: stationId } }, next);
  1087. },
  1088. (res, next) => {
  1089. if (res.nModified === 0) return next("The station was already favorited.");
  1090. next();
  1091. }
  1092. ], async (err) => {
  1093. if (err) {
  1094. err = await utils.getError(err);
  1095. logger.error("FAVORITE_STATION", `Favoriting station "${stationId}" failed. "${err}"`);
  1096. return cb({'status': 'failure', 'message': err});
  1097. }
  1098. logger.success("FAVORITE_STATION", `Favorited station "${stationId}" successfully.`);
  1099. cache.pub('user.favoritedStation', { userId: session.userId, stationId });
  1100. return cb({'status': 'success', 'message': 'Succesfully favorited station.'});
  1101. });
  1102. }),
  1103. unfavoriteStation: hooks.loginRequired((session, stationId, cb) => {
  1104. async.waterfall([
  1105. (next) => {
  1106. db.models.user.updateOne({ _id: session.userId }, { $pull: { favoriteStations: stationId } }, next);
  1107. },
  1108. (res, next) => {
  1109. if (res.nModified === 0) return next("The station wasn't favorited.");
  1110. next();
  1111. }
  1112. ], async (err) => {
  1113. if (err) {
  1114. err = await utils.getError(err);
  1115. logger.error("UNFAVORITE_STATION", `Unfavoriting station "${stationId}" failed. "${err}"`);
  1116. return cb({'status': 'failure', 'message': err});
  1117. }
  1118. logger.success("UNFAVORITE_STATION", `Unfavorited station "${stationId}" successfully.`);
  1119. cache.pub('user.unfavoritedStation', { userId: session.userId, stationId });
  1120. return cb({'status': 'success', 'message': 'Succesfully unfavorited station.'});
  1121. });
  1122. }),
  1123. /**
  1124. * Updates a station's theme
  1125. *
  1126. * @param session
  1127. * @param stationId - the station id
  1128. * @param newTheme - the new station theme
  1129. * @param cb
  1130. */
  1131. updateTheme: hooks.ownerRequired((session, stationId, newTheme, cb) => {
  1132. async.waterfall([
  1133. (next) => {
  1134. db.models.station.updateOne({_id: stationId}, {$set: {theme: newTheme}}, {runValidators: true}, next);
  1135. },
  1136. (res, next) => {
  1137. stations.updateStation(stationId, next);
  1138. }
  1139. ], async (err) => {
  1140. if (err) {
  1141. err = await utils.getError(err);
  1142. logger.error("STATIONS_UPDATE_THEME", `Updating station "${stationId}" theme to "${newTheme}" failed. "${err}"`);
  1143. return cb({'status': 'failure', 'message': err});
  1144. }
  1145. logger.success("STATIONS_UPDATE_THEME", `Updated station "${stationId}" theme to "${newTheme}" successfully.`);
  1146. return cb({'status': 'success', 'message': 'Successfully updated the theme.'});
  1147. });
  1148. }),
  1149. };