playlists.js 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564
  1. import async from "async";
  2. import config from "config";
  3. import isLoginRequired from "../hooks/loginRequired";
  4. import { hasPermission, useHasPermission } from "../hooks/hasPermission";
  5. // eslint-disable-next-line
  6. import moduleManager from "../../index";
  7. const DBModule = moduleManager.modules.db;
  8. const UtilsModule = moduleManager.modules.utils;
  9. const WSModule = moduleManager.modules.ws;
  10. const SongsModule = moduleManager.modules.songs;
  11. const CacheModule = moduleManager.modules.cache;
  12. const PlaylistsModule = moduleManager.modules.playlists;
  13. const YouTubeModule = moduleManager.modules.youtube;
  14. const ActivitiesModule = moduleManager.modules.activities;
  15. const MediaModule = moduleManager.modules.media;
  16. CacheModule.runJob("SUB", {
  17. channel: "playlist.create",
  18. cb: playlist => {
  19. WSModule.runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy }, this).then(sockets =>
  20. sockets.forEach(socket => socket.dispatch("event:playlist.created", { data: { playlist } }))
  21. );
  22. if (playlist.privacy === "public")
  23. WSModule.runJob("EMIT_TO_ROOM", {
  24. room: `profile.${playlist.createdBy}.playlists`,
  25. args: ["event:playlist.created", { data: { playlist } }]
  26. });
  27. WSModule.runJob("EMIT_TO_ROOM", {
  28. room: "admin.playlists",
  29. args: ["event:admin.playlist.created", { data: { playlist } }]
  30. });
  31. }
  32. });
  33. CacheModule.runJob("SUB", {
  34. channel: "playlist.delete",
  35. cb: res => {
  36. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  37. sockets.forEach(socket => {
  38. socket.dispatch("event:playlist.deleted", { data: { playlistId: res.playlistId } });
  39. });
  40. });
  41. WSModule.runJob("EMIT_TO_ROOM", {
  42. room: `profile.${res.userId}.playlists`,
  43. args: ["event:playlist.deleted", { data: { playlistId: res.playlistId } }]
  44. });
  45. WSModule.runJob("EMIT_TO_ROOM", {
  46. room: "admin.playlists",
  47. args: ["event:admin.playlist.deleted", { data: { playlistId: res.playlistId } }]
  48. });
  49. }
  50. });
  51. CacheModule.runJob("SUB", {
  52. channel: "playlist.repositionSong",
  53. cb: res => {
  54. const { userId, playlistId, song } = res;
  55. WSModule.runJob("SOCKETS_FROM_USER", { userId }, this).then(sockets =>
  56. sockets.forEach(socket =>
  57. socket.dispatch("event:playlist.song.repositioned", {
  58. data: { playlistId, song }
  59. })
  60. )
  61. );
  62. }
  63. });
  64. CacheModule.runJob("SUB", {
  65. channel: "playlist.addSong",
  66. cb: res => {
  67. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  68. sockets.forEach(socket => {
  69. socket.dispatch("event:playlist.song.added", {
  70. data: {
  71. playlistId: res.playlistId,
  72. song: res.song
  73. }
  74. });
  75. });
  76. });
  77. if (res.privacy === "public")
  78. WSModule.runJob("EMIT_TO_ROOM", {
  79. room: `profile.${res.userId}.playlists`,
  80. args: [
  81. "event:playlist.song.added",
  82. {
  83. data: {
  84. playlistId: res.playlistId,
  85. song: res.song
  86. }
  87. }
  88. ]
  89. });
  90. WSModule.runJob("EMIT_TO_ROOM", {
  91. room: "admin.playlists",
  92. args: ["event:admin.playlist.song.added", { data: { playlistId: res.playlistId, song: res.song } }]
  93. });
  94. }
  95. });
  96. CacheModule.runJob("SUB", {
  97. channel: "playlist.removeSong",
  98. cb: res => {
  99. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  100. sockets.forEach(socket => {
  101. socket.dispatch("event:playlist.song.removed", {
  102. data: {
  103. playlistId: res.playlistId,
  104. youtubeId: res.youtubeId
  105. }
  106. });
  107. });
  108. });
  109. if (res.privacy === "public")
  110. WSModule.runJob("EMIT_TO_ROOM", {
  111. room: `profile.${res.userId}.playlists`,
  112. args: [
  113. "event:playlist.song.removed",
  114. {
  115. data: {
  116. playlistId: res.playlistId,
  117. youtubeId: res.youtubeId
  118. }
  119. }
  120. ]
  121. });
  122. WSModule.runJob("EMIT_TO_ROOM", {
  123. room: "admin.playlists",
  124. args: [
  125. "event:admin.playlist.song.removed",
  126. { data: { playlistId: res.playlistId, youtubeId: res.youtubeId } }
  127. ]
  128. });
  129. }
  130. });
  131. CacheModule.runJob("SUB", {
  132. channel: "playlist.updateDisplayName",
  133. cb: res => {
  134. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  135. sockets.forEach(socket => {
  136. socket.dispatch("event:playlist.displayName.updated", {
  137. data: {
  138. playlistId: res.playlistId,
  139. displayName: res.displayName
  140. }
  141. });
  142. });
  143. });
  144. if (res.privacy === "public")
  145. WSModule.runJob("EMIT_TO_ROOM", {
  146. room: `profile.${res.userId}.playlists`,
  147. args: [
  148. "event:playlist.displayName.updated",
  149. {
  150. data: {
  151. playlistId: res.playlistId,
  152. displayName: res.displayName
  153. }
  154. }
  155. ]
  156. });
  157. WSModule.runJob("EMIT_TO_ROOM", {
  158. room: "admin.playlists",
  159. args: [
  160. "event:admin.playlist.displayName.updated",
  161. { data: { playlistId: res.playlistId, displayName: res.displayName } }
  162. ]
  163. });
  164. }
  165. });
  166. CacheModule.runJob("SUB", {
  167. channel: "playlist.updatePrivacy",
  168. cb: res => {
  169. WSModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(sockets => {
  170. sockets.forEach(socket => {
  171. socket.dispatch("event:playlist.privacy.updated", {
  172. data: {
  173. playlist: res.playlist
  174. }
  175. });
  176. });
  177. });
  178. WSModule.runJob("EMIT_TO_ROOM", {
  179. room: "admin.playlists",
  180. args: [
  181. "event:admin.playlist.privacy.updated",
  182. { data: { playlistId: res.playlist._id, privacy: res.playlist.privacy } }
  183. ]
  184. });
  185. if (res.playlist.privacy === "public")
  186. return WSModule.runJob("EMIT_TO_ROOM", {
  187. room: `profile.${res.userId}.playlists`,
  188. args: [
  189. "event:playlist.created",
  190. {
  191. data: {
  192. playlist: res.playlist
  193. }
  194. }
  195. ]
  196. });
  197. return WSModule.runJob("EMIT_TO_ROOM", {
  198. room: `profile.${res.userId}.playlists`,
  199. args: [
  200. "event:playlist.deleted",
  201. {
  202. data: {
  203. playlistId: res.playlist._id
  204. }
  205. }
  206. ]
  207. });
  208. }
  209. });
  210. CacheModule.runJob("SUB", {
  211. channel: "playlist.updated",
  212. cb: async data => {
  213. const playlistModel = await DBModule.runJob("GET_MODEL", {
  214. modelName: "playlist"
  215. });
  216. playlistModel.findOne(
  217. { _id: data.playlistId },
  218. ["_id", "displayName", "type", "privacy", "songs", "createdBy", "createdAt", "createdFor"],
  219. (err, playlist) => {
  220. const newPlaylist = {
  221. ...playlist._doc,
  222. songsCount: playlist.songs.length,
  223. songsLength: playlist.songs.reduce(
  224. (previous, current) => ({
  225. duration: previous.duration + current.duration
  226. }),
  227. { duration: 0 }
  228. ).duration
  229. };
  230. delete newPlaylist.songs;
  231. WSModule.runJob("EMIT_TO_ROOMS", {
  232. rooms: ["admin.playlists"],
  233. args: ["event:admin.playlist.updated", { data: { playlist: newPlaylist } }]
  234. });
  235. }
  236. );
  237. }
  238. });
  239. export default {
  240. /**
  241. * Gets playlists, used in the admin playlists page by the AdvancedTable component
  242. *
  243. * @param {object} session - the session object automatically added by the websocket
  244. * @param page - the page
  245. * @param pageSize - the size per page
  246. * @param properties - the properties to return for each playlist
  247. * @param sort - the sort object
  248. * @param queries - the queries array
  249. * @param operator - the operator for queries
  250. * @param cb
  251. */
  252. getData: useHasPermission(
  253. "playlists.getData",
  254. async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
  255. async.waterfall(
  256. [
  257. next => {
  258. DBModule.runJob(
  259. "GET_DATA",
  260. {
  261. page,
  262. pageSize,
  263. properties,
  264. sort,
  265. queries,
  266. operator,
  267. modelName: "playlist",
  268. blacklistedProperties: [],
  269. specialProperties: {
  270. totalLength: [
  271. {
  272. $addFields: {
  273. totalLength: { $sum: "$songs.duration" }
  274. }
  275. }
  276. ],
  277. songsCount: [
  278. {
  279. $addFields: {
  280. songsCount: { $size: "$songs" }
  281. }
  282. }
  283. ],
  284. createdBy: [
  285. {
  286. $addFields: {
  287. createdByOID: {
  288. $convert: {
  289. input: "$createdBy",
  290. to: "objectId",
  291. onError: "unknown",
  292. onNull: "unknown"
  293. }
  294. }
  295. }
  296. },
  297. {
  298. $lookup: {
  299. from: "users",
  300. localField: "createdByOID",
  301. foreignField: "_id",
  302. as: "createdByUser"
  303. }
  304. },
  305. {
  306. $unwind: {
  307. path: "$createdByUser",
  308. preserveNullAndEmptyArrays: true
  309. }
  310. },
  311. {
  312. $addFields: {
  313. createdByUsername: {
  314. $cond: [
  315. { $eq: ["$createdBy", "Musare"] },
  316. "Musare",
  317. { $ifNull: ["$createdByUser.username", "unknown"] }
  318. ]
  319. }
  320. }
  321. },
  322. {
  323. $project: {
  324. createdByOID: 0,
  325. createdByUser: 0
  326. }
  327. }
  328. ]
  329. },
  330. specialQueries: {
  331. createdBy: newQuery => ({
  332. $or: [newQuery, { createdByUsername: newQuery.createdBy }]
  333. })
  334. }
  335. },
  336. this
  337. )
  338. .then(response => {
  339. next(null, response);
  340. })
  341. .catch(err => {
  342. next(err);
  343. });
  344. }
  345. ],
  346. async (err, response) => {
  347. if (err) {
  348. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  349. this.log("ERROR", "PLAYLISTS_GET_DATA", `Failed to get data from playlists. "${err}"`);
  350. return cb({ status: "error", message: err });
  351. }
  352. this.log("SUCCESS", "PLAYLISTS_GET_DATA", `Got data from playlists successfully.`);
  353. return cb({ status: "success", message: "Successfully got data from playlists.", data: response });
  354. }
  355. );
  356. }
  357. ),
  358. /**
  359. * Searches through all playlists that can be included in a community station
  360. *
  361. * @param {object} session - the session object automatically added by the websocket
  362. * @param {string} query - the query
  363. * @param {string} query - the page
  364. * @param {Function} cb - gets called with the result
  365. */
  366. searchCommunity: isLoginRequired(async function searchCommunity(session, query, page, cb) {
  367. async.waterfall(
  368. [
  369. next => {
  370. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  371. else next();
  372. },
  373. next => {
  374. PlaylistsModule.runJob("SEARCH", {
  375. query,
  376. includeUser: true,
  377. includeGenre: true,
  378. includeOwn: true,
  379. includeSongs: true,
  380. userId: session.userId,
  381. page
  382. })
  383. .then(response => {
  384. next(null, response);
  385. })
  386. .catch(err => {
  387. next(err);
  388. });
  389. }
  390. ],
  391. async (err, data) => {
  392. if (err) {
  393. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  394. this.log("ERROR", "PLAYLISTS_SEARCH_COMMUNITY", `Searching playlists failed. "${err}"`);
  395. return cb({ status: "error", message: err });
  396. }
  397. this.log("SUCCESS", "PLAYLISTS_SEARCH_COMMUNITY", "Searching playlists successful.");
  398. return cb({ status: "success", data });
  399. }
  400. );
  401. }),
  402. /**
  403. * Searches through all playlists that can be included in an official station
  404. *
  405. * @param {object} session - the session object automatically added by the websocket
  406. * @param {string} query - the query
  407. * @param {string} query - the page
  408. * @param {Function} cb - gets called with the result
  409. */
  410. searchOfficial: useHasPermission(
  411. "playlists.searchOfficial",
  412. async function searchOfficial(session, query, page, cb) {
  413. async.waterfall(
  414. [
  415. next => {
  416. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  417. else next();
  418. },
  419. next => {
  420. PlaylistsModule.runJob("SEARCH", {
  421. query,
  422. includeGenre: true,
  423. includePrivate: true,
  424. includeSongs: true,
  425. page
  426. })
  427. .then(response => {
  428. next(null, response);
  429. })
  430. .catch(err => {
  431. next(err);
  432. });
  433. }
  434. ],
  435. async (err, data) => {
  436. if (err) {
  437. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  438. this.log("ERROR", "PLAYLISTS_SEARCH_OFFICIAL", `Searching playlists failed. "${err}"`);
  439. return cb({ status: "error", message: err });
  440. }
  441. this.log("SUCCESS", "PLAYLISTS_SEARCH_OFFICIAL", "Searching playlists successful.");
  442. return cb({ status: "success", data });
  443. }
  444. );
  445. }
  446. ),
  447. /**
  448. * Gets the first song from a private playlist
  449. *
  450. * @param {object} session - the session object automatically added by the websocket
  451. * @param {string} playlistId - the id of the playlist we are getting the first song from
  452. * @param {Function} cb - gets called with the result
  453. */
  454. getFirstSong: isLoginRequired(function getFirstSong(session, playlistId, cb) {
  455. async.waterfall(
  456. [
  457. next => {
  458. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  459. .then(playlist => next(null, playlist))
  460. .catch(next);
  461. },
  462. (playlist, next) => {
  463. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
  464. playlist.songs.sort((a, b) => a.position - b.position);
  465. return next(null, playlist.songs[0]);
  466. }
  467. ],
  468. async (err, song) => {
  469. if (err) {
  470. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  471. this.log(
  472. "ERROR",
  473. "PLAYLIST_GET_FIRST_SONG",
  474. `Getting the first song of playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  475. );
  476. return cb({ status: "error", message: err });
  477. }
  478. this.log(
  479. "SUCCESS",
  480. "PLAYLIST_GET_FIRST_SONG",
  481. `Successfully got the first song of playlist "${playlistId}" for user "${session.userId}".`
  482. );
  483. return cb({
  484. status: "success",
  485. data: { song }
  486. });
  487. }
  488. );
  489. }),
  490. /**
  491. * Gets a list of all the playlists for a specific user
  492. *
  493. * @param {object} session - the session object automatically added by the websocket
  494. * @param {string} userId - the user id in question
  495. * @param {Function} cb - gets called with the result
  496. */
  497. indexForUser: async function indexForUser(session, userId, cb) {
  498. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  499. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  500. async.waterfall(
  501. [
  502. next => {
  503. userModel.findById(userId).select({ "preferences.orderOfPlaylists": -1 }).exec(next);
  504. },
  505. (user, next) => {
  506. if (!user) next("User not found");
  507. else {
  508. const { preferences } = user;
  509. const { orderOfPlaylists } = preferences;
  510. const match = {
  511. createdBy: userId,
  512. type: { $in: ["user", "user-liked", "user-disliked"] }
  513. };
  514. // if a playlist order exists
  515. if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
  516. playlistModel
  517. .aggregate()
  518. .match(match)
  519. .addFields({
  520. weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
  521. })
  522. .sort({ weight: 1 })
  523. .exec(next);
  524. }
  525. },
  526. (playlists, next) => {
  527. if (session.userId === userId) return next(null, playlists); // user requesting playlists is the owner of the playlists
  528. const filteredPlaylists = [];
  529. return async.each(
  530. playlists,
  531. (playlist, nextPlaylist) => {
  532. if (playlist.privacy === "public") filteredPlaylists.push(playlist);
  533. return nextPlaylist();
  534. },
  535. () => next(null, filteredPlaylists)
  536. );
  537. }
  538. ],
  539. async (err, playlists) => {
  540. if (err) {
  541. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  542. this.log(
  543. "ERROR",
  544. "PLAYLIST_INDEX_FOR_USER",
  545. `Indexing playlists for user "${userId}" failed. "${err}"`
  546. );
  547. return cb({ status: "error", message: err });
  548. }
  549. this.log("SUCCESS", "PLAYLIST_INDEX_FOR_USER", `Successfully indexed playlists for user "${userId}".`);
  550. return cb({
  551. status: "success",
  552. data: { playlists }
  553. });
  554. }
  555. );
  556. },
  557. /**
  558. * Gets all playlists for the user requesting it
  559. *
  560. * @param {object} session - the session object automatically added by the websocket
  561. * @param {Function} cb - gets called with the result
  562. */
  563. indexMyPlaylists: isLoginRequired(async function indexMyPlaylists(session, cb) {
  564. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  565. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  566. async.waterfall(
  567. [
  568. next => {
  569. userModel.findById(session.userId).select({ "preferences.orderOfPlaylists": -1 }).exec(next);
  570. },
  571. (user, next) => {
  572. if (!user) next("User not found");
  573. else {
  574. const { preferences } = user;
  575. const { orderOfPlaylists } = preferences;
  576. const match = {
  577. createdBy: session.userId,
  578. type: { $in: ["user", "user-liked", "user-disliked"] }
  579. };
  580. // if a playlist order exists
  581. if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
  582. playlistModel
  583. .aggregate()
  584. .match(match)
  585. .addFields({
  586. weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
  587. })
  588. .sort({ weight: 1 })
  589. .exec(next);
  590. }
  591. }
  592. ],
  593. async (err, playlists) => {
  594. if (err) {
  595. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  596. this.log(
  597. "ERROR",
  598. "PLAYLIST_INDEX_FOR_ME",
  599. `Indexing playlists for user "${session.userId}" failed. "${err}"`
  600. );
  601. return cb({ status: "error", message: err });
  602. }
  603. this.log(
  604. "SUCCESS",
  605. "PLAYLIST_INDEX_FOR_ME",
  606. `Successfully indexed playlists for user "${session.userId}".`
  607. );
  608. return cb({
  609. status: "success",
  610. data: { playlists }
  611. });
  612. }
  613. );
  614. }),
  615. /**
  616. * Gets all playlists playlists
  617. *
  618. * @param {object} session - the session object automatically added by the websocket
  619. * @param {Function} cb - gets called with the result
  620. */
  621. indexFeaturedPlaylists: isLoginRequired(async function indexMyPlaylists(session, cb) {
  622. async.waterfall(
  623. [
  624. next => {
  625. const featuredPlaylistIds = config.get("featuredPlaylists");
  626. if (featuredPlaylistIds.length === 0) next(true, []);
  627. else next(null, featuredPlaylistIds);
  628. },
  629. (featuredPlaylistIds, next) => {
  630. const featuredPlaylists = [];
  631. async.eachLimit(
  632. featuredPlaylistIds,
  633. 1,
  634. (playlistId, next) => {
  635. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  636. .then(playlist => {
  637. if (playlist.privacy === "public") featuredPlaylists.push(playlist);
  638. next();
  639. })
  640. .catch(next);
  641. },
  642. err => {
  643. next(err, featuredPlaylists);
  644. }
  645. );
  646. }
  647. ],
  648. async (err, playlists) => {
  649. if (err && err !== true) {
  650. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  651. this.log("ERROR", "PLAYLIST_INDEX_FEATURED", `Indexing featured playlists failed. "${err}"`);
  652. return cb({ status: "error", message: err });
  653. }
  654. this.log("SUCCESS", "PLAYLIST_INDEX_FEATURED", `Successfully indexed featured playlists.`);
  655. return cb({
  656. status: "success",
  657. data: { playlists }
  658. });
  659. }
  660. );
  661. }),
  662. /**
  663. * Creates a new private playlist
  664. *
  665. * @param {object} session - the session object automatically added by the websocket
  666. * @param {object} data - the data for the new private playlist
  667. * @param {Function} cb - gets called with the result
  668. */
  669. create: isLoginRequired(async function create(session, data, cb) {
  670. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  671. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  672. const blacklist = ["liked songs", "likedsongs", "disliked songs", "dislikedsongs"];
  673. async.waterfall(
  674. [
  675. next => (data ? next() : cb({ status: "error", message: "Invalid data" })),
  676. next => {
  677. const { displayName, songs, privacy } = data;
  678. if (blacklist.indexOf(displayName.toLowerCase()) !== -1)
  679. return next("That playlist name is blacklisted. Please use a different name.");
  680. return playlistModel.create(
  681. {
  682. displayName,
  683. songs,
  684. privacy,
  685. createdBy: session.userId,
  686. createdAt: Date.now(),
  687. createdFor: null,
  688. type: "user"
  689. },
  690. next
  691. );
  692. },
  693. (playlist, next) => {
  694. userModel.updateOne(
  695. { _id: session.userId },
  696. { $push: { "preferences.orderOfPlaylists": playlist._id } },
  697. err => {
  698. if (err) return next(err);
  699. return next(null, playlist);
  700. }
  701. );
  702. }
  703. ],
  704. async (err, playlist) => {
  705. if (err) {
  706. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  707. this.log(
  708. "ERROR",
  709. "PLAYLIST_CREATE",
  710. `Creating private playlist failed for user "${session.userId}". "${err}"`
  711. );
  712. return cb({ status: "error", message: err });
  713. }
  714. CacheModule.runJob("PUB", {
  715. channel: "playlist.create",
  716. value: playlist
  717. });
  718. ActivitiesModule.runJob("ADD_ACTIVITY", {
  719. userId: playlist.createdBy,
  720. type: "playlist__create",
  721. payload: {
  722. message: `Created playlist <playlistId>${playlist.displayName}</playlistId>`,
  723. playlistId: playlist._id
  724. }
  725. });
  726. this.log(
  727. "SUCCESS",
  728. "PLAYLIST_CREATE",
  729. `Successfully created private playlist for user "${session.userId}".`
  730. );
  731. return cb({
  732. status: "success",
  733. message: "Successfully created playlist",
  734. data: {
  735. playlistId: playlist._id
  736. }
  737. });
  738. }
  739. );
  740. }),
  741. /**
  742. * Gets a playlist from id
  743. *
  744. * @param {object} session - the session object automatically added by the websocket
  745. * @param {string} playlistId - the id of the playlist we are getting
  746. * @param {Function} cb - gets called with the result
  747. */
  748. getPlaylist: function getPlaylist(session, playlistId, cb) {
  749. async.waterfall(
  750. [
  751. next => {
  752. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  753. .then(playlist => next(null, playlist))
  754. .catch(next);
  755. },
  756. (playlist, next) => {
  757. if (!playlist) return next("Playlist not found");
  758. if (playlist.privacy !== "public" && playlist.createdBy !== session.userId) {
  759. if (session)
  760. // check if user requested to get a playlist is an admin
  761. return DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  762. userModel.findOne({ _id: session.userId }, (err, user) => {
  763. if (user && user.role === "admin") return next(null, playlist);
  764. return next("User unauthorised to view playlist.");
  765. });
  766. });
  767. return next("User unauthorised to view playlist.");
  768. }
  769. return next(null, playlist);
  770. }
  771. ],
  772. async (err, playlist) => {
  773. if (err) {
  774. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  775. this.log(
  776. "ERROR",
  777. "PLAYLIST_GET",
  778. `Getting private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  779. );
  780. return cb({ status: "error", message: err });
  781. }
  782. this.log(
  783. "SUCCESS",
  784. "PLAYLIST_GET",
  785. `Successfully got private playlist "${playlistId}" for user "${session.userId}".`
  786. );
  787. return cb({
  788. status: "success",
  789. data: { playlist }
  790. });
  791. }
  792. );
  793. },
  794. /**
  795. * Gets a playlist from station id
  796. *
  797. * @param {object} session - the session object automatically added by the websocket
  798. * @param {string} stationId - the id of the station we are getting
  799. * @param {string} includeSongs - include songs
  800. * @param {Function} cb - gets called with the result
  801. */
  802. getPlaylistForStation: function getPlaylist(session, stationId, includeSongs, cb) {
  803. async.waterfall(
  804. [
  805. next => {
  806. PlaylistsModule.runJob("GET_STATION_PLAYLIST", { stationId, includeSongs }, this)
  807. .then(response => next(null, response.playlist))
  808. .catch(next);
  809. },
  810. (playlist, next) => {
  811. if (!playlist) return next("Playlist not found");
  812. if (playlist.privacy !== "public" && playlist.createdBy !== session.userId) {
  813. if (session)
  814. // check if user requested to get a playlist is an admin
  815. return DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  816. userModel.findOne({ _id: session.userId }, (err, user) => {
  817. if (user && user.role === "admin") return next(null, playlist);
  818. return next("User unauthorised to view playlist.");
  819. });
  820. });
  821. return next("User unauthorised to view playlist.");
  822. }
  823. return next(null, playlist);
  824. }
  825. ],
  826. async (err, playlist) => {
  827. if (err) {
  828. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  829. this.log(
  830. "ERROR",
  831. "PLAYLIST_GET",
  832. `Getting playlist for station "${stationId}" failed for user "${session.userId}". "${err}"`
  833. );
  834. return cb({ status: "error", message: err });
  835. }
  836. this.log(
  837. "SUCCESS",
  838. "PLAYLIST_GET",
  839. `Successfully got playlist for station "${stationId}" for user "${session.userId}".`
  840. );
  841. return cb({
  842. status: "success",
  843. data: { playlist }
  844. });
  845. }
  846. );
  847. },
  848. /**
  849. * Shuffles songs in a private playlist
  850. *
  851. * @param {object} session - the session object automatically added by the websocket
  852. * @param {string} playlistId - the id of the playlist we are updating
  853. * @param {Function} cb - gets called with the result
  854. */
  855. shuffle: isLoginRequired(async function shuffle(session, playlistId, cb) {
  856. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  857. async.waterfall(
  858. [
  859. next => {
  860. if (!playlistId) return next("No playlist id.");
  861. return next();
  862. },
  863. next => {
  864. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  865. .then(playlist => {
  866. if (!playlist || playlist.createdBy !== session.userId)
  867. return next("Something went wrong when trying to get the playlist");
  868. return next(null, playlist);
  869. })
  870. .catch(next);
  871. },
  872. (playlist, next) => {
  873. if (!playlist.isUserModifiable) return next("Playlist cannot be shuffled.");
  874. return UtilsModule.runJob("SHUFFLE_SONG_POSITIONS", { array: playlist.songs }, this)
  875. .then(result => next(null, result.array))
  876. .catch(next);
  877. },
  878. (songs, next) => {
  879. playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, { runValidators: true }, next);
  880. },
  881. (res, next) => {
  882. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  883. .then(playlist => next(null, playlist))
  884. .catch(next);
  885. }
  886. ],
  887. async (err, playlist) => {
  888. if (err) {
  889. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  890. this.log(
  891. "ERROR",
  892. "PLAYLIST_SHUFFLE",
  893. `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  894. );
  895. return cb({ status: "error", message: err });
  896. }
  897. this.log(
  898. "SUCCESS",
  899. "PLAYLIST_SHUFFLE",
  900. `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
  901. );
  902. return cb({
  903. status: "success",
  904. message: "Successfully shuffled playlist.",
  905. data: { playlist }
  906. });
  907. }
  908. );
  909. }),
  910. /**
  911. * Changes the order (position) of a song in a playlist
  912. *
  913. * @param {object} session - the session object automatically added by the websocket
  914. * @param {string} playlistId - the id of the playlist we are targeting
  915. * @param {object} song - the song to be repositioned
  916. * @param {string} song.youtubeId - the youtube id of the song being repositioned
  917. * @param {string} song.newIndex - the new position of the song in the playlist
  918. * @param {...any} song.args - any other elements that would be included with a song item in a playlist
  919. * @param {Function} cb - gets called with the result
  920. */
  921. repositionSong: isLoginRequired(async function repositionSong(session, playlistId, song, cb) {
  922. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  923. async.waterfall(
  924. [
  925. next => {
  926. if (!playlistId) return next("Please provide a playlist.");
  927. if (!song || !song.youtubeId) return next("You must provide a song to reposition.");
  928. return next();
  929. },
  930. next => {
  931. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  932. .then(playlist => {
  933. if (!playlist || playlist.createdBy !== session.userId) {
  934. return DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  935. userModel.findOne({ _id: session.userId }, (err, user) => {
  936. if (user && user.role === "admin") return next();
  937. return next("Something went wrong when trying to get the playlist");
  938. });
  939. });
  940. }
  941. return next();
  942. })
  943. .catch(next);
  944. },
  945. // remove song from playlist
  946. next => {
  947. playlistModel.updateOne(
  948. { _id: playlistId },
  949. { $pull: { songs: { youtubeId: song.youtubeId } } },
  950. next
  951. );
  952. },
  953. // add song back to playlist (in new position)
  954. (res, next) => {
  955. playlistModel.updateOne(
  956. { _id: playlistId },
  957. { $push: { songs: { $each: [song], $position: song.newIndex } } },
  958. err => next(err)
  959. );
  960. },
  961. // update the cache with the new songs positioning
  962. next => {
  963. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  964. .then(playlist => next(null, playlist))
  965. .catch(next);
  966. }
  967. ],
  968. async err => {
  969. if (err) {
  970. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  971. this.log(
  972. "ERROR",
  973. "PLAYLIST_REPOSITION_SONG",
  974. `Repositioning song ${song.youtubeId} for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  975. );
  976. return cb({ status: "error", message: err });
  977. }
  978. this.log(
  979. "SUCCESS",
  980. "PLAYLIST_REPOSITION_SONG",
  981. `Successfully repositioned song ${song.youtubeId} for private playlist "${playlistId}" for user "${session.userId}".`
  982. );
  983. CacheModule.runJob("PUB", {
  984. channel: "playlist.repositionSong",
  985. value: {
  986. userId: session.userId,
  987. playlistId,
  988. song
  989. }
  990. });
  991. return cb({
  992. status: "success",
  993. message: "Successfully repositioned song"
  994. });
  995. }
  996. );
  997. }),
  998. /**
  999. * Adds a song to a private playlist
  1000. *
  1001. * @param {object} session - the session object automatically added by the websocket
  1002. * @param {boolean} isSet - is the song part of a set of songs to be added
  1003. * @param {string} youtubeId - the youtube id of the song we are trying to add
  1004. * @param {string} playlistId - the id of the playlist we are adding the song to
  1005. * @param {Function} cb - gets called with the result
  1006. */
  1007. addSongToPlaylist: isLoginRequired(async function addSongToPlaylist(session, isSet, youtubeId, playlistId, cb) {
  1008. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1009. async.waterfall(
  1010. [
  1011. next => {
  1012. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1013. .then(playlist => {
  1014. if (!playlist || playlist.createdBy !== session.userId) {
  1015. DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  1016. userModel.findOne({ _id: session.userId }, (err, user) => {
  1017. if (user && user.role === "admin") return next(null, playlist);
  1018. return next("Something went wrong when trying to get the playlist");
  1019. });
  1020. });
  1021. } else next(null, playlist);
  1022. })
  1023. .catch(next);
  1024. },
  1025. (playlist, next) => {
  1026. if (playlist.type === "user-liked" || playlist.type === "user-disliked") {
  1027. const oppositeType = playlist.type === "user-liked" ? "user-disliked" : "user-liked";
  1028. const oppositePlaylistName = oppositeType === "user-liked" ? "Liked Songs" : "Disliked Songs";
  1029. playlistModel.count(
  1030. { type: oppositeType, createdBy: session.userId, "songs.youtubeId": youtubeId },
  1031. (err, results) => {
  1032. if (err) next(err);
  1033. else if (results > 0)
  1034. next(
  1035. `That song is already in your ${oppositePlaylistName} playlist. A song cannot be in both the Liked Songs playlist and the Disliked Songs playlist at the same time.`
  1036. );
  1037. else next();
  1038. }
  1039. );
  1040. } else next();
  1041. },
  1042. next => {
  1043. PlaylistsModule.runJob("ADD_SONG_TO_PLAYLIST", { playlistId, youtubeId }, this)
  1044. .then(res => {
  1045. const { playlist, song, ratings } = res;
  1046. next(null, playlist, song, ratings);
  1047. })
  1048. .catch(next);
  1049. }
  1050. ],
  1051. async (err, playlist, newSong, ratings) => {
  1052. if (err) {
  1053. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1054. this.log(
  1055. "ERROR",
  1056. "PLAYLIST_ADD_SONG",
  1057. `Adding song "${youtubeId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1058. );
  1059. return cb({ status: "error", message: err });
  1060. }
  1061. this.log(
  1062. "SUCCESS",
  1063. "PLAYLIST_ADD_SONG",
  1064. `Successfully added song "${youtubeId}" to private playlist "${playlistId}" for user "${session.userId}".`
  1065. );
  1066. if (!isSet && playlist.type === "user" && playlist.privacy === "public") {
  1067. const songName = newSong.artists
  1068. ? `${newSong.title} by ${newSong.artists.join(", ")}`
  1069. : newSong.title;
  1070. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1071. userId: session.userId,
  1072. type: "playlist__add_song",
  1073. payload: {
  1074. message: `Added <youtubeId>${songName}</youtubeId> to playlist <playlistId>${playlist.displayName}</playlistId>`,
  1075. thumbnail: newSong.thumbnail,
  1076. playlistId,
  1077. youtubeId
  1078. }
  1079. });
  1080. }
  1081. CacheModule.runJob("PUB", {
  1082. channel: "playlist.addSong",
  1083. value: {
  1084. playlistId: playlist._id,
  1085. song: newSong,
  1086. userId: session.userId,
  1087. privacy: playlist.privacy
  1088. }
  1089. });
  1090. CacheModule.runJob("PUB", {
  1091. channel: "playlist.updated",
  1092. value: { playlistId }
  1093. });
  1094. if (ratings && (playlist.type === "user-liked" || playlist.type === "user-disliked")) {
  1095. const { _id, youtubeId, title, artists, thumbnail } = newSong;
  1096. const { likes, dislikes } = ratings;
  1097. SongsModule.runJob("UPDATE_SONG", { songId: _id });
  1098. if (playlist.type === "user-liked") {
  1099. CacheModule.runJob("PUB", {
  1100. channel: "song.like",
  1101. value: JSON.stringify({
  1102. youtubeId,
  1103. userId: session.userId,
  1104. likes,
  1105. dislikes
  1106. })
  1107. });
  1108. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1109. userId: session.userId,
  1110. type: "song__like",
  1111. payload: {
  1112. message: `Liked song <youtubeId>${title} by ${artists.join(", ")}</youtubeId>`,
  1113. youtubeId,
  1114. thumbnail
  1115. }
  1116. });
  1117. } else {
  1118. CacheModule.runJob("PUB", {
  1119. channel: "song.dislike",
  1120. value: JSON.stringify({
  1121. youtubeId,
  1122. userId: session.userId,
  1123. likes,
  1124. dislikes
  1125. })
  1126. });
  1127. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1128. userId: session.userId,
  1129. type: "song__dislike",
  1130. payload: {
  1131. message: `Disliked song <youtubeId>${title} by ${artists.join(", ")}</youtubeId>`,
  1132. youtubeId,
  1133. thumbnail
  1134. }
  1135. });
  1136. }
  1137. }
  1138. return cb({
  1139. status: "success",
  1140. message: "Song has been successfully added to the playlist",
  1141. data: { songs: playlist.songs }
  1142. });
  1143. }
  1144. );
  1145. }),
  1146. /**
  1147. * Adds a set of songs to a private playlist
  1148. *
  1149. * @param {object} session - the session object automatically added by the websocket
  1150. * @param {string} url - the url of the the YouTube playlist
  1151. * @param {string} playlistId - the id of the playlist we are adding the set of songs to
  1152. * @param {boolean} musicOnly - whether to only add music to the playlist
  1153. * @param {Function} cb - gets called with the result
  1154. */
  1155. addSetToPlaylist: isLoginRequired(async function addSetToPlaylist(session, url, playlistId, musicOnly, cb) {
  1156. let videosInPlaylistTotal = 0;
  1157. let songsInPlaylistTotal = 0;
  1158. let addSongsStats = null;
  1159. const addedSongs = [];
  1160. this.keepLongJob();
  1161. this.publishProgress({
  1162. status: "started",
  1163. title: "Import YouTube playlist",
  1164. message: "Importing YouTube playlist.",
  1165. id: this.toString()
  1166. });
  1167. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  1168. await CacheModule.runJob(
  1169. "PUB",
  1170. {
  1171. channel: "longJob.added",
  1172. value: { jobId: this.toString(), userId: session.userId }
  1173. },
  1174. this
  1175. );
  1176. async.waterfall(
  1177. [
  1178. next => {
  1179. this.publishProgress({ status: "update", message: `Importing YouTube playlist (stage 1)` });
  1180. YouTubeModule.runJob("GET_PLAYLIST", { url, musicOnly }, this)
  1181. .then(res => {
  1182. if (res.filteredSongs) {
  1183. videosInPlaylistTotal = res.songs.length;
  1184. songsInPlaylistTotal = res.filteredSongs.length;
  1185. } else {
  1186. songsInPlaylistTotal = videosInPlaylistTotal = res.songs.length;
  1187. }
  1188. next(null, res.songs);
  1189. })
  1190. .catch(err => {
  1191. next(err);
  1192. });
  1193. },
  1194. (youtubeIds, next) => {
  1195. this.publishProgress({ status: "update", message: `Importing YouTube playlist (stage 2)` });
  1196. let successful = 0;
  1197. let failed = 0;
  1198. let alreadyInPlaylist = 0;
  1199. let alreadyInLikedPlaylist = 0;
  1200. let alreadyInDislikedPlaylist = 0;
  1201. if (youtubeIds.length === 0) next();
  1202. async.eachLimit(
  1203. youtubeIds,
  1204. 1,
  1205. (youtubeId, next) => {
  1206. WSModule.runJob(
  1207. "RUN_ACTION2",
  1208. {
  1209. session,
  1210. namespace: "playlists",
  1211. action: "addSongToPlaylist",
  1212. args: [true, youtubeId, playlistId]
  1213. },
  1214. this
  1215. )
  1216. .then(res => {
  1217. if (res.status === "success") {
  1218. successful += 1;
  1219. addedSongs.push(youtubeId);
  1220. } else failed += 1;
  1221. if (res.message === "That song is already in the playlist") alreadyInPlaylist += 1;
  1222. else if (
  1223. res.message ===
  1224. "That song is already in your Liked Songs playlist. " +
  1225. "A song cannot be in both the Liked Songs playlist" +
  1226. " and the Disliked Songs playlist at the same time."
  1227. )
  1228. alreadyInLikedPlaylist += 1;
  1229. else if (
  1230. res.message ===
  1231. "That song is already in your Disliked Songs playlist. " +
  1232. "A song cannot be in both the Liked Songs playlist " +
  1233. "and the Disliked Songs playlist at the same time."
  1234. )
  1235. alreadyInDislikedPlaylist += 1;
  1236. })
  1237. .catch(() => {
  1238. failed += 1;
  1239. })
  1240. .finally(() => next());
  1241. },
  1242. () => {
  1243. addSongsStats = {
  1244. successful,
  1245. failed,
  1246. alreadyInPlaylist,
  1247. alreadyInLikedPlaylist,
  1248. alreadyInDislikedPlaylist
  1249. };
  1250. next(null);
  1251. }
  1252. );
  1253. },
  1254. next => {
  1255. this.publishProgress({ status: "update", message: `Importing YouTube playlist (stage 3)` });
  1256. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1257. .then(playlist => next(null, playlist))
  1258. .catch(next);
  1259. },
  1260. (playlist, next) => {
  1261. this.publishProgress({ status: "update", message: `Importing YouTube playlist (stage 4)` });
  1262. if (!playlist || playlist.createdBy !== session.userId) {
  1263. return DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  1264. userModel.findOne({ _id: session.userId }, (err, user) => {
  1265. if (user && user.role === "admin") return next(null, playlist);
  1266. return next("Something went wrong when trying to get the playlist");
  1267. });
  1268. });
  1269. }
  1270. return next(null, playlist);
  1271. }
  1272. ],
  1273. async (err, playlist) => {
  1274. if (err) {
  1275. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1276. this.log(
  1277. "ERROR",
  1278. "PLAYLIST_IMPORT",
  1279. `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1280. );
  1281. this.publishProgress({
  1282. status: "error",
  1283. message: err
  1284. });
  1285. return cb({ status: "error", message: err });
  1286. }
  1287. if (playlist.privacy === "public")
  1288. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1289. userId: session.userId,
  1290. type: "playlist__import_playlist",
  1291. payload: {
  1292. message: `Imported ${addSongsStats.successful} songs to playlist <playlistId>${playlist.displayName}</playlistId>`,
  1293. playlistId
  1294. }
  1295. });
  1296. this.log(
  1297. "SUCCESS",
  1298. "PLAYLIST_IMPORT",
  1299. `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}". Videos in playlist: ${videosInPlaylistTotal}, songs in playlist: ${songsInPlaylistTotal}, songs successfully added: ${addSongsStats.successful}, songs failed: ${addSongsStats.failed}, already in playlist: ${addSongsStats.alreadyInPlaylist}, already in liked ${addSongsStats.alreadyInLikedPlaylist}, already in disliked ${addSongsStats.alreadyInDislikedPlaylist}.`
  1300. );
  1301. this.publishProgress({
  1302. status: "success",
  1303. message: `Playlist has been imported. ${addSongsStats.successful} were added successfully, ${addSongsStats.failed} failed (${addSongsStats.alreadyInPlaylist} were already in the playlist)`
  1304. });
  1305. return cb({
  1306. status: "success",
  1307. message: `Playlist has been imported. ${addSongsStats.successful} were added successfully, ${addSongsStats.failed} failed (${addSongsStats.alreadyInPlaylist} were already in the playlist)`,
  1308. data: {
  1309. songs: playlist.songs,
  1310. stats: {
  1311. videosInPlaylistTotal,
  1312. songsInPlaylistTotal,
  1313. alreadyInLikedPlaylist: addSongsStats.alreadyInLikedPlaylist,
  1314. alreadyInDislikedPlaylist: addSongsStats.alreadyInDislikedPlaylist
  1315. }
  1316. }
  1317. });
  1318. }
  1319. );
  1320. }),
  1321. /**
  1322. * Removes a song from a private playlist
  1323. *
  1324. * @param {object} session - the session object automatically added by the websocket
  1325. * @param {string} youtubeId - the youtube id of the song we are removing from the private playlist
  1326. * @param {string} playlistId - the id of the playlist we are removing the song from
  1327. * @param {Function} cb - gets called with the result
  1328. */
  1329. removeSongFromPlaylist: isLoginRequired(async function removeSongFromPlaylist(session, youtubeId, playlistId, cb) {
  1330. async.waterfall(
  1331. [
  1332. next => {
  1333. if (!youtubeId || typeof youtubeId !== "string") return next("Invalid song id.");
  1334. if (!playlistId || typeof youtubeId !== "string") return next("Invalid playlist id.");
  1335. return next();
  1336. },
  1337. next => {
  1338. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1339. .then(playlist => {
  1340. if (!playlist || playlist.createdBy !== session.userId) {
  1341. return DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  1342. userModel.findOne({ _id: session.userId }, (err, user) => {
  1343. if (user && user.role === "admin") return next(null, playlist);
  1344. return next("Something went wrong when trying to get the playlist");
  1345. });
  1346. });
  1347. }
  1348. return next(null, playlist);
  1349. })
  1350. .catch(next);
  1351. },
  1352. (playlist, next) => {
  1353. MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
  1354. .then(res =>
  1355. next(null, playlist, {
  1356. _id: res.song._id,
  1357. title: res.song.title,
  1358. thumbnail: res.song.thumbnail,
  1359. artists: res.song.artists,
  1360. youtubeId: res.song.youtubeId
  1361. })
  1362. )
  1363. .catch(next);
  1364. },
  1365. (playlist, newSong, next) => {
  1366. PlaylistsModule.runJob("REMOVE_FROM_PLAYLIST", { playlistId, youtubeId }, this)
  1367. .then(res => {
  1368. const { ratings } = res;
  1369. next(null, playlist, newSong, ratings);
  1370. })
  1371. .catch(next);
  1372. },
  1373. (playlist, newSong, ratings, next) => {
  1374. const { _id, title, artists, thumbnail } = newSong;
  1375. const songName = artists ? `${title} by ${artists.join(", ")}` : title;
  1376. if (playlist.type === "user" && playlist.privacy === "public") {
  1377. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1378. userId: session.userId,
  1379. type: "playlist__remove_song",
  1380. payload: {
  1381. message: `Removed <youtubeId>${songName}</youtubeId> from playlist <playlistId>${playlist.displayName}</playlistId>`,
  1382. thumbnail,
  1383. playlistId,
  1384. youtubeId: newSong.youtubeId
  1385. }
  1386. });
  1387. }
  1388. if (ratings && (playlist.type === "user-liked" || playlist.type === "user-disliked")) {
  1389. const { likes, dislikes } = ratings;
  1390. SongsModule.runJob("UPDATE_SONG", { songId: _id });
  1391. if (playlist.type === "user-liked") {
  1392. CacheModule.runJob("PUB", {
  1393. channel: "ratings.unlike",
  1394. value: JSON.stringify({
  1395. youtubeId: newSong.youtubeId,
  1396. userId: session.userId,
  1397. likes,
  1398. dislikes
  1399. })
  1400. });
  1401. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1402. userId: session.userId,
  1403. type: "song__unlike",
  1404. payload: {
  1405. message: `Removed <youtubeId>${title} by ${artists.join(
  1406. ", "
  1407. )}</youtubeId> from your Liked Songs`,
  1408. youtubeId: newSong.youtubeId,
  1409. thumbnail
  1410. }
  1411. });
  1412. } else {
  1413. CacheModule.runJob("PUB", {
  1414. channel: "song.undislike",
  1415. value: JSON.stringify({
  1416. youtubeId: newSong.youtubeId,
  1417. userId: session.userId,
  1418. likes,
  1419. dislikes
  1420. })
  1421. });
  1422. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1423. userId: session.userId,
  1424. type: "song__undislike",
  1425. payload: {
  1426. message: `Removed <youtubeId>${title} by ${artists.join(
  1427. ", "
  1428. )}</youtubeId> from your Disliked Songs`,
  1429. youtubeId: newSong.youtubeId,
  1430. thumbnail
  1431. }
  1432. });
  1433. }
  1434. }
  1435. return next(null, playlist);
  1436. }
  1437. ],
  1438. async (err, playlist) => {
  1439. if (err) {
  1440. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1441. this.log(
  1442. "ERROR",
  1443. "PLAYLIST_REMOVE_SONG",
  1444. `Removing song "${youtubeId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1445. );
  1446. return cb({ status: "error", message: err });
  1447. }
  1448. this.log(
  1449. "SUCCESS",
  1450. "PLAYLIST_REMOVE_SONG",
  1451. `Successfully removed song "${youtubeId}" from private playlist "${playlistId}" for user "${session.userId}".`
  1452. );
  1453. CacheModule.runJob("PUB", {
  1454. channel: "playlist.removeSong",
  1455. value: {
  1456. playlistId: playlist._id,
  1457. youtubeId,
  1458. userId: session.userId,
  1459. privacy: playlist.privacy
  1460. }
  1461. });
  1462. return cb({
  1463. status: "success",
  1464. message: "Song has been successfully removed from playlist",
  1465. data: { songs: playlist.songs }
  1466. });
  1467. }
  1468. );
  1469. }),
  1470. /**
  1471. * Updates the displayName of a private playlist
  1472. *
  1473. * @param {object} session - the session object automatically added by the websocket
  1474. * @param {string} playlistId - the id of the playlist we are updating the displayName for
  1475. * @param {Function} cb - gets called with the result
  1476. */
  1477. updateDisplayName: isLoginRequired(async function updateDisplayName(session, playlistId, displayName, cb) {
  1478. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1479. async.waterfall(
  1480. [
  1481. next => {
  1482. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1483. .then(playlist => next(null, playlist))
  1484. .catch(next);
  1485. },
  1486. (playlist, next) => {
  1487. if (playlist.type !== "user") return next("Playlist cannot be modified.");
  1488. return next(null);
  1489. },
  1490. next => {
  1491. playlistModel.updateOne(
  1492. { _id: playlistId, createdBy: session.userId },
  1493. { $set: { displayName } },
  1494. { runValidators: true },
  1495. next
  1496. );
  1497. },
  1498. (res, next) => {
  1499. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  1500. .then(playlist => next(null, playlist))
  1501. .catch(next);
  1502. }
  1503. ],
  1504. async (err, playlist) => {
  1505. if (err) {
  1506. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1507. this.log(
  1508. "ERROR",
  1509. "PLAYLIST_UPDATE_DISPLAY_NAME",
  1510. `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1511. );
  1512. return cb({ status: "error", message: err });
  1513. }
  1514. this.log(
  1515. "SUCCESS",
  1516. "PLAYLIST_UPDATE_DISPLAY_NAME",
  1517. `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
  1518. );
  1519. CacheModule.runJob("PUB", {
  1520. channel: "playlist.updateDisplayName",
  1521. value: {
  1522. playlistId,
  1523. displayName,
  1524. userId: session.userId,
  1525. privacy: playlist.privacy
  1526. }
  1527. });
  1528. CacheModule.runJob("PUB", {
  1529. channel: "playlist.updated",
  1530. value: { playlistId }
  1531. });
  1532. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1533. userId: session.userId,
  1534. type: "playlist__edit_display_name",
  1535. payload: {
  1536. message: `Changed display name of playlist <playlistId>${displayName}</playlistId>`,
  1537. playlistId
  1538. }
  1539. });
  1540. return cb({
  1541. status: "success",
  1542. message: "Playlist has been successfully updated"
  1543. });
  1544. }
  1545. );
  1546. }),
  1547. /**
  1548. * Removes a user's own modifiable user playlist
  1549. *
  1550. * @param {object} session - the session object automatically added by the websocket
  1551. * @param {string} playlistId - the id of the playlist we are removing
  1552. * @param {Function} cb - gets called with the result
  1553. */
  1554. remove: isLoginRequired(async function remove(session, playlistId, cb) {
  1555. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1556. async.waterfall(
  1557. [
  1558. next => {
  1559. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1560. .then(playlist => next(null, playlist))
  1561. .catch(next);
  1562. },
  1563. (playlist, next) => {
  1564. if (playlist.createdBy !== session.userId) return next("You do not own this playlist.");
  1565. if (playlist.type !== "user") return next("Playlist cannot be removed.");
  1566. return next(null, playlist);
  1567. },
  1568. (playlist, next) => {
  1569. userModel.updateOne(
  1570. { _id: playlist.createdBy },
  1571. { $pull: { "preferences.orderOfPlaylists": playlist._id } },
  1572. err => next(err, playlist)
  1573. );
  1574. },
  1575. (playlist, next) => {
  1576. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId }, this)
  1577. .then(() => next(null, playlist))
  1578. .catch(next);
  1579. }
  1580. ],
  1581. async (err, playlist) => {
  1582. if (err) {
  1583. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1584. this.log(
  1585. "ERROR",
  1586. "PLAYLIST_REMOVE",
  1587. `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1588. );
  1589. return cb({ status: "error", message: err });
  1590. }
  1591. this.log(
  1592. "SUCCESS",
  1593. "PLAYLIST_REMOVE",
  1594. `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
  1595. );
  1596. CacheModule.runJob("PUB", {
  1597. channel: "playlist.delete",
  1598. value: {
  1599. userId: session.userId,
  1600. playlistId
  1601. }
  1602. });
  1603. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1604. userId: playlist.createdBy,
  1605. type: "playlist__remove",
  1606. payload: {
  1607. message: `Removed playlist ${playlist.displayName}`
  1608. }
  1609. });
  1610. ActivitiesModule.runJob("REMOVE_ACTIVITY_REFERENCES", { type: "playlistId", playlistId });
  1611. return cb({
  1612. status: "success",
  1613. message: "Playlist successfully removed"
  1614. });
  1615. }
  1616. );
  1617. }),
  1618. /**
  1619. * Removes a user's modifiable user playlist as an admin
  1620. *
  1621. * @param {object} session - the session object automatically added by the websocket
  1622. * @param {string} playlistId - the id of the playlist we are removing
  1623. * @param {Function} cb - gets called with the result
  1624. */
  1625. removeAdmin: useHasPermission("playlists.removeAdmin", async function removeAdmin(session, playlistId, cb) {
  1626. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1627. async.waterfall(
  1628. [
  1629. next => {
  1630. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1631. .then(playlist => next(null, playlist))
  1632. .catch(next);
  1633. },
  1634. (playlist, next) => {
  1635. if (playlist.type !== "user") return next("Playlist cannot be removed.");
  1636. return next(null, playlist);
  1637. },
  1638. (playlist, next) => {
  1639. userModel.updateOne(
  1640. { _id: playlist.createdBy },
  1641. { $pull: { "preferences.orderOfPlaylists": playlist._id } },
  1642. err => next(err, playlist, playlist.createdBy)
  1643. );
  1644. },
  1645. (playlist, playlistCreator, next) => {
  1646. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId }, this)
  1647. .then(() => next(null, playlistCreator))
  1648. .catch(next);
  1649. }
  1650. ],
  1651. async (err, playlistCreator) => {
  1652. if (err) {
  1653. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1654. this.log(
  1655. "ERROR",
  1656. "PLAYLIST_REMOVE_ADMIN",
  1657. `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1658. );
  1659. return cb({ status: "error", message: err });
  1660. }
  1661. this.log(
  1662. "SUCCESS",
  1663. "PLAYLIST_REMOVE_ADMIN",
  1664. `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
  1665. );
  1666. CacheModule.runJob("PUB", {
  1667. channel: "playlist.delete",
  1668. value: {
  1669. userId: playlistCreator,
  1670. playlistId
  1671. }
  1672. });
  1673. ActivitiesModule.runJob("REMOVE_ACTIVITY_REFERENCES", { type: "playlistId", playlistId });
  1674. return cb({
  1675. status: "success",
  1676. message: "Playlist successfully removed"
  1677. });
  1678. }
  1679. );
  1680. }),
  1681. /**
  1682. * Updates the privacy of a private playlist
  1683. *
  1684. * @param {object} session - the session object automatically added by the websocket
  1685. * @param {string} playlistId - the id of the playlist we are updating the privacy for
  1686. * @param {string} privacy - what the new privacy of the playlist should be e.g. public
  1687. * @param {Function} cb - gets called with the result
  1688. */
  1689. updatePrivacy: isLoginRequired(async function updatePrivacy(session, playlistId, privacy, cb) {
  1690. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1691. async.waterfall(
  1692. [
  1693. next => {
  1694. playlistModel.updateOne(
  1695. { _id: playlistId, createdBy: session.userId },
  1696. { $set: { privacy } },
  1697. { runValidators: true },
  1698. next
  1699. );
  1700. },
  1701. (res, next) => {
  1702. if (res.n === 0) next("No user playlist found with that id and owned by you.");
  1703. else if (res.nModified === 0) next(`Nothing changed, the playlist was already ${privacy}.`);
  1704. else {
  1705. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  1706. .then(playlist => next(null, playlist))
  1707. .catch(next);
  1708. }
  1709. }
  1710. ],
  1711. async (err, playlist) => {
  1712. if (err) {
  1713. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1714. this.log(
  1715. "ERROR",
  1716. "PLAYLIST_UPDATE_PRIVACY",
  1717. `Updating privacy to "${privacy}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1718. );
  1719. return cb({ status: "error", message: err });
  1720. }
  1721. this.log(
  1722. "SUCCESS",
  1723. "PLAYLIST_UPDATE_PRIVACY",
  1724. `Successfully updated privacy to "${privacy}" for private playlist "${playlistId}" for user "${session.userId}".`
  1725. );
  1726. CacheModule.runJob("PUB", {
  1727. channel: "playlist.updatePrivacy",
  1728. value: {
  1729. userId: session.userId,
  1730. playlist
  1731. }
  1732. });
  1733. CacheModule.runJob("PUB", {
  1734. channel: "playlist.updated",
  1735. value: { playlistId }
  1736. });
  1737. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1738. userId: session.userId,
  1739. type: "playlist__edit_privacy",
  1740. payload: {
  1741. message: `Changed privacy of playlist <playlistId>${playlist.displayName}</playlistId> to ${privacy}`,
  1742. playlistId
  1743. }
  1744. });
  1745. return cb({
  1746. status: "success",
  1747. message: "Playlist has been successfully updated"
  1748. });
  1749. }
  1750. );
  1751. }),
  1752. /**
  1753. * Updates the privacy of a playlist
  1754. *
  1755. * @param {object} session - the session object automatically added by the websocket
  1756. * @param {string} playlistId - the id of the playlist we are updating the privacy for
  1757. * @param {string} privacy - what the new privacy of the playlist should be e.g. public
  1758. * @param {Function} cb - gets called with the result
  1759. */
  1760. updatePrivacyAdmin: useHasPermission(
  1761. "playlists.updatePrivacyAdmin",
  1762. async function updatePrivacyAdmin(session, playlistId, privacy, cb) {
  1763. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1764. async.waterfall(
  1765. [
  1766. next => {
  1767. playlistModel.updateOne(
  1768. { _id: playlistId },
  1769. { $set: { privacy } },
  1770. { runValidators: true },
  1771. next
  1772. );
  1773. },
  1774. (res, next) => {
  1775. if (res.n === 0) next("No playlist found with that id.");
  1776. else if (res.nModified === 0) next(`Nothing changed, the playlist was already ${privacy}.`);
  1777. else {
  1778. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  1779. .then(playlist => next(null, playlist))
  1780. .catch(next);
  1781. }
  1782. }
  1783. ],
  1784. async (err, playlist) => {
  1785. if (err) {
  1786. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1787. this.log(
  1788. "ERROR",
  1789. "PLAYLIST_UPDATE_PRIVACY_ADMIN",
  1790. `Updating privacy to "${privacy}" for playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1791. );
  1792. return cb({ status: "error", message: err });
  1793. }
  1794. this.log(
  1795. "SUCCESS",
  1796. "PLAYLIST_UPDATE_PRIVACY_ADMIn",
  1797. `Successfully updated privacy to "${privacy}" for playlist "${playlistId}" for user "${session.userId}".`
  1798. );
  1799. if (playlist.type === "user") {
  1800. CacheModule.runJob("PUB", {
  1801. channel: "playlist.updatePrivacy",
  1802. value: {
  1803. userId: playlist.createdBy,
  1804. playlist
  1805. }
  1806. });
  1807. }
  1808. CacheModule.runJob("PUB", {
  1809. channel: "playlist.updated",
  1810. value: { playlistId }
  1811. });
  1812. return cb({
  1813. status: "success",
  1814. message: "Playlist has been successfully updated"
  1815. });
  1816. }
  1817. );
  1818. }
  1819. ),
  1820. /**
  1821. * Deletes all orphaned station playlists
  1822. *
  1823. * @param {object} session - the session object automatically added by socket.io
  1824. * @param {Function} cb - gets called with the result
  1825. */
  1826. deleteOrphanedStationPlaylists: useHasPermission(
  1827. "playlists.deleteOrphanedStationPlaylists",
  1828. async function index(session, cb) {
  1829. this.keepLongJob();
  1830. this.publishProgress({
  1831. status: "started",
  1832. title: "Delete orphaned station playlists",
  1833. message: "Deleting orphaned station playlists.",
  1834. id: this.toString()
  1835. });
  1836. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  1837. await CacheModule.runJob(
  1838. "PUB",
  1839. {
  1840. channel: "longJob.added",
  1841. value: { jobId: this.toString(), userId: session.userId }
  1842. },
  1843. this
  1844. );
  1845. async.waterfall(
  1846. [
  1847. next => {
  1848. PlaylistsModule.runJob("DELETE_ORPHANED_STATION_PLAYLISTS", {}, this)
  1849. .then(() => next())
  1850. .catch(next);
  1851. }
  1852. ],
  1853. async err => {
  1854. if (err) {
  1855. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1856. this.log(
  1857. "ERROR",
  1858. "PLAYLISTS_DELETE_ORPHANED_STATION_PLAYLISTS",
  1859. `Deleting orphaned station playlists failed. "${err}"`
  1860. );
  1861. this.publishProgress({
  1862. status: "error",
  1863. message: err
  1864. });
  1865. return cb({ status: "error", message: err });
  1866. }
  1867. this.log(
  1868. "SUCCESS",
  1869. "PLAYLISTS_DELETE_ORPHANED_STATION_PLAYLISTS",
  1870. "Deleting orphaned station playlists successful."
  1871. );
  1872. this.publishProgress({
  1873. status: "success",
  1874. message: "Successfully deleted orphaned station playlists."
  1875. });
  1876. return cb({ status: "success", message: "Successfully deleted orphaned station playlists." });
  1877. }
  1878. );
  1879. }
  1880. ),
  1881. /**
  1882. * Deletes all orphaned genre playlists
  1883. *
  1884. * @param {object} session - the session object automatically added by socket.io
  1885. * @param {Function} cb - gets called with the result
  1886. */
  1887. deleteOrphanedGenrePlaylists: useHasPermission(
  1888. "playlists.deleteOrphanedGenrePlaylists",
  1889. async function index(session, cb) {
  1890. this.keepLongJob();
  1891. this.publishProgress({
  1892. status: "started",
  1893. title: "Delete orphaned genre playlists",
  1894. message: "Deleting orphaned genre playlists.",
  1895. id: this.toString()
  1896. });
  1897. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  1898. await CacheModule.runJob(
  1899. "PUB",
  1900. {
  1901. channel: "longJob.added",
  1902. value: { jobId: this.toString(), userId: session.userId }
  1903. },
  1904. this
  1905. );
  1906. async.waterfall(
  1907. [
  1908. next => {
  1909. PlaylistsModule.runJob("DELETE_ORPHANED_GENRE_PLAYLISTS", {}, this)
  1910. .then(() => next())
  1911. .catch(next);
  1912. }
  1913. ],
  1914. async err => {
  1915. if (err) {
  1916. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1917. this.log(
  1918. "ERROR",
  1919. "PLAYLISTS_DELETE_ORPHANED_GENRE_PLAYLISTS",
  1920. `Deleting orphaned genre playlists failed. "${err}"`
  1921. );
  1922. this.publishProgress({
  1923. status: "error",
  1924. message: err
  1925. });
  1926. return cb({ status: "error", message: err });
  1927. }
  1928. this.log(
  1929. "SUCCESS",
  1930. "PLAYLISTS_DELETE_ORPHANED_GENRE_PLAYLISTS",
  1931. "Deleting orphaned genre playlists successful."
  1932. );
  1933. this.publishProgress({
  1934. status: "success",
  1935. message: "Successfully deleted orphaned genre playlists."
  1936. });
  1937. return cb({ status: "success", message: "Successfully deleted orphaned genre playlists." });
  1938. }
  1939. );
  1940. }
  1941. ),
  1942. /**
  1943. * Requests orpahned playlist songs
  1944. *
  1945. * @param {object} session - the session object automatically added by socket.io
  1946. * @param {Function} cb - gets called with the result
  1947. */
  1948. requestOrphanedPlaylistSongs: useHasPermission(
  1949. "playlists.requestOrphanedPlaylistSongs",
  1950. async function index(session, cb) {
  1951. this.keepLongJob();
  1952. this.publishProgress({
  1953. status: "started",
  1954. title: "Request orphaned playlist songs",
  1955. message: "Requesting orphaned playlist songs.",
  1956. id: this.toString()
  1957. });
  1958. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  1959. await CacheModule.runJob(
  1960. "PUB",
  1961. {
  1962. channel: "longJob.added",
  1963. value: { jobId: this.toString(), userId: session.userId }
  1964. },
  1965. this
  1966. );
  1967. async.waterfall(
  1968. [
  1969. next => {
  1970. SongsModule.runJob("REQUEST_ORPHANED_PLAYLIST_SONGS", {}, this)
  1971. .then(() => next())
  1972. .catch(next);
  1973. }
  1974. ],
  1975. async err => {
  1976. if (err) {
  1977. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1978. this.log(
  1979. "ERROR",
  1980. "REQUEST_ORPHANED_PLAYLIST_SONGS",
  1981. `Requesting orphaned playlist songs failed. "${err}"`
  1982. );
  1983. this.publishProgress({
  1984. status: "error",
  1985. message: err
  1986. });
  1987. return cb({ status: "error", message: err });
  1988. }
  1989. this.log(
  1990. "SUCCESS",
  1991. "REQUEST_ORPHANED_PLAYLIST_SONGS",
  1992. "Requesting orphaned playlist songs was successful."
  1993. );
  1994. this.publishProgress({
  1995. status: "success",
  1996. message: "Successfully requested orphaned playlist songs."
  1997. });
  1998. return cb({ status: "success", message: "Successfully requested orphaned playlist songs." });
  1999. }
  2000. );
  2001. }
  2002. ),
  2003. /**
  2004. * Clears and refills a station playlist
  2005. *
  2006. * @param {object} session - the session object automatically added by socket.io
  2007. * @param {string} playlistId - the id of the playlist we are clearing and refilling
  2008. * @param {Function} cb - gets called with the result
  2009. */
  2010. clearAndRefillStationPlaylist: useHasPermission(
  2011. "playlists.clearAndRefillStationPlaylist",
  2012. async function index(session, playlistId, cb) {
  2013. async.waterfall(
  2014. [
  2015. next => {
  2016. if (!playlistId) next("Please specify a playlist id");
  2017. else {
  2018. PlaylistsModule.runJob("CLEAR_AND_REFILL_STATION_PLAYLIST", { playlistId }, this)
  2019. .then(() => {
  2020. next();
  2021. })
  2022. .catch(err => {
  2023. next(err);
  2024. });
  2025. }
  2026. }
  2027. ],
  2028. async err => {
  2029. if (err) {
  2030. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2031. this.log(
  2032. "ERROR",
  2033. "PLAYLIST_CLEAR_AND_REFILL_STATION_PLAYLIST",
  2034. `Clearing and refilling station playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  2035. );
  2036. return cb({ status: "error", message: err });
  2037. }
  2038. this.log(
  2039. "SUCCESS",
  2040. "PLAYLIST_CLEAR_AND_REFILL_STATION_PLAYLIST",
  2041. `Successfully cleared and refilled station playlist "${playlistId}" for user "${session.userId}".`
  2042. );
  2043. return cb({
  2044. status: "success",
  2045. message: "Playlist has been successfully cleared and refilled"
  2046. });
  2047. }
  2048. );
  2049. }
  2050. ),
  2051. /**
  2052. * Clears and refills a genre playlist
  2053. *
  2054. * @param {object} session - the session object automatically added by socket.io
  2055. * @param {string} playlistId - the id of the playlist we are clearing and refilling
  2056. * @param {Function} cb - gets called with the result
  2057. */
  2058. clearAndRefillGenrePlaylist: useHasPermission(
  2059. "playlists.clearAndRefillGenrePlaylist",
  2060. async function index(session, playlistId, cb) {
  2061. async.waterfall(
  2062. [
  2063. next => {
  2064. if (!playlistId) next("Please specify a playlist id");
  2065. else {
  2066. PlaylistsModule.runJob("CLEAR_AND_REFILL_GENRE_PLAYLIST", { playlistId }, this)
  2067. .then(() => {
  2068. next();
  2069. })
  2070. .catch(err => {
  2071. next(err);
  2072. });
  2073. }
  2074. }
  2075. ],
  2076. async err => {
  2077. if (err) {
  2078. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2079. this.log(
  2080. "ERROR",
  2081. "PLAYLIST_CLEAR_AND_REFILL_GENRE_PLAYLIST",
  2082. `Clearing and refilling genre playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  2083. );
  2084. return cb({ status: "error", message: err });
  2085. }
  2086. this.log(
  2087. "SUCCESS",
  2088. "PLAYLIST_CLEAR_AND_REFILL_GENRE_PLAYLIST",
  2089. `Successfully cleared and refilled genre playlist "${playlistId}" for user "${session.userId}".`
  2090. );
  2091. return cb({
  2092. status: "success",
  2093. message: "Playlist has been successfully cleared and refilled"
  2094. });
  2095. }
  2096. );
  2097. }
  2098. ),
  2099. /**
  2100. * Clears and refills all station playlists
  2101. *
  2102. * @param {object} session - the session object automatically added by socket.io
  2103. * @param {Function} cb - gets called with the result
  2104. */
  2105. clearAndRefillAllStationPlaylists: useHasPermission(
  2106. "playlists.clearAndRefillAllStationPlaylists",
  2107. async function index(session, cb) {
  2108. this.keepLongJob();
  2109. this.publishProgress({
  2110. status: "started",
  2111. title: "Clear and refill all station playlists",
  2112. message: "Clearing and refilling all station playlists.",
  2113. id: this.toString()
  2114. });
  2115. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  2116. await CacheModule.runJob(
  2117. "PUB",
  2118. {
  2119. channel: "longJob.added",
  2120. value: { jobId: this.toString(), userId: session.userId }
  2121. },
  2122. this
  2123. );
  2124. async.waterfall(
  2125. [
  2126. next => {
  2127. PlaylistsModule.runJob("GET_ALL_STATION_PLAYLISTS", {}, this)
  2128. .then(response => {
  2129. next(null, response.playlists);
  2130. })
  2131. .catch(err => {
  2132. next(err);
  2133. });
  2134. },
  2135. (playlists, next) => {
  2136. async.eachLimit(
  2137. playlists,
  2138. 1,
  2139. (playlist, next) => {
  2140. this.publishProgress({
  2141. status: "update",
  2142. message: `Clearing and refilling "${playlist._id}"`
  2143. });
  2144. PlaylistsModule.runJob(
  2145. "CLEAR_AND_REFILL_STATION_PLAYLIST",
  2146. { playlistId: playlist._id },
  2147. this
  2148. )
  2149. .then(() => {
  2150. next();
  2151. })
  2152. .catch(err => {
  2153. next(err);
  2154. });
  2155. },
  2156. next
  2157. );
  2158. }
  2159. ],
  2160. async err => {
  2161. if (err) {
  2162. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2163. this.log(
  2164. "ERROR",
  2165. "PLAYLIST_CLEAR_AND_REFILL_ALL_STATION_PLAYLISTS",
  2166. `Clearing and refilling all station playlists failed for user "${session.userId}". "${err}"`
  2167. );
  2168. this.publishProgress({
  2169. status: "error",
  2170. message: err
  2171. });
  2172. return cb({ status: "error", message: err });
  2173. }
  2174. this.log(
  2175. "SUCCESS",
  2176. "PLAYLIST_CLEAR_AND_REFILL_ALL_STATION_PLAYLISTS",
  2177. `Successfully cleared and refilled all station playlists for user "${session.userId}".`
  2178. );
  2179. this.publishProgress({
  2180. status: "success",
  2181. message: "Playlists have been successfully cleared and refilled."
  2182. });
  2183. return cb({
  2184. status: "success",
  2185. message: "Playlists have been successfully cleared and refilled"
  2186. });
  2187. }
  2188. );
  2189. }
  2190. ),
  2191. /**
  2192. * Clears and refills all genre playlists
  2193. *
  2194. * @param {object} session - the session object automatically added by socket.io
  2195. * @param {Function} cb - gets called with the result
  2196. */
  2197. clearAndRefillAllGenrePlaylists: useHasPermission(
  2198. "playlists.clearAndRefillAllGenrePlaylists",
  2199. async function index(session, cb) {
  2200. this.keepLongJob();
  2201. this.publishProgress({
  2202. status: "started",
  2203. title: "Clear and refill all genre playlists",
  2204. message: "Clearing and refilling all genre playlists.",
  2205. id: this.toString()
  2206. });
  2207. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  2208. await CacheModule.runJob(
  2209. "PUB",
  2210. {
  2211. channel: "longJob.added",
  2212. value: { jobId: this.toString(), userId: session.userId }
  2213. },
  2214. this
  2215. );
  2216. async.waterfall(
  2217. [
  2218. next => {
  2219. PlaylistsModule.runJob("GET_ALL_GENRE_PLAYLISTS", {}, this)
  2220. .then(response => {
  2221. next(null, response.playlists);
  2222. })
  2223. .catch(err => {
  2224. next(err);
  2225. });
  2226. },
  2227. (playlists, next) => {
  2228. async.eachLimit(
  2229. playlists,
  2230. 1,
  2231. (playlist, next) => {
  2232. this.publishProgress({
  2233. status: "update",
  2234. message: `Clearing and refilling "${playlist._id}"`
  2235. });
  2236. PlaylistsModule.runJob(
  2237. "CLEAR_AND_REFILL_GENRE_PLAYLIST",
  2238. { playlistId: playlist._id },
  2239. this
  2240. )
  2241. .then(() => {
  2242. next();
  2243. })
  2244. .catch(err => {
  2245. next(err);
  2246. });
  2247. },
  2248. next
  2249. );
  2250. }
  2251. ],
  2252. async err => {
  2253. if (err) {
  2254. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2255. this.log(
  2256. "ERROR",
  2257. "PLAYLIST_CLEAR_AND_REFILL_ALL_GENRE_PLAYLISTS",
  2258. `Clearing and refilling all genre playlists failed for user "${session.userId}". "${err}"`
  2259. );
  2260. this.publishProgress({
  2261. status: "error",
  2262. message: err
  2263. });
  2264. return cb({ status: "error", message: err });
  2265. }
  2266. this.log(
  2267. "SUCCESS",
  2268. "PLAYLIST_CLEAR_AND_REFILL_ALL_GENRE_PLAYLISTS",
  2269. `Successfully cleared and refilled all genre playlists for user "${session.userId}".`
  2270. );
  2271. this.publishProgress({
  2272. status: "success",
  2273. message: "Playlists have been successfully cleared and refilled."
  2274. });
  2275. return cb({
  2276. status: "success",
  2277. message: "Playlists have been successfully cleared and refilled"
  2278. });
  2279. }
  2280. );
  2281. }
  2282. ),
  2283. /**
  2284. * Create missing genre playlists
  2285. *
  2286. * @param {object} session - the session object automatically added by socket.io
  2287. * @param {Function} cb - gets called with the result
  2288. */
  2289. createMissingGenrePlaylists: useHasPermission(
  2290. "playlists.createMissingGenrePlaylists",
  2291. async function index(session, cb) {
  2292. this.keepLongJob();
  2293. this.publishProgress({
  2294. status: "started",
  2295. title: "Create missing genre playlists",
  2296. message: "Creating missing genre playlists.",
  2297. id: this.toString()
  2298. });
  2299. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  2300. await CacheModule.runJob(
  2301. "PUB",
  2302. {
  2303. channel: "longJob.added",
  2304. value: { jobId: this.toString(), userId: session.userId }
  2305. },
  2306. this
  2307. );
  2308. async.waterfall(
  2309. [
  2310. next => {
  2311. PlaylistsModule.runJob("CREATE_MISSING_GENRE_PLAYLISTS", this)
  2312. .then(() => {
  2313. next();
  2314. })
  2315. .catch(err => {
  2316. next(err);
  2317. });
  2318. }
  2319. ],
  2320. async err => {
  2321. if (err) {
  2322. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  2323. this.log(
  2324. "ERROR",
  2325. "PLAYLIST_CREATE_MISSING_GENRE_PLAYLISTS",
  2326. `Creating missing genre playlists failed for user "${session.userId}". "${err}"`
  2327. );
  2328. this.publishProgress({
  2329. status: "error",
  2330. message: err
  2331. });
  2332. return cb({ status: "error", message: err });
  2333. }
  2334. this.log(
  2335. "SUCCESS",
  2336. "PLAYLIST_CREATE_MISSING_GENRE_PLAYLISTS",
  2337. `Successfully created missing genre playlists for user "${session.userId}".`
  2338. );
  2339. this.publishProgress({
  2340. status: "success",
  2341. message: "Missing genre playlists have been successfully created."
  2342. });
  2343. return cb({
  2344. status: "success",
  2345. message: "Missing genre playlists have been successfully created"
  2346. });
  2347. }
  2348. );
  2349. }
  2350. )
  2351. };