playlists.js 72 KB

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