2
0

stations.js 37 KB

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