stations.js 67 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683
  1. import async from "async";
  2. import { isLoginRequired, isOwnerRequired } from "./hooks";
  3. import moduleManager from "../../index";
  4. const DBModule = moduleManager.modules.db;
  5. const UtilsModule = moduleManager.modules.utils;
  6. const IOModule = moduleManager.modules.io;
  7. const SongsModule = moduleManager.modules.songs;
  8. const CacheModule = moduleManager.modules.cache;
  9. const NotificationsModule = moduleManager.modules.notifications;
  10. const StationsModule = moduleManager.modules.stations;
  11. const ActivitiesModule = moduleManager.modules.activities;
  12. const YouTubeModule = moduleManager.modules.youtube;
  13. CacheModule.runJob("SUB", {
  14. channel: "station.updateUsers",
  15. cb: ({ stationId, usersPerStation }) => {
  16. IOModule.runJob("EMIT_TO_ROOM", {
  17. room: `station.${stationId}`,
  18. args: ["event:users.updated", usersPerStation]
  19. });
  20. }
  21. });
  22. CacheModule.runJob("SUB", {
  23. channel: "station.updateUserCount",
  24. cb: ({ stationId, usersPerStationCount }) => {
  25. const count = usersPerStationCount || 0;
  26. IOModule.runJob("EMIT_TO_ROOM", {
  27. room: `station.${stationId}`,
  28. args: ["event:userCount.updated", count]
  29. });
  30. StationsModule.runJob("GET_STATION", { stationId }).then(async station => {
  31. if (station.privacy === "public")
  32. IOModule.runJob("EMIT_TO_ROOM", {
  33. room: "home",
  34. args: ["event:userCount.updated", stationId, count]
  35. });
  36. else {
  37. const sockets = await IOModule.runJob("GET_ROOM_SOCKETS", {
  38. room: "home"
  39. });
  40. Object.keys(sockets).forEach(socketKey => {
  41. const socket = sockets[socketKey];
  42. const { session } = socket;
  43. if (session.sessionId) {
  44. CacheModule.runJob("HGET", {
  45. table: "sessions",
  46. key: session.sessionId
  47. }).then(session => {
  48. if (session)
  49. DBModule.runJob("GET_MODEL", {
  50. modelName: "user"
  51. }).then(userModel =>
  52. userModel.findOne({ _id: session.userId }, (err, user) => {
  53. if (user.role === "admin")
  54. socket.emit("event:userCount.updated", stationId, count);
  55. else if (station.type === "community" && station.owner === session.userId)
  56. socket.emit("event:userCount.updated", stationId, count);
  57. })
  58. );
  59. });
  60. }
  61. });
  62. }
  63. });
  64. }
  65. });
  66. CacheModule.runJob("SUB", {
  67. channel: "station.updateTheme",
  68. cb: data => {
  69. IOModule.runJob("EMIT_TO_ROOM", {
  70. room: `station.${data.stationId}`,
  71. args: ["event:theme.updated", data.theme]
  72. });
  73. }
  74. });
  75. CacheModule.runJob("SUB", {
  76. channel: "station.queueLockToggled",
  77. cb: data => {
  78. IOModule.runJob("EMIT_TO_ROOM", {
  79. room: `station.${data.stationId}`,
  80. args: ["event:queueLockToggled", data.locked]
  81. });
  82. }
  83. });
  84. CacheModule.runJob("SUB", {
  85. channel: "station.updatePartyMode",
  86. cb: data => {
  87. IOModule.runJob("EMIT_TO_ROOM", {
  88. room: `station.${data.stationId}`,
  89. args: ["event:partyMode.updated", data.partyMode]
  90. });
  91. }
  92. });
  93. CacheModule.runJob("SUB", {
  94. channel: "privatePlaylist.selected",
  95. cb: data => {
  96. IOModule.runJob("EMIT_TO_ROOM", {
  97. room: `station.${data.stationId}`,
  98. args: ["event:privatePlaylist.selected", data.playlistId]
  99. });
  100. }
  101. });
  102. CacheModule.runJob("SUB", {
  103. channel: "privatePlaylist.deselected",
  104. cb: data => {
  105. IOModule.runJob("EMIT_TO_ROOM", {
  106. room: `station.${data.stationId}`,
  107. args: ["event:privatePlaylist.deselected"]
  108. });
  109. }
  110. });
  111. CacheModule.runJob("SUB", {
  112. channel: "station.pause",
  113. cb: stationId => {
  114. StationsModule.runJob("GET_STATION", { stationId }).then(station => {
  115. IOModule.runJob("EMIT_TO_ROOM", {
  116. room: `station.${stationId}`,
  117. args: ["event:stations.pause", { pausedAt: station.pausedAt }]
  118. });
  119. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  120. room: `home`,
  121. station
  122. }).then(response => {
  123. const { socketsThatCan } = response;
  124. socketsThatCan.forEach(socket => {
  125. socket.emit("event:station.pause", { stationId });
  126. });
  127. });
  128. });
  129. }
  130. });
  131. CacheModule.runJob("SUB", {
  132. channel: "station.resume",
  133. cb: stationId => {
  134. StationsModule.runJob("GET_STATION", { stationId }).then(station => {
  135. IOModule.runJob("EMIT_TO_ROOM", {
  136. room: `station.${stationId}`,
  137. args: ["event:stations.resume", { timePaused: station.timePaused }]
  138. });
  139. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  140. room: `home`,
  141. station
  142. })
  143. .then(response => {
  144. const { socketsThatCan } = response;
  145. socketsThatCan.forEach(socket => {
  146. socket.emit("event:station.resume", { stationId });
  147. });
  148. })
  149. .catch(console.log);
  150. });
  151. }
  152. });
  153. CacheModule.runJob("SUB", {
  154. channel: "station.privacyUpdate",
  155. cb: response => {
  156. const { stationId, previousPrivacy } = response;
  157. StationsModule.runJob("GET_STATION", { stationId }).then(station => {
  158. if (previousPrivacy !== station.privacy) {
  159. if (station.privacy === "public") {
  160. // Station became public
  161. IOModule.runJob("EMIT_TO_ROOM", {
  162. room: "home",
  163. args: ["event:stations.created", station]
  164. });
  165. } else if (previousPrivacy === "public") {
  166. // Station became hidden
  167. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  168. room: `home`,
  169. station
  170. }).then(response => {
  171. const { socketsThatCan, socketsThatCannot } = response;
  172. socketsThatCan.forEach(socket => {
  173. socket.emit("event:station.updatePrivacy", { stationId, privacy: station.privacy });
  174. });
  175. socketsThatCannot.forEach(socket => {
  176. socket.emit("event:station.removed", { stationId });
  177. });
  178. });
  179. } else {
  180. // Station was hidden and is still hidden
  181. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  182. room: `home`,
  183. station
  184. }).then(response => {
  185. const { socketsThatCan } = response;
  186. socketsThatCan.forEach(socket => {
  187. socket.emit("event:station.updatePrivacy", { stationId, privacy: station.privacy });
  188. });
  189. });
  190. }
  191. }
  192. });
  193. }
  194. });
  195. CacheModule.runJob("SUB", {
  196. channel: "station.nameUpdate",
  197. cb: res => {
  198. const { stationId, name } = res;
  199. StationsModule.runJob("GET_STATION", { stationId }).then(station =>
  200. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  201. room: `home`,
  202. station
  203. }).then(response => {
  204. const { socketsThatCan } = response;
  205. socketsThatCan.forEach(socket => socket.emit("event:station.updateName", { stationId, name }));
  206. })
  207. );
  208. IOModule.runJob("EMIT_TO_ROOM", {
  209. room: `station.${stationId}`,
  210. args: ["event:station.updateName", { stationId, name }]
  211. });
  212. }
  213. });
  214. CacheModule.runJob("SUB", {
  215. channel: "station.displayNameUpdate",
  216. cb: response => {
  217. const { stationId, displayName } = response;
  218. StationsModule.runJob("GET_STATION", { stationId }).then(station =>
  219. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  220. room: `home`,
  221. station
  222. }).then(response => {
  223. const { socketsThatCan } = response;
  224. socketsThatCan.forEach(socket =>
  225. socket.emit("event:station.updateDisplayName", { stationId, displayName })
  226. );
  227. })
  228. );
  229. IOModule.runJob("EMIT_TO_ROOM", {
  230. room: `station.${stationId}`,
  231. args: ["event:station.updateDisplayName", { stationId, displayName }]
  232. });
  233. }
  234. });
  235. CacheModule.runJob("SUB", {
  236. channel: "station.descriptionUpdate",
  237. cb: response => {
  238. const { stationId, description } = response;
  239. StationsModule.runJob("GET_STATION", { stationId }).then(station =>
  240. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  241. room: `home`,
  242. station
  243. }).then(response => {
  244. const { socketsThatCan } = response;
  245. socketsThatCan.forEach(socket =>
  246. socket.emit("event:station.updateDescription", { stationId, description })
  247. );
  248. })
  249. );
  250. IOModule.runJob("EMIT_TO_ROOM", {
  251. room: `station.${stationId}`,
  252. args: ["event:station.updateDescription", { stationId, description }]
  253. });
  254. }
  255. });
  256. CacheModule.runJob("SUB", {
  257. channel: "station.themeUpdate",
  258. cb: response => {
  259. const { stationId } = response;
  260. StationsModule.runJob("GET_STATION", { stationId }).then(station => {
  261. IOModule.runJob("EMIT_TO_ROOM", {
  262. room: `station.${stationId}`,
  263. args: ["event:station.themeUpdated", station.theme]
  264. });
  265. StationsModule.runJob("GET_SOCKETS_THAT_CAN_KNOW_ABOUT_STATION", {
  266. room: `home`,
  267. station
  268. }).then(response => {
  269. const { socketsThatCan } = response;
  270. socketsThatCan.forEach(socket => {
  271. socket.emit("event:station.themeUpdated", { stationId, theme: station.theme });
  272. });
  273. });
  274. });
  275. }
  276. });
  277. CacheModule.runJob("SUB", {
  278. channel: "station.queueUpdate",
  279. cb: stationId => {
  280. StationsModule.runJob("GET_STATION", { stationId }).then(station => {
  281. IOModule.runJob("EMIT_TO_ROOM", {
  282. room: `station.${stationId}`,
  283. args: ["event:queue.update", station.queue]
  284. });
  285. });
  286. }
  287. });
  288. CacheModule.runJob("SUB", {
  289. channel: "station.voteSkipSong",
  290. cb: stationId => {
  291. IOModule.runJob("EMIT_TO_ROOM", {
  292. room: `station.${stationId}`,
  293. args: ["event:song.voteSkipSong"]
  294. });
  295. }
  296. });
  297. CacheModule.runJob("SUB", {
  298. channel: "station.remove",
  299. cb: stationId => {
  300. IOModule.runJob("EMIT_TO_ROOM", {
  301. room: `station.${stationId}`,
  302. args: ["event:stations.remove"]
  303. });
  304. console.log(111, "REMOVED");
  305. IOModule.runJob("EMIT_TO_ROOM", {
  306. room: `home`,
  307. args: ["event:station.removed", { stationId }]
  308. });
  309. IOModule.runJob("EMIT_TO_ROOM", {
  310. room: "admin.stations",
  311. args: ["event:admin.station.removed", stationId]
  312. });
  313. }
  314. });
  315. CacheModule.runJob("SUB", {
  316. channel: "station.create",
  317. cb: async stationId => {
  318. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" });
  319. StationsModule.runJob("INITIALIZE_STATION", { stationId }).then(async response => {
  320. const { station } = response;
  321. station.userCount = StationsModule.usersPerStationCount[stationId] || 0;
  322. IOModule.runJob("EMIT_TO_ROOM", {
  323. room: "admin.stations",
  324. args: ["event:admin.station.added", station]
  325. });
  326. // TODO If community, check if on whitelist
  327. if (station.privacy === "public")
  328. IOModule.runJob("EMIT_TO_ROOM", {
  329. room: "home",
  330. args: ["event:stations.created", station]
  331. });
  332. else {
  333. const sockets = await IOModule.runJob("GET_ROOM_SOCKETS", {
  334. room: "home"
  335. });
  336. Object.keys(sockets).forEach(socketKey => {
  337. const socket = sockets[socketKey];
  338. const { session } = socket;
  339. if (session.sessionId) {
  340. CacheModule.runJob("HGET", {
  341. table: "sessions",
  342. key: session.sessionId
  343. }).then(session => {
  344. if (session) {
  345. userModel.findOne({ _id: session.userId }, (err, user) => {
  346. if (user.role === "admin") socket.emit("event:stations.created", station);
  347. else if (station.type === "community" && station.owner === session.userId)
  348. socket.emit("event:stations.created", station);
  349. });
  350. }
  351. });
  352. }
  353. });
  354. }
  355. });
  356. }
  357. });
  358. export default {
  359. /**
  360. * Get a list of all the stations
  361. *
  362. * @param {object} session - user session
  363. * @param {Function} cb - callback
  364. */
  365. index(session, cb) {
  366. async.waterfall(
  367. [
  368. next => {
  369. CacheModule.runJob("HGETALL", { table: "stations" }, this).then(stations => {
  370. next(null, stations);
  371. });
  372. },
  373. (items, next) => {
  374. const filteredStations = [];
  375. async.each(
  376. items,
  377. (station, nextStation) => {
  378. async.waterfall(
  379. [
  380. callback => {
  381. // only relevant if user logged in
  382. if (session.userId) {
  383. return StationsModule.runJob(
  384. "HAS_USER_FAVORITED_STATION",
  385. {
  386. userId: session.userId,
  387. stationId: station._id
  388. },
  389. this
  390. )
  391. .then(isStationFavorited => {
  392. station.isFavorited = isStationFavorited;
  393. return callback();
  394. })
  395. .catch(err => callback(err));
  396. }
  397. return callback();
  398. },
  399. callback => {
  400. StationsModule.runJob(
  401. "CAN_USER_VIEW_STATION",
  402. {
  403. station,
  404. userId: session.userId,
  405. hideUnlisted: true
  406. },
  407. this
  408. )
  409. .then(exists => callback(null, exists))
  410. .catch(callback);
  411. }
  412. ],
  413. (err, exists) => {
  414. if (err) return this.log("ERROR", "STATIONS_INDEX", err);
  415. station.userCount = StationsModule.usersPerStationCount[station._id] || 0;
  416. if (exists) filteredStations.push(station);
  417. return nextStation();
  418. }
  419. );
  420. },
  421. () => next(null, filteredStations)
  422. );
  423. }
  424. ],
  425. async (err, stations) => {
  426. if (err) {
  427. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  428. this.log("ERROR", "STATIONS_INDEX", `Indexing stations failed. "${err}"`);
  429. return cb({ status: "failure", message: err });
  430. }
  431. this.log("SUCCESS", "STATIONS_INDEX", `Indexing stations successful.`, false);
  432. return cb({ status: "success", stations });
  433. }
  434. );
  435. },
  436. /**
  437. * Obtains basic metadata of a station in order to format an activity
  438. *
  439. * @param {object} session - user session
  440. * @param {string} stationId - the station id
  441. * @param {Function} cb - callback
  442. */
  443. getStationForActivity(session, stationId, cb) {
  444. async.waterfall(
  445. [
  446. next => {
  447. StationsModule.runJob("GET_STATION", { stationId }, this)
  448. .then(station => {
  449. next(null, station);
  450. })
  451. .catch(next);
  452. }
  453. ],
  454. async (err, station) => {
  455. if (err) {
  456. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  457. this.log(
  458. "ERROR",
  459. "STATIONS_GET_STATION_FOR_ACTIVITY",
  460. `Failed to obtain metadata of station ${stationId} for activity formatting. "${err}"`
  461. );
  462. return cb({ status: "failure", message: err });
  463. }
  464. this.log(
  465. "SUCCESS",
  466. "STATIONS_GET_STATION_FOR_ACTIVITY",
  467. `Obtained metadata of station ${stationId} for activity formatting successfully.`
  468. );
  469. return cb({
  470. status: "success",
  471. data: {
  472. title: station.displayName,
  473. thumbnail: station.currentSong ? station.currentSong.thumbnail : ""
  474. }
  475. });
  476. }
  477. );
  478. },
  479. /**
  480. * Verifies that a station exists
  481. *
  482. * @param {object} session - user session
  483. * @param {string} stationName - the station name
  484. * @param {Function} cb - callback
  485. */
  486. existsByName(session, stationName, cb) {
  487. async.waterfall(
  488. [
  489. next => {
  490. StationsModule.runJob("GET_STATION_BY_NAME", { stationName }, this)
  491. .then(station => {
  492. next(null, station);
  493. })
  494. .catch(next);
  495. },
  496. (station, next) => {
  497. if (!station) return next(null, false);
  498. return StationsModule.runJob(
  499. "CAN_USER_VIEW_STATION",
  500. {
  501. station,
  502. userId: session.userId
  503. },
  504. this
  505. )
  506. .then(exists => {
  507. next(null, exists);
  508. })
  509. .catch(next);
  510. }
  511. ],
  512. async (err, exists) => {
  513. if (err) {
  514. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  515. this.log(
  516. "ERROR",
  517. "STATION_EXISTS_BY_NAME",
  518. `Checking if station "${stationName}" exists failed. "${err}"`
  519. );
  520. return cb({ status: "failure", message: err });
  521. }
  522. this.log(
  523. "SUCCESS",
  524. "STATION_EXISTS_BY_NAME",
  525. `Station "${stationName}" exists successfully.` /* , false */
  526. );
  527. return cb({ status: "success", exists });
  528. }
  529. );
  530. },
  531. /**
  532. * Gets the official playlist for a station
  533. *
  534. * @param {object} session - user session
  535. * @param {string} stationId - the station id
  536. * @param {Function} cb - callback
  537. */
  538. getPlaylist(session, stationId, cb) {
  539. async.waterfall(
  540. [
  541. next => {
  542. StationsModule.runJob("GET_STATION", { stationId }, this)
  543. .then(station => {
  544. next(null, station);
  545. })
  546. .catch(next);
  547. },
  548. (station, next) => {
  549. StationsModule.runJob(
  550. "CAN_USER_VIEW_STATION",
  551. {
  552. station,
  553. userId: session.userId
  554. },
  555. this
  556. )
  557. .then(canView => {
  558. if (canView) return next(null, station);
  559. return next("Insufficient permissions.");
  560. })
  561. .catch(err => next(err));
  562. },
  563. (station, next) => {
  564. if (!station) return next("Station not found.");
  565. if (station.type !== "official") return next("This is not an official station.");
  566. return next();
  567. },
  568. next => {
  569. CacheModule.runJob(
  570. "HGET",
  571. {
  572. table: "officialPlaylists",
  573. key: stationId
  574. },
  575. this
  576. )
  577. .then(playlist => {
  578. next(null, playlist);
  579. })
  580. .catch(next);
  581. },
  582. (playlist, next) => {
  583. if (!playlist) return next("Playlist not found.");
  584. return next(null, playlist);
  585. }
  586. ],
  587. async (err, playlist) => {
  588. if (err) {
  589. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  590. this.log(
  591. "ERROR",
  592. "STATIONS_GET_PLAYLIST",
  593. `Getting playlist for station "${stationId}" failed. "${err}"`
  594. );
  595. return cb({ status: "failure", message: err });
  596. }
  597. this.log(
  598. "SUCCESS",
  599. "STATIONS_GET_PLAYLIST",
  600. `Got playlist for station "${stationId}" successfully.`,
  601. false
  602. );
  603. return cb({ status: "success", data: playlist.songs });
  604. }
  605. );
  606. },
  607. /**
  608. * Joins the station by its name
  609. *
  610. * @param {object} session - user session
  611. * @param {string} stationName - the station name
  612. * @param {Function} cb - callback
  613. */
  614. join(session, stationName, cb) {
  615. async.waterfall(
  616. [
  617. next => {
  618. StationsModule.runJob("GET_STATION_BY_NAME", { stationName }, this)
  619. .then(station => {
  620. next(null, station);
  621. })
  622. .catch(next);
  623. },
  624. (station, next) => {
  625. if (!station) return next("Station not found.");
  626. return StationsModule.runJob(
  627. "CAN_USER_VIEW_STATION",
  628. {
  629. station,
  630. userId: session.userId
  631. },
  632. this
  633. )
  634. .then(canView => {
  635. if (!canView) next("Not allowed to join station.");
  636. else next(null, station);
  637. })
  638. .catch(err => next(err));
  639. },
  640. (station, next) => {
  641. IOModule.runJob("SOCKET_JOIN_ROOM", {
  642. socketId: session.socketId,
  643. room: `station.${station._id}`
  644. });
  645. const data = {
  646. _id: station._id,
  647. type: station.type,
  648. currentSong: station.currentSong,
  649. startedAt: station.startedAt,
  650. paused: station.paused,
  651. timePaused: station.timePaused,
  652. pausedAt: station.pausedAt,
  653. description: station.description,
  654. displayName: station.displayName,
  655. privacy: station.privacy,
  656. locked: station.locked,
  657. partyMode: station.partyMode,
  658. owner: station.owner,
  659. privatePlaylist: station.privatePlaylist,
  660. genres: station.genres,
  661. blacklistedGenres: station.blacklistedGenres,
  662. theme: station.theme
  663. };
  664. StationsModule.userList[session.socketId] = station._id;
  665. next(null, data);
  666. },
  667. (data, next) => {
  668. data = JSON.parse(JSON.stringify(data));
  669. data.userCount = StationsModule.usersPerStationCount[data._id] || 0;
  670. data.users = StationsModule.usersPerStation[data._id] || [];
  671. if (!data.currentSong || !data.currentSong.title) return next(null, data);
  672. IOModule.runJob("SOCKET_JOIN_SONG_ROOM", {
  673. socketId: session.socketId,
  674. room: `song.${data.currentSong.songId}`
  675. });
  676. data.currentSong.skipVotes = data.currentSong.skipVotes.length;
  677. return SongsModule.runJob(
  678. "GET_SONG_FROM_ID",
  679. {
  680. songId: data.currentSong.songId
  681. },
  682. this
  683. )
  684. .then(response => {
  685. const { song } = response;
  686. if (song) {
  687. data.currentSong.likes = song.likes;
  688. data.currentSong.dislikes = song.dislikes;
  689. } else {
  690. data.currentSong.likes = -1;
  691. data.currentSong.dislikes = -1;
  692. }
  693. })
  694. .catch(() => {
  695. data.currentSong.likes = -1;
  696. data.currentSong.dislikes = -1;
  697. })
  698. .finally(() => next(null, data));
  699. },
  700. (data, next) => {
  701. // only relevant if user logged in
  702. if (session.userId) {
  703. return StationsModule.runJob(
  704. "HAS_USER_FAVORITED_STATION",
  705. {
  706. userId: session.userId,
  707. stationId: data._id
  708. },
  709. this
  710. )
  711. .then(isStationFavorited => {
  712. data.isFavorited = isStationFavorited;
  713. return next(null, data);
  714. })
  715. .catch(err => next(err));
  716. }
  717. return next(null, data);
  718. }
  719. ],
  720. async (err, data) => {
  721. if (err) {
  722. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  723. this.log("ERROR", "STATIONS_JOIN", `Joining station "${stationName}" failed. "${err}"`);
  724. return cb({ status: "failure", message: err });
  725. }
  726. this.log("SUCCESS", "STATIONS_JOIN", `Joined station "${data._id}" successfully.`);
  727. return cb({ status: "success", data });
  728. }
  729. );
  730. },
  731. /**
  732. * Gets a station by id
  733. *
  734. * @param {object} session - user session
  735. * @param {string} stationId - the station id
  736. * @param {Function} cb - callback
  737. */
  738. getStationById(session, stationId, cb) {
  739. async.waterfall(
  740. [
  741. next => {
  742. StationsModule.runJob("GET_STATION", { stationId }, this)
  743. .then(station => {
  744. next(null, station);
  745. })
  746. .catch(next);
  747. },
  748. (station, next) => {
  749. if (!station) return next("Station not found.");
  750. return StationsModule.runJob(
  751. "CAN_USER_VIEW_STATION",
  752. {
  753. station,
  754. userId: session.userId
  755. },
  756. this
  757. )
  758. .then(canView => {
  759. if (!canView) next("Not allowed to get station.");
  760. else next(null, station);
  761. })
  762. .catch(err => next(err));
  763. },
  764. (station, next) => {
  765. const data = {
  766. _id: station._id,
  767. type: station.type,
  768. description: station.description,
  769. displayName: station.displayName,
  770. name: station.name,
  771. privacy: station.privacy,
  772. locked: station.locked,
  773. partyMode: station.partyMode,
  774. owner: station.owner,
  775. privatePlaylist: station.privatePlaylist,
  776. genres: station.genres,
  777. blacklistedGenres: station.blacklistedGenres,
  778. theme: station.theme
  779. };
  780. next(null, data);
  781. }
  782. ],
  783. async (err, data) => {
  784. if (err) {
  785. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  786. this.log("ERROR", "GET_STATION_BY_ID", `Getting station "${stationId}" failed. "${err}"`);
  787. return cb({ status: "failure", message: err });
  788. }
  789. this.log("SUCCESS", "GET_STATION_BY_ID", `Got station "${stationId}" successfully.`);
  790. return cb({ status: "success", station: data });
  791. }
  792. );
  793. },
  794. /**
  795. * Toggles if a station is locked
  796. *
  797. * @param session
  798. * @param stationId - the station id
  799. * @param cb
  800. */
  801. toggleLock: isOwnerRequired(async function toggleLock(session, stationId, cb) {
  802. const stationModel = await DBModule.runJob(
  803. "GET_MODEL",
  804. {
  805. modelName: "station"
  806. },
  807. this
  808. );
  809. async.waterfall(
  810. [
  811. next => {
  812. StationsModule.runJob("GET_STATION", { stationId }, this)
  813. .then(station => {
  814. next(null, station);
  815. })
  816. .catch(next);
  817. },
  818. (station, next) => {
  819. stationModel.updateOne({ _id: stationId }, { $set: { locked: !station.locked } }, next);
  820. },
  821. (res, next) => {
  822. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  823. .then(station => {
  824. next(null, station);
  825. })
  826. .catch(next);
  827. }
  828. ],
  829. async (err, station) => {
  830. if (err) {
  831. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  832. this.log(
  833. "ERROR",
  834. "STATIONS_UPDATE_LOCKED_STATUS",
  835. `Toggling the queue lock for station "${stationId}" failed. "${err}"`
  836. );
  837. return cb({ status: "failure", message: err });
  838. }
  839. this.log(
  840. "SUCCESS",
  841. "STATIONS_UPDATE_LOCKED_STATUS",
  842. `Toggled the queue lock for station "${stationId}" successfully to "${station.locked}".`
  843. );
  844. CacheModule.runJob("PUB", {
  845. channel: "station.queueLockToggled",
  846. value: {
  847. stationId,
  848. locked: station.locked
  849. }
  850. });
  851. return cb({ status: "success", data: station.locked });
  852. }
  853. );
  854. }),
  855. /**
  856. * Votes to skip a station
  857. *
  858. * @param session
  859. * @param stationId - the station id
  860. * @param cb
  861. */
  862. voteSkip: isLoginRequired(async function voteSkip(session, stationId, cb) {
  863. const stationModel = await DBModule.runJob(
  864. "GET_MODEL",
  865. {
  866. modelName: "station"
  867. },
  868. this
  869. );
  870. let skipVotes = 0;
  871. let shouldSkip = false;
  872. async.waterfall(
  873. [
  874. next => {
  875. StationsModule.runJob("GET_STATION", { stationId }, this)
  876. .then(station => {
  877. next(null, station);
  878. })
  879. .catch(next);
  880. },
  881. (station, next) => {
  882. if (!station) return next("Station not found.");
  883. return StationsModule.runJob(
  884. "CAN_USER_VIEW_STATION",
  885. {
  886. station,
  887. userId: session.userId
  888. },
  889. this
  890. )
  891. .then(canView => {
  892. if (canView) return next(null, station);
  893. return next("Insufficient permissions.");
  894. })
  895. .catch(err => next(err));
  896. },
  897. (station, next) => {
  898. if (!station.currentSong) return next("There is currently no song to skip.");
  899. if (station.currentSong.skipVotes.indexOf(session.userId) !== -1)
  900. return next("You have already voted to skip this song.");
  901. return next(null, station);
  902. },
  903. (station, next) => {
  904. stationModel.updateOne(
  905. { _id: stationId },
  906. { $push: { "currentSong.skipVotes": session.userId } },
  907. next
  908. );
  909. },
  910. (res, next) => {
  911. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  912. .then(station => {
  913. next(null, station);
  914. })
  915. .catch(next);
  916. },
  917. (station, next) => {
  918. if (!station) return next("Station not found.");
  919. return next(null, station);
  920. },
  921. (station, next) => {
  922. skipVotes = station.currentSong.skipVotes.length;
  923. IOModule.runJob(
  924. "GET_ROOM_SOCKETS",
  925. {
  926. room: `station.${stationId}`
  927. },
  928. this
  929. )
  930. .then(sockets => {
  931. next(null, sockets);
  932. })
  933. .catch(next);
  934. },
  935. (sockets, next) => {
  936. if (sockets.length <= skipVotes) shouldSkip = true;
  937. next();
  938. }
  939. ],
  940. async err => {
  941. if (err) {
  942. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  943. this.log("ERROR", "STATIONS_VOTE_SKIP", `Vote skipping station "${stationId}" failed. "${err}"`);
  944. return cb({ status: "failure", message: err });
  945. }
  946. this.log("SUCCESS", "STATIONS_VOTE_SKIP", `Vote skipping "${stationId}" successful.`);
  947. CacheModule.runJob("PUB", {
  948. channel: "station.voteSkipSong",
  949. value: stationId
  950. });
  951. if (shouldSkip) StationsModule.runJob("SKIP_STATION", { stationId });
  952. return cb({
  953. status: "success",
  954. message: "Successfully voted to skip the song."
  955. });
  956. }
  957. );
  958. }),
  959. /**
  960. * Force skips a station
  961. *
  962. * @param session
  963. * @param stationId - the station id
  964. * @param cb
  965. */
  966. forceSkip: isOwnerRequired(function forceSkip(session, stationId, cb) {
  967. async.waterfall(
  968. [
  969. next => {
  970. StationsModule.runJob("GET_STATION", { stationId }, this)
  971. .then(station => {
  972. next(null, station);
  973. })
  974. .catch(next);
  975. },
  976. (station, next) => {
  977. if (!station) return next("Station not found.");
  978. return next();
  979. }
  980. ],
  981. async err => {
  982. if (err) {
  983. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  984. this.log("ERROR", "STATIONS_FORCE_SKIP", `Force skipping station "${stationId}" failed. "${err}"`);
  985. return cb({ status: "failure", message: err });
  986. }
  987. StationsModule.runJob("SKIP_STATION", { stationId });
  988. this.log("SUCCESS", "STATIONS_FORCE_SKIP", `Force skipped station "${stationId}" successfully.`);
  989. return cb({
  990. status: "success",
  991. message: "Successfully skipped station."
  992. });
  993. }
  994. );
  995. }),
  996. /**
  997. * Leaves the user's current station
  998. *
  999. * @param {object} session - user session
  1000. * @param {string} stationId - id of station to leave
  1001. * @param {Function} cb - callback
  1002. */
  1003. leave(session, stationId, cb) {
  1004. async.waterfall(
  1005. [
  1006. next => {
  1007. StationsModule.runJob("GET_STATION", { stationId }, this)
  1008. .then(station => {
  1009. next(null, station);
  1010. })
  1011. .catch(next);
  1012. },
  1013. (station, next) => {
  1014. if (!station) return next("Station not found.");
  1015. return next();
  1016. }
  1017. ],
  1018. async (err, userCount) => {
  1019. if (err) {
  1020. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1021. this.log("ERROR", "STATIONS_LEAVE", `Leaving station "${stationId}" failed. "${err}"`);
  1022. return cb({ status: "failure", message: err });
  1023. }
  1024. this.log("SUCCESS", "STATIONS_LEAVE", `Left station "${stationId}" successfully.`);
  1025. IOModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session });
  1026. delete StationsModule.userList[session.socketId];
  1027. return cb({
  1028. status: "success",
  1029. message: "Successfully left station.",
  1030. userCount
  1031. });
  1032. }
  1033. );
  1034. },
  1035. /**
  1036. * Updates a station's name
  1037. *
  1038. * @param session
  1039. * @param stationId - the station id
  1040. * @param newName - the new station name
  1041. * @param cb
  1042. */
  1043. updateName: isOwnerRequired(async function updateName(session, stationId, newName, cb) {
  1044. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1045. async.waterfall(
  1046. [
  1047. next => {
  1048. stationModel.updateOne(
  1049. { _id: stationId },
  1050. { $set: { name: newName } },
  1051. { runValidators: true },
  1052. next
  1053. );
  1054. },
  1055. (res, next) => {
  1056. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1057. .then(station => next(null, station))
  1058. .catch(next);
  1059. }
  1060. ],
  1061. async (err, station) => {
  1062. if (err) {
  1063. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1064. this.log(
  1065. "ERROR",
  1066. "STATIONS_UPDATE_NAME",
  1067. `Updating station "${stationId}" name to "${newName}" failed. "${err}"`
  1068. );
  1069. return cb({ status: "failure", message: err });
  1070. }
  1071. this.log(
  1072. "SUCCESS",
  1073. "STATIONS_UPDATE_NAME",
  1074. `Updated station "${stationId}" name to "${newName}" successfully.`
  1075. );
  1076. CacheModule.runJob("PUB", {
  1077. channel: "station.nameUpdate",
  1078. value: { stationId, name: newName }
  1079. });
  1080. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1081. userId: session.userId,
  1082. type: "station__edit_name",
  1083. payload: {
  1084. message: `Changed name of station <stationId>${station.displayName}</stationId> to ${newName}`,
  1085. stationId
  1086. }
  1087. });
  1088. return cb({
  1089. status: "success",
  1090. message: "Successfully updated the name."
  1091. });
  1092. }
  1093. );
  1094. }),
  1095. /**
  1096. * Updates a station's display name
  1097. *
  1098. * @param session
  1099. * @param stationId - the station id
  1100. * @param newDisplayName - the new station display name
  1101. * @param cb
  1102. */
  1103. updateDisplayName: isOwnerRequired(async function updateDisplayName(session, stationId, newDisplayName, cb) {
  1104. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1105. async.waterfall(
  1106. [
  1107. next => {
  1108. stationModel.updateOne(
  1109. { _id: stationId },
  1110. { $set: { displayName: newDisplayName } },
  1111. { runValidators: true },
  1112. next
  1113. );
  1114. },
  1115. (res, next) => {
  1116. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1117. .then(station => next(null, station))
  1118. .catch(next);
  1119. }
  1120. ],
  1121. async err => {
  1122. if (err) {
  1123. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1124. this.log(
  1125. "ERROR",
  1126. "STATIONS_UPDATE_DISPLAY_NAME",
  1127. `Updating station "${stationId}" displayName to "${newDisplayName}" failed. "${err}"`
  1128. );
  1129. return cb({ status: "failure", message: err });
  1130. }
  1131. this.log(
  1132. "SUCCESS",
  1133. "STATIONS_UPDATE_DISPLAY_NAME",
  1134. `Updated station "${stationId}" displayName to "${newDisplayName}" successfully.`
  1135. );
  1136. CacheModule.runJob("PUB", {
  1137. channel: "station.displayNameUpdate",
  1138. value: { stationId, displayName: newDisplayName }
  1139. });
  1140. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1141. userId: session.userId,
  1142. type: "station__edit_display_name",
  1143. payload: {
  1144. message: `Changed display name of station <stationId>${newDisplayName}</stationId>`,
  1145. stationId
  1146. }
  1147. });
  1148. return cb({
  1149. status: "success",
  1150. message: "Successfully updated the display name."
  1151. });
  1152. }
  1153. );
  1154. }),
  1155. /**
  1156. * Updates a station's description
  1157. *
  1158. * @param session
  1159. * @param stationId - the station id
  1160. * @param newDescription - the new station description
  1161. * @param cb
  1162. */
  1163. updateDescription: isOwnerRequired(async function updateDescription(session, stationId, newDescription, cb) {
  1164. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1165. async.waterfall(
  1166. [
  1167. next => {
  1168. stationModel.updateOne(
  1169. { _id: stationId },
  1170. { $set: { description: newDescription } },
  1171. { runValidators: true },
  1172. next
  1173. );
  1174. },
  1175. (res, next) => {
  1176. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1177. .then(station => next(null, station))
  1178. .catch(next);
  1179. }
  1180. ],
  1181. async (err, station) => {
  1182. if (err) {
  1183. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1184. this.log(
  1185. "ERROR",
  1186. "STATIONS_UPDATE_DESCRIPTION",
  1187. `Updating station "${stationId}" description to "${newDescription}" failed. "${err}"`
  1188. );
  1189. return cb({ status: "failure", message: err });
  1190. }
  1191. this.log(
  1192. "SUCCESS",
  1193. "STATIONS_UPDATE_DESCRIPTION",
  1194. `Updated station "${stationId}" description to "${newDescription}" successfully.`
  1195. );
  1196. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1197. userId: session.userId,
  1198. type: "station__edit_description",
  1199. payload: {
  1200. message: `Changed description of station <stationId>${station.displayName}</stationId> to ${newDescription}`,
  1201. stationId
  1202. }
  1203. });
  1204. CacheModule.runJob("PUB", {
  1205. channel: "station.descriptionUpdate",
  1206. value: { stationId, description: newDescription }
  1207. });
  1208. return cb({
  1209. status: "success",
  1210. message: "Successfully updated the description."
  1211. });
  1212. }
  1213. );
  1214. }),
  1215. /**
  1216. * Updates a station's privacy
  1217. *
  1218. * @param session
  1219. * @param stationId - the station id
  1220. * @param newPrivacy - the new station privacy
  1221. * @param cb
  1222. */
  1223. updatePrivacy: isOwnerRequired(async function updatePrivacy(session, stationId, newPrivacy, cb) {
  1224. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1225. let previousPrivacy = null;
  1226. async.waterfall(
  1227. [
  1228. next => {
  1229. stationModel.findOne({ _id: stationId }, next);
  1230. },
  1231. (station, next) => {
  1232. if (!station) next("No station found.");
  1233. else {
  1234. previousPrivacy = station.privacy;
  1235. next();
  1236. }
  1237. },
  1238. next => {
  1239. stationModel.updateOne(
  1240. { _id: stationId },
  1241. { $set: { privacy: newPrivacy } },
  1242. { runValidators: true },
  1243. next
  1244. );
  1245. },
  1246. (res, next) => {
  1247. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1248. .then(station => next(null, station))
  1249. .catch(next);
  1250. }
  1251. ],
  1252. async (err, station) => {
  1253. if (err) {
  1254. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1255. this.log(
  1256. "ERROR",
  1257. "STATIONS_UPDATE_PRIVACY",
  1258. `Updating station "${stationId}" privacy to "${newPrivacy}" failed. "${err}"`
  1259. );
  1260. return cb({ status: "failure", message: err });
  1261. }
  1262. this.log(
  1263. "SUCCESS",
  1264. "STATIONS_UPDATE_PRIVACY",
  1265. `Updated station "${stationId}" privacy to "${newPrivacy}" successfully.`
  1266. );
  1267. CacheModule.runJob("PUB", {
  1268. channel: "station.privacyUpdate",
  1269. value: { stationId, previousPrivacy }
  1270. });
  1271. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1272. userId: session.userId,
  1273. type: "station__edit_privacy",
  1274. payload: {
  1275. message: `Changed privacy of station <stationId>${station.displayName}</stationId> to ${newPrivacy}`,
  1276. stationId
  1277. }
  1278. });
  1279. return cb({
  1280. status: "success",
  1281. message: "Successfully updated the privacy."
  1282. });
  1283. }
  1284. );
  1285. }),
  1286. /**
  1287. * Updates a station's genres
  1288. *
  1289. * @param session
  1290. * @param stationId - the station id
  1291. * @param newGenres - the new station genres
  1292. * @param cb
  1293. */
  1294. updateGenres: isOwnerRequired(async function updateGenres(session, stationId, newGenres, cb) {
  1295. const stationModel = await DBModule.runJob(
  1296. "GET_MODEL",
  1297. {
  1298. modelName: "station"
  1299. },
  1300. this
  1301. );
  1302. async.waterfall(
  1303. [
  1304. next => {
  1305. stationModel.updateOne(
  1306. { _id: stationId },
  1307. { $set: { genres: newGenres } },
  1308. { runValidators: true },
  1309. next
  1310. );
  1311. },
  1312. (res, next) => {
  1313. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1314. .then(station => next(null, station))
  1315. .catch(next);
  1316. }
  1317. ],
  1318. async (err, station) => {
  1319. if (err) {
  1320. err = await UtilsModule.runJob("GET_ERROR", { error: err });
  1321. this.log(
  1322. "ERROR",
  1323. "STATIONS_UPDATE_GENRES",
  1324. `Updating station "${stationId}" genres to "${newGenres}" failed. "${err}"`
  1325. );
  1326. return cb({ status: "failure", message: err });
  1327. }
  1328. this.log(
  1329. "SUCCESS",
  1330. "STATIONS_UPDATE_GENRES",
  1331. `Updated station "${stationId}" genres to "${newGenres}" successfully.`
  1332. );
  1333. if (newGenres.length > 0) {
  1334. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1335. userId: session.userId,
  1336. type: "station__edit_genres",
  1337. payload: {
  1338. message: `Updated genres of station <stationId>${station.displayName}</stationId> to
  1339. ${newGenres.join(", ")}`,
  1340. stationId
  1341. }
  1342. });
  1343. } else {
  1344. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1345. userId: session.userId,
  1346. type: "station__edit_genres",
  1347. payload: {
  1348. message: `Removed all genres of station <stationId>${station.displayName}</stationId>`,
  1349. stationId
  1350. }
  1351. });
  1352. }
  1353. return cb({
  1354. status: "success",
  1355. message: "Successfully updated the genres."
  1356. });
  1357. }
  1358. );
  1359. }),
  1360. /**
  1361. * Updates a station's blacklisted genres
  1362. *
  1363. * @param session
  1364. * @param stationId - the station id
  1365. * @param newBlacklistedGenres - the new station blacklisted genres
  1366. * @param cb
  1367. */
  1368. updateBlacklistedGenres: isOwnerRequired(async function updateBlacklistedGenres(
  1369. session,
  1370. stationId,
  1371. newBlacklistedGenres,
  1372. cb
  1373. ) {
  1374. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1375. async.waterfall(
  1376. [
  1377. next => {
  1378. stationModel.updateOne(
  1379. { _id: stationId },
  1380. {
  1381. $set: {
  1382. blacklistedGenres: newBlacklistedGenres
  1383. }
  1384. },
  1385. { runValidators: true },
  1386. next
  1387. );
  1388. },
  1389. (res, next) => {
  1390. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1391. .then(station => next(null, station))
  1392. .catch(next);
  1393. }
  1394. ],
  1395. async (err, station) => {
  1396. if (err) {
  1397. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1398. this.log(
  1399. "ERROR",
  1400. "STATIONS_UPDATE_BLACKLISTED_GENRES",
  1401. `Updating station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" failed. "${err}"`
  1402. );
  1403. return cb({ status: "failure", message: err });
  1404. }
  1405. this.log(
  1406. "SUCCESS",
  1407. "STATIONS_UPDATE_BLACKLISTED_GENRES",
  1408. `Updated station "${stationId}" blacklisted genres to "${newBlacklistedGenres}" successfully.`
  1409. );
  1410. if (newBlacklistedGenres.length > 0) {
  1411. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1412. userId: session.userId,
  1413. type: "station__edit_blacklisted_genres",
  1414. payload: {
  1415. message: `Updated blacklisted genres of station <stationId>${
  1416. station.displayName
  1417. }</stationId> to ${newBlacklistedGenres.join(", ")}`,
  1418. stationId
  1419. }
  1420. });
  1421. } else {
  1422. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1423. userId: session.userId,
  1424. type: "station__edit_blacklisted_genres",
  1425. payload: {
  1426. message: `Removed all blacklisted genres of station <stationId>${station.displayName}</stationId>`,
  1427. stationId
  1428. }
  1429. });
  1430. }
  1431. return cb({
  1432. status: "success",
  1433. message: "Successfully updated the blacklisted genres."
  1434. });
  1435. }
  1436. );
  1437. }),
  1438. /**
  1439. * Updates a station's party mode
  1440. *
  1441. * @param session
  1442. * @param stationId - the station id
  1443. * @param newPartyMode - the new station party mode
  1444. * @param cb
  1445. */
  1446. updatePartyMode: isOwnerRequired(async function updatePartyMode(session, stationId, newPartyMode, cb) {
  1447. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1448. async.waterfall(
  1449. [
  1450. next => {
  1451. StationsModule.runJob("GET_STATION", { stationId }, this)
  1452. .then(station => {
  1453. next(null, station);
  1454. })
  1455. .catch(next);
  1456. },
  1457. (station, next) => {
  1458. if (!station) return next("Station not found.");
  1459. if (station.partyMode === newPartyMode)
  1460. return next(`The party mode was already ${newPartyMode ? "enabled." : "disabled."}`);
  1461. return stationModel.updateOne(
  1462. { _id: stationId },
  1463. { $set: { partyMode: newPartyMode } },
  1464. { runValidators: true },
  1465. next
  1466. );
  1467. },
  1468. (res, next) => {
  1469. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1470. .then(station => {
  1471. next(null, station);
  1472. })
  1473. .catch(next);
  1474. }
  1475. ],
  1476. async err => {
  1477. if (err) {
  1478. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1479. this.log(
  1480. "ERROR",
  1481. "STATIONS_UPDATE_PARTY_MODE",
  1482. `Updating station "${stationId}" party mode to "${newPartyMode}" failed. "${err}"`
  1483. );
  1484. return cb({ status: "failure", message: err });
  1485. }
  1486. this.log(
  1487. "SUCCESS",
  1488. "STATIONS_UPDATE_PARTY_MODE",
  1489. `Updated station "${stationId}" party mode to "${newPartyMode}" successfully.`
  1490. );
  1491. CacheModule.runJob("PUB", {
  1492. channel: "station.updatePartyMode",
  1493. value: {
  1494. stationId,
  1495. partyMode: newPartyMode
  1496. }
  1497. });
  1498. StationsModule.runJob("SKIP_STATION", { stationId });
  1499. return cb({
  1500. status: "success",
  1501. message: "Successfully updated the party mode."
  1502. });
  1503. }
  1504. );
  1505. }),
  1506. /**
  1507. * Updates a station's theme
  1508. *
  1509. * @param session
  1510. * @param stationId - the station id
  1511. * @param newTheme - the new station theme
  1512. * @param cb
  1513. */
  1514. updateTheme: isOwnerRequired(async function updateTheme(session, stationId, newTheme, cb) {
  1515. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1516. async.waterfall(
  1517. [
  1518. next => {
  1519. StationsModule.runJob("GET_STATION", { stationId }, this)
  1520. .then(station => {
  1521. next(null, station);
  1522. })
  1523. .catch(next);
  1524. },
  1525. (station, next) => {
  1526. if (!station) return next("Station not found.");
  1527. if (station.theme === newTheme) return next("No change in theme.");
  1528. return stationModel.updateOne(
  1529. { _id: stationId },
  1530. { $set: { theme: newTheme } },
  1531. { runValidators: true },
  1532. next
  1533. );
  1534. },
  1535. (res, next) => {
  1536. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1537. .then(station => next(null, station))
  1538. .catch(next);
  1539. }
  1540. ],
  1541. async (err, station) => {
  1542. if (err) {
  1543. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1544. this.log(
  1545. "ERROR",
  1546. "STATIONS_UPDATE_THEME",
  1547. `Updating station "${stationId}" theme to "${newTheme}" failed. "${err}"`
  1548. );
  1549. return cb({ status: "failure", message: err });
  1550. }
  1551. this.log(
  1552. "SUCCESS",
  1553. "STATIONS_UPDATE_THEME",
  1554. `Updated station "${stationId}" theme to "${newTheme}" successfully.`
  1555. );
  1556. CacheModule.runJob("PUB", {
  1557. channel: "station.themeUpdate",
  1558. value: { stationId }
  1559. });
  1560. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1561. userId: session.userId,
  1562. type: "station__edit_theme",
  1563. payload: {
  1564. message: `Changed theme of station <stationId>${station.displayName}</stationId> to ${newTheme}`,
  1565. stationId
  1566. }
  1567. });
  1568. return cb({
  1569. status: "success",
  1570. message: "Successfully updated the theme."
  1571. });
  1572. }
  1573. );
  1574. }),
  1575. /**
  1576. * Pauses a station
  1577. *
  1578. * @param session
  1579. * @param stationId - the station id
  1580. * @param cb
  1581. */
  1582. pause: isOwnerRequired(async function pause(session, stationId, cb) {
  1583. const stationModel = await DBModule.runJob(
  1584. "GET_MODEL",
  1585. {
  1586. modelName: "station"
  1587. },
  1588. this
  1589. );
  1590. async.waterfall(
  1591. [
  1592. next => {
  1593. StationsModule.runJob("GET_STATION", { stationId }, this)
  1594. .then(station => {
  1595. next(null, station);
  1596. })
  1597. .catch(next);
  1598. },
  1599. (station, next) => {
  1600. if (!station) return next("Station not found.");
  1601. if (station.paused) return next("That station was already paused.");
  1602. return stationModel.updateOne(
  1603. { _id: stationId },
  1604. { $set: { paused: true, pausedAt: Date.now() } },
  1605. next
  1606. );
  1607. },
  1608. (res, next) => {
  1609. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1610. .then(station => {
  1611. next(null, station);
  1612. })
  1613. .catch(next);
  1614. }
  1615. ],
  1616. async err => {
  1617. if (err) {
  1618. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1619. this.log("ERROR", "STATIONS_PAUSE", `Pausing station "${stationId}" failed. "${err}"`);
  1620. return cb({ status: "failure", message: err });
  1621. }
  1622. this.log("SUCCESS", "STATIONS_PAUSE", `Paused station "${stationId}" successfully.`);
  1623. CacheModule.runJob("PUB", {
  1624. channel: "station.pause",
  1625. value: stationId
  1626. });
  1627. NotificationsModule.runJob("UNSCHEDULE", {
  1628. name: `stations.nextSong?id=${stationId}`
  1629. });
  1630. return cb({
  1631. status: "success",
  1632. message: "Successfully paused."
  1633. });
  1634. }
  1635. );
  1636. }),
  1637. /**
  1638. * Resumes a station
  1639. *
  1640. * @param session
  1641. * @param stationId - the station id
  1642. * @param cb
  1643. */
  1644. resume: isOwnerRequired(async function resume(session, stationId, cb) {
  1645. const stationModel = await DBModule.runJob(
  1646. "GET_MODEL",
  1647. {
  1648. modelName: "station"
  1649. },
  1650. this
  1651. );
  1652. async.waterfall(
  1653. [
  1654. next => {
  1655. StationsModule.runJob("GET_STATION", { stationId }, this)
  1656. .then(station => {
  1657. next(null, station);
  1658. })
  1659. .catch(next);
  1660. },
  1661. (station, next) => {
  1662. if (!station) return next("Station not found.");
  1663. if (!station.paused) return next("That station is not paused.");
  1664. station.timePaused += Date.now() - station.pausedAt;
  1665. return stationModel.updateOne(
  1666. { _id: stationId },
  1667. {
  1668. $set: { paused: false },
  1669. $inc: { timePaused: Date.now() - station.pausedAt }
  1670. },
  1671. next
  1672. );
  1673. },
  1674. (res, next) => {
  1675. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  1676. .then(station => {
  1677. next(null, station);
  1678. })
  1679. .catch(next);
  1680. }
  1681. ],
  1682. async err => {
  1683. if (err) {
  1684. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1685. this.log("ERROR", "STATIONS_RESUME", `Resuming station "${stationId}" failed. "${err}"`);
  1686. return cb({ status: "failure", message: err });
  1687. }
  1688. this.log("SUCCESS", "STATIONS_RESUME", `Resuming station "${stationId}" successfully.`);
  1689. CacheModule.runJob("PUB", {
  1690. channel: "station.resume",
  1691. value: stationId
  1692. });
  1693. return cb({
  1694. status: "success",
  1695. message: "Successfully resumed."
  1696. });
  1697. }
  1698. );
  1699. }),
  1700. /**
  1701. * Removes a station
  1702. *
  1703. * @param session
  1704. * @param stationId - the station id
  1705. * @param cb
  1706. */
  1707. remove: isOwnerRequired(async function remove(session, stationId, cb) {
  1708. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1709. async.waterfall(
  1710. [
  1711. next => {
  1712. stationModel.findById(stationId, (err, station) => {
  1713. if (err) return next(err);
  1714. return next(null, station);
  1715. });
  1716. },
  1717. (station, next) => {
  1718. stationModel.deleteOne({ _id: stationId }, err => next(err, station));
  1719. },
  1720. (station, next) => {
  1721. CacheModule.runJob("HDEL", { table: "stations", key: stationId }, this)
  1722. .then(next(null, station))
  1723. .catch(next);
  1724. }
  1725. ],
  1726. async (err, station) => {
  1727. if (err) {
  1728. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1729. this.log("ERROR", "STATIONS_REMOVE", `Removing station "${stationId}" failed. "${err}"`);
  1730. return cb({ status: "failure", message: err });
  1731. }
  1732. this.log("SUCCESS", "STATIONS_REMOVE", `Removing station "${stationId}" successfully.`);
  1733. CacheModule.runJob("PUB", {
  1734. channel: "station.remove",
  1735. value: stationId
  1736. });
  1737. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1738. userId: session.userId,
  1739. type: "station__remove",
  1740. payload: { message: `Removed a station named <stationId>${station.displayName}</stationId>` }
  1741. });
  1742. return cb({
  1743. status: "success",
  1744. message: "Successfully removed."
  1745. });
  1746. }
  1747. );
  1748. }),
  1749. /**
  1750. * Create a station
  1751. *
  1752. * @param session
  1753. * @param data - the station data
  1754. * @param cb
  1755. */
  1756. create: isLoginRequired(async function create(session, data, cb) {
  1757. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1758. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  1759. data.name = data.name.toLowerCase();
  1760. const blacklist = [
  1761. "country",
  1762. "edm",
  1763. "musare",
  1764. "hip-hop",
  1765. "rap",
  1766. "top-hits",
  1767. "todays-hits",
  1768. "old-school",
  1769. "christmas",
  1770. "about",
  1771. "support",
  1772. "staff",
  1773. "help",
  1774. "news",
  1775. "terms",
  1776. "privacy",
  1777. "profile",
  1778. "c",
  1779. "community",
  1780. "tos",
  1781. "login",
  1782. "register",
  1783. "p",
  1784. "official",
  1785. "o",
  1786. "trap",
  1787. "faq",
  1788. "team",
  1789. "donate",
  1790. "buy",
  1791. "shop",
  1792. "forums",
  1793. "explore",
  1794. "settings",
  1795. "admin",
  1796. "auth",
  1797. "reset_password"
  1798. ];
  1799. async.waterfall(
  1800. [
  1801. next => {
  1802. if (!data) return next("Invalid data.");
  1803. return next();
  1804. },
  1805. next => {
  1806. stationModel.findOne(
  1807. {
  1808. $or: [
  1809. { name: data.name },
  1810. {
  1811. displayName: new RegExp(`^${data.displayName}$`, "i")
  1812. }
  1813. ]
  1814. },
  1815. next
  1816. );
  1817. },
  1818. // eslint-disable-next-line consistent-return
  1819. (station, next) => {
  1820. this.log(station);
  1821. if (station) return next("A station with that name or display name already exists.");
  1822. const { name, displayName, description, genres, playlist, type, blacklistedGenres } = data;
  1823. if (type === "official") {
  1824. return userModel.findOne({ _id: session.userId }, (err, user) => {
  1825. if (err) return next(err);
  1826. if (!user) return next("User not found.");
  1827. if (user.role !== "admin") return next("Admin required.");
  1828. return stationModel.create(
  1829. {
  1830. name,
  1831. displayName,
  1832. description,
  1833. type,
  1834. privacy: "private",
  1835. playlist,
  1836. genres,
  1837. blacklistedGenres,
  1838. currentSong: StationsModule.defaultSong
  1839. },
  1840. next
  1841. );
  1842. });
  1843. }
  1844. if (type === "community") {
  1845. if (blacklist.indexOf(name) !== -1)
  1846. return next("That name is blacklisted. Please use a different name.");
  1847. return stationModel.create(
  1848. {
  1849. name,
  1850. displayName,
  1851. description,
  1852. type,
  1853. privacy: "private",
  1854. owner: session.userId,
  1855. queue: [],
  1856. currentSong: null
  1857. },
  1858. next
  1859. );
  1860. }
  1861. }
  1862. ],
  1863. async (err, station) => {
  1864. if (err) {
  1865. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1866. this.log("ERROR", "STATIONS_CREATE", `Creating station failed. "${err}"`);
  1867. return cb({ status: "failure", message: err });
  1868. }
  1869. this.log("SUCCESS", "STATIONS_CREATE", `Created station "${station._id}" successfully.`);
  1870. CacheModule.runJob("PUB", {
  1871. channel: "station.create",
  1872. value: station._id
  1873. });
  1874. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1875. userId: session.userId,
  1876. type: "station__create",
  1877. payload: {
  1878. message: `Created a station named <stationId>${station.displayName}</stationId>`,
  1879. stationId: station._id
  1880. }
  1881. });
  1882. return cb({
  1883. status: "success",
  1884. message: "Successfully created station."
  1885. });
  1886. }
  1887. );
  1888. }),
  1889. /**
  1890. * Adds song to station queue
  1891. *
  1892. * @param session
  1893. * @param stationId - the station id
  1894. * @param songId - the song id
  1895. * @param cb
  1896. */
  1897. addToQueue: isLoginRequired(async function addToQueue(session, stationId, songId, cb) {
  1898. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1899. const stationModel = await DBModule.runJob(
  1900. "GET_MODEL",
  1901. {
  1902. modelName: "station"
  1903. },
  1904. this
  1905. );
  1906. async.waterfall(
  1907. [
  1908. next => {
  1909. StationsModule.runJob("GET_STATION", { stationId }, this)
  1910. .then(station => {
  1911. next(null, station);
  1912. })
  1913. .catch(next);
  1914. },
  1915. (station, next) => {
  1916. if (!station) return next("Station not found.");
  1917. if (station.locked) {
  1918. return userModel.findOne({ _id: session.userId }, (err, user) => {
  1919. if (user.role !== "admin" && station.owner !== session.userId)
  1920. return next("Only owners and admins can add songs to a locked queue.");
  1921. return next(null, station);
  1922. });
  1923. }
  1924. return next(null, station);
  1925. },
  1926. (station, next) => {
  1927. if (station.type !== "community") return next("That station is not a community station.");
  1928. return StationsModule.runJob(
  1929. "CAN_USER_VIEW_STATION",
  1930. {
  1931. station,
  1932. userId: session.userId
  1933. },
  1934. this
  1935. )
  1936. .then(canView => {
  1937. if (canView) return next(null, station);
  1938. return next("Insufficient permissions.");
  1939. })
  1940. .catch(err => next(err));
  1941. },
  1942. (station, next) => {
  1943. if (station.currentSong && station.currentSong.songId === songId)
  1944. return next("That song is currently playing.");
  1945. return async.each(
  1946. station.queue,
  1947. (queueSong, next) => {
  1948. if (queueSong.songId === songId) return next("That song is already in the queue.");
  1949. return next();
  1950. },
  1951. err => next(err, station)
  1952. );
  1953. },
  1954. (station, next) => {
  1955. SongsModule.runJob("GET_SONG_FROM_ID", { songId }, this)
  1956. .then(res => {
  1957. if (res.song) return next(null, res.song, station);
  1958. return YouTubeModule.runJob("GET_SONG", { songId }, this)
  1959. .then(response => {
  1960. const { song } = response;
  1961. song.artists = [];
  1962. song.skipDuration = 0;
  1963. song.likes = -1;
  1964. song.dislikes = -1;
  1965. song.thumbnail = "empty";
  1966. song.explicit = false;
  1967. return next(null, song, station);
  1968. })
  1969. .catch(err => next(err));
  1970. })
  1971. .catch(err => next(err));
  1972. },
  1973. (song, station, next) => {
  1974. const { queue } = station;
  1975. song.requestedBy = session.userId;
  1976. song.requestedAt = Date.now();
  1977. queue.push(song);
  1978. let totalDuration = 0;
  1979. queue.forEach(song => {
  1980. totalDuration += song.duration;
  1981. });
  1982. if (totalDuration >= 3600 * 3) return next("The max length of the queue is 3 hours.");
  1983. return next(null, song, station);
  1984. },
  1985. (song, station, next) => {
  1986. const { queue } = station;
  1987. if (queue.length === 0) return next(null, song, station);
  1988. let totalDuration = 0;
  1989. const userId = queue[queue.length - 1].requestedBy;
  1990. station.queue.forEach(song => {
  1991. if (userId === song.requestedBy) {
  1992. totalDuration += song.duration;
  1993. }
  1994. });
  1995. if (totalDuration >= 900) return next("The max length of songs per user is 15 minutes.");
  1996. return next(null, song, station);
  1997. },
  1998. (song, station, next) => {
  1999. const { queue } = station;
  2000. if (queue.length === 0) return next(null, song);
  2001. let totalSongs = 0;
  2002. const userId = queue[queue.length - 1].requestedBy;
  2003. queue.forEach(song => {
  2004. if (userId === song.requestedBy) {
  2005. totalSongs += 1;
  2006. }
  2007. });
  2008. if (totalSongs <= 2) return next(null, song);
  2009. if (totalSongs > 3)
  2010. return next("The max amount of songs per user is 3, and only 2 in a row is allowed.");
  2011. if (queue[queue.length - 2].requestedBy !== userId || queue[queue.length - 3] !== userId)
  2012. return next("The max amount of songs per user is 3, and only 2 in a row is allowed.");
  2013. return next(null, song);
  2014. },
  2015. (song, next) => {
  2016. stationModel.updateOne(
  2017. { _id: stationId },
  2018. { $pushr: { queue: song } },
  2019. { runValidators: true },
  2020. next
  2021. );
  2022. },
  2023. (res, next) => {
  2024. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  2025. .then(station => next(null, station))
  2026. .catch(next);
  2027. }
  2028. ],
  2029. async err => {
  2030. if (err) {
  2031. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2032. this.log(
  2033. "ERROR",
  2034. "STATIONS_ADD_SONG_TO_QUEUE",
  2035. `Adding song "${songId}" to station "${stationId}" queue failed. "${err}"`
  2036. );
  2037. return cb({ status: "failure", message: err });
  2038. }
  2039. this.log(
  2040. "SUCCESS",
  2041. "STATIONS_ADD_SONG_TO_QUEUE",
  2042. `Added song "${songId}" to station "${stationId}" successfully.`
  2043. );
  2044. CacheModule.runJob("PUB", {
  2045. channel: "station.queueUpdate",
  2046. value: stationId
  2047. });
  2048. return cb({
  2049. status: "success",
  2050. message: "Successfully added song to queue."
  2051. });
  2052. }
  2053. );
  2054. }),
  2055. /**
  2056. * Removes song from station queue
  2057. *
  2058. * @param session
  2059. * @param stationId - the station id
  2060. * @param songId - the song id
  2061. * @param cb
  2062. */
  2063. removeFromQueue: isOwnerRequired(async function removeFromQueue(session, stationId, songId, cb) {
  2064. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  2065. async.waterfall(
  2066. [
  2067. next => {
  2068. if (!songId) return next("Invalid song id.");
  2069. return StationsModule.runJob("GET_STATION", { stationId }, this)
  2070. .then(station => next(null, station))
  2071. .catch(next);
  2072. },
  2073. (station, next) => {
  2074. if (!station) return next("Station not found.");
  2075. if (station.type !== "community") return next("Station is not a community station.");
  2076. return async.each(
  2077. station.queue,
  2078. (queueSong, next) => {
  2079. if (queueSong.songId === songId) return next(true);
  2080. return next();
  2081. },
  2082. err => {
  2083. if (err === true) return next();
  2084. return next("Song is not currently in the queue.");
  2085. }
  2086. );
  2087. },
  2088. next => {
  2089. stationModel.updateOne({ _id: stationId }, { $pull: { queue: { songId } } }, next);
  2090. },
  2091. (res, next) => {
  2092. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  2093. .then(station => next(null, station))
  2094. .catch(next);
  2095. }
  2096. ],
  2097. async err => {
  2098. if (err) {
  2099. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2100. this.log(
  2101. "ERROR",
  2102. "STATIONS_REMOVE_SONG_TO_QUEUE",
  2103. `Removing song "${songId}" from station "${stationId}" queue failed. "${err}"`
  2104. );
  2105. return cb({ status: "failure", message: err });
  2106. }
  2107. this.log(
  2108. "SUCCESS",
  2109. "STATIONS_REMOVE_SONG_TO_QUEUE",
  2110. `Removed song "${songId}" from station "${stationId}" successfully.`
  2111. );
  2112. CacheModule.runJob("PUB", {
  2113. channel: "station.queueUpdate",
  2114. value: stationId
  2115. });
  2116. return cb({
  2117. status: "success",
  2118. message: "Successfully removed song from queue."
  2119. });
  2120. }
  2121. );
  2122. }),
  2123. /**
  2124. * Gets the queue from a station
  2125. *
  2126. * @param {object} session - user session
  2127. * @param {string} stationId - the station id
  2128. * @param {Function} cb - callback
  2129. */
  2130. getQueue(session, stationId, cb) {
  2131. async.waterfall(
  2132. [
  2133. next => {
  2134. StationsModule.runJob("GET_STATION", { stationId }, this)
  2135. .then(station => next(null, station))
  2136. .catch(next);
  2137. },
  2138. (station, next) => {
  2139. if (!station) return next("Station not found.");
  2140. if (station.type !== "community") return next("Station is not a community station.");
  2141. return next(null, station);
  2142. },
  2143. (station, next) => {
  2144. StationsModule.runJob("CAN_USER_VIEW_STATION", { station, userId: session.userId }, this)
  2145. .then(canView => {
  2146. if (canView) return next(null, station);
  2147. return next("Insufficient permissions.");
  2148. })
  2149. .catch(err => next(err));
  2150. }
  2151. ],
  2152. async (err, station) => {
  2153. if (err) {
  2154. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2155. this.log(
  2156. "ERROR",
  2157. "STATIONS_GET_QUEUE",
  2158. `Getting queue for station "${stationId}" failed. "${err}"`
  2159. );
  2160. return cb({ status: "failure", message: err });
  2161. }
  2162. this.log("SUCCESS", "STATIONS_GET_QUEUE", `Got queue for station "${stationId}" successfully.`);
  2163. return cb({
  2164. status: "success",
  2165. message: "Successfully got queue.",
  2166. queue: station.queue
  2167. });
  2168. }
  2169. );
  2170. },
  2171. /**
  2172. * Selects a private playlist for a station
  2173. *
  2174. * @param session
  2175. * @param stationId - the station id
  2176. * @param playlistId - the private playlist id
  2177. * @param cb
  2178. */
  2179. selectPrivatePlaylist: isOwnerRequired(async function selectPrivatePlaylist(session, stationId, playlistId, cb) {
  2180. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  2181. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  2182. async.waterfall(
  2183. [
  2184. next => {
  2185. StationsModule.runJob("GET_STATION", { stationId }, this)
  2186. .then(station => next(null, station))
  2187. .catch(next);
  2188. },
  2189. (station, next) => {
  2190. if (!station) return next("Station not found.");
  2191. if (station.type !== "community") return next("Station is not a community station.");
  2192. if (station.privatePlaylist === playlistId)
  2193. return next("That private playlist is already selected.");
  2194. return playlistModel.findOne({ _id: playlistId }, next);
  2195. },
  2196. (playlist, next) => {
  2197. if (!playlist) return next("Playlist not found.");
  2198. const currentSongIndex = playlist.songs.length > 0 ? playlist.songs.length - 1 : 0;
  2199. return stationModel.updateOne(
  2200. { _id: stationId },
  2201. {
  2202. $set: {
  2203. privatePlaylist: playlistId,
  2204. currentSongIndex
  2205. }
  2206. },
  2207. { runValidators: true },
  2208. next
  2209. );
  2210. },
  2211. (res, next) => {
  2212. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  2213. .then(station => next(null, station))
  2214. .catch(next);
  2215. }
  2216. ],
  2217. async (err, station) => {
  2218. if (err) {
  2219. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2220. this.log(
  2221. "ERROR",
  2222. "STATIONS_SELECT_PRIVATE_PLAYLIST",
  2223. `Selecting private playlist "${playlistId}" for station "${stationId}" failed. "${err}"`
  2224. );
  2225. return cb({ status: "failure", message: err });
  2226. }
  2227. this.log(
  2228. "SUCCESS",
  2229. "STATIONS_SELECT_PRIVATE_PLAYLIST",
  2230. `Selected private playlist "${playlistId}" for station "${stationId}" successfully.`
  2231. );
  2232. NotificationsModule.runJob("UNSCHEDULE", {
  2233. name: `stations.nextSong?id${stationId}`
  2234. });
  2235. if (!station.partyMode) StationsModule.runJob("SKIP_STATION", { stationId });
  2236. CacheModule.runJob("PUB", {
  2237. channel: "privatePlaylist.selected",
  2238. value: {
  2239. playlistId,
  2240. stationId
  2241. }
  2242. });
  2243. return cb({
  2244. status: "success",
  2245. message: "Successfully selected playlist."
  2246. });
  2247. }
  2248. );
  2249. }),
  2250. /**
  2251. * Deselects the private playlist selected in a station
  2252. *
  2253. * @param session
  2254. * @param stationId - the station id
  2255. * @param cb
  2256. */
  2257. deselectPrivatePlaylist: isOwnerRequired(async function deselectPrivatePlaylist(session, stationId, cb) {
  2258. const stationModel = await DBModule.runJob(
  2259. "GET_MODEL",
  2260. {
  2261. modelName: "station"
  2262. },
  2263. this
  2264. );
  2265. async.waterfall(
  2266. [
  2267. next => {
  2268. StationsModule.runJob("GET_STATION", { stationId }, this)
  2269. .then(station => {
  2270. next(null, station);
  2271. })
  2272. .catch(next);
  2273. },
  2274. (station, next) => {
  2275. if (!station) return next("Station not found.");
  2276. if (station.type !== "community") return next("Station is not a community station.");
  2277. if (!station.privatePlaylist) return next("No private playlist is currently selected.");
  2278. return stationModel.updateOne(
  2279. { _id: stationId },
  2280. {
  2281. $set: {
  2282. privatePlaylist: null,
  2283. currentSongIndex: 0
  2284. }
  2285. },
  2286. { runValidators: true },
  2287. next
  2288. );
  2289. },
  2290. (res, next) => {
  2291. StationsModule.runJob("UPDATE_STATION", { stationId }, this)
  2292. .then(station => {
  2293. next(null, station);
  2294. })
  2295. .catch(next);
  2296. }
  2297. ],
  2298. async (err, station) => {
  2299. if (err) {
  2300. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2301. this.log(
  2302. "ERROR",
  2303. "STATIONS_DESELECT_PRIVATE_PLAYLIST",
  2304. `Deselecting private playlist for station "${stationId}" failed. "${err}"`
  2305. );
  2306. return cb({ status: "failure", message: err });
  2307. }
  2308. this.log(
  2309. "SUCCESS",
  2310. "STATIONS_DESELECT_PRIVATE_PLAYLIST",
  2311. `Deselected private playlist for station "${stationId}" successfully.`
  2312. );
  2313. NotificationsModule.runJob("UNSCHEDULE", {
  2314. name: `stations.nextSong?id${stationId}`
  2315. });
  2316. if (!station.partyMode) StationsModule.runJob("SKIP_STATION", { stationId });
  2317. CacheModule.runJob("PUB", {
  2318. channel: "privatePlaylist.deselected",
  2319. value: {
  2320. stationId
  2321. }
  2322. });
  2323. return cb({
  2324. status: "success",
  2325. message: "Successfully deselected playlist."
  2326. });
  2327. }
  2328. );
  2329. }),
  2330. favoriteStation: isLoginRequired(async function favoriteStation(session, stationId, cb) {
  2331. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  2332. async.waterfall(
  2333. [
  2334. next => {
  2335. StationsModule.runJob("GET_STATION", { stationId }, this)
  2336. .then(station => next(null, station))
  2337. .catch(next);
  2338. },
  2339. (station, next) => {
  2340. if (!station) return next("Station not found.");
  2341. return StationsModule.runJob("CAN_USER_VIEW_STATION", { station, userId: session.userId }, this)
  2342. .then(canView => {
  2343. if (canView) return next(null, station);
  2344. return next("Insufficient permissions.");
  2345. })
  2346. .catch(err => next(err));
  2347. },
  2348. (station, next) => {
  2349. userModel.updateOne(
  2350. { _id: session.userId },
  2351. { $addToSet: { favoriteStations: stationId } },
  2352. (err, res) => next(err, station, res)
  2353. );
  2354. },
  2355. (station, res, next) => {
  2356. if (res.nModified === 0) return next("The station was already favorited.");
  2357. return next(null, station);
  2358. }
  2359. ],
  2360. async (err, station) => {
  2361. if (err) {
  2362. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2363. this.log("ERROR", "FAVORITE_STATION", `Favoriting station "${stationId}" failed. "${err}"`);
  2364. return cb({ status: "failure", message: err });
  2365. }
  2366. this.log("SUCCESS", "FAVORITE_STATION", `Favorited station "${stationId}" successfully.`);
  2367. CacheModule.runJob("PUB", {
  2368. channel: "user.favoritedStation",
  2369. value: {
  2370. userId: session.userId,
  2371. stationId
  2372. }
  2373. });
  2374. ActivitiesModule.runJob("ADD_ACTIVITY", {
  2375. userId: session.userId,
  2376. type: "station__favorite",
  2377. payload: {
  2378. message: `Favorited station <stationId>${station.displayName}</stationId>`,
  2379. stationId
  2380. }
  2381. });
  2382. return cb({
  2383. status: "success",
  2384. message: "Succesfully favorited station."
  2385. });
  2386. }
  2387. );
  2388. }),
  2389. unfavoriteStation: isLoginRequired(async function unfavoriteStation(session, stationId, cb) {
  2390. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  2391. async.waterfall(
  2392. [
  2393. next => {
  2394. userModel.updateOne({ _id: session.userId }, { $pull: { favoriteStations: stationId } }, next);
  2395. },
  2396. (res, next) => {
  2397. if (res.nModified === 0) return next("The station wasn't favorited.");
  2398. return next();
  2399. },
  2400. next => {
  2401. StationsModule.runJob("GET_STATION", { stationId }, this)
  2402. .then(station => next(null, station))
  2403. .catch(next);
  2404. }
  2405. ],
  2406. async (err, station) => {
  2407. if (err) {
  2408. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2409. this.log("ERROR", "UNFAVORITE_STATION", `Unfavoriting station "${stationId}" failed. "${err}"`);
  2410. return cb({ status: "failure", message: err });
  2411. }
  2412. this.log("SUCCESS", "UNFAVORITE_STATION", `Unfavorited station "${stationId}" successfully.`);
  2413. CacheModule.runJob("PUB", {
  2414. channel: "user.unfavoritedStation",
  2415. value: {
  2416. userId: session.userId,
  2417. stationId
  2418. }
  2419. });
  2420. ActivitiesModule.runJob("ADD_ACTIVITY", {
  2421. userId: session.userId,
  2422. type: "station__unfavorite",
  2423. payload: {
  2424. message: `Unfavorited station <stationId>${station.displayName}</stationId>`,
  2425. stationId
  2426. }
  2427. });
  2428. return cb({
  2429. status: "success",
  2430. message: "Succesfully unfavorited station."
  2431. });
  2432. }
  2433. );
  2434. })
  2435. };