youtube.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. import mongoose from "mongoose";
  2. import async from "async";
  3. import { isAdminRequired, isLoginRequired } from "./hooks";
  4. // eslint-disable-next-line
  5. import moduleManager from "../../index";
  6. const DBModule = moduleManager.modules.db;
  7. const CacheModule = moduleManager.modules.cache;
  8. const UtilsModule = moduleManager.modules.utils;
  9. const YouTubeModule = moduleManager.modules.youtube;
  10. const MediaModule = moduleManager.modules.media;
  11. export default {
  12. /**
  13. * Returns details about the YouTube quota usage
  14. *
  15. * @returns {{status: string, data: object}}
  16. */
  17. getQuotaStatus: isAdminRequired(function getQuotaStatus(session, fromDate, cb) {
  18. YouTubeModule.runJob("GET_QUOTA_STATUS", { fromDate }, this)
  19. .then(response => {
  20. this.log("SUCCESS", "YOUTUBE_GET_QUOTA_STATUS", `Getting quota status was successful.`);
  21. return cb({ status: "success", data: { status: response.status } });
  22. })
  23. .catch(async err => {
  24. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  25. this.log("ERROR", "YOUTUBE_GET_QUOTA_STATUS", `Getting quota status failed. "${err}"`);
  26. return cb({ status: "error", message: err });
  27. });
  28. }),
  29. /**
  30. * Returns YouTube quota chart data
  31. *
  32. * @param {object} session - the session object automatically added by the websocket
  33. * @param timePeriod - either hours or days
  34. * @param startDate - beginning date
  35. * @param endDate - end date
  36. * @param dataType - either usage or count
  37. * @returns {{status: string, data: object}}
  38. */
  39. getQuotaChartData: isAdminRequired(function getQuotaChartData(
  40. session,
  41. timePeriod,
  42. startDate,
  43. endDate,
  44. dataType,
  45. cb
  46. ) {
  47. YouTubeModule.runJob(
  48. "GET_QUOTA_CHART_DATA",
  49. { timePeriod, startDate: new Date(startDate), endDate: new Date(endDate), dataType },
  50. this
  51. )
  52. .then(data => {
  53. this.log("SUCCESS", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data was successful.`);
  54. return cb({ status: "success", data });
  55. })
  56. .catch(async err => {
  57. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  58. this.log("ERROR", "YOUTUBE_GET_QUOTA_CHART_DATA", `Getting quota chart data failed. "${err}"`);
  59. return cb({ status: "error", message: err });
  60. });
  61. }),
  62. /**
  63. * Gets api requests, used in the admin youtube page by the AdvancedTable component
  64. *
  65. * @param {object} session - the session object automatically added by the websocket
  66. * @param page - the page
  67. * @param pageSize - the size per page
  68. * @param properties - the properties to return for each news item
  69. * @param sort - the sort object
  70. * @param queries - the queries array
  71. * @param operator - the operator for queries
  72. * @param cb
  73. */
  74. getApiRequests: isAdminRequired(async function getApiRequests(
  75. session,
  76. page,
  77. pageSize,
  78. properties,
  79. sort,
  80. queries,
  81. operator,
  82. cb
  83. ) {
  84. async.waterfall(
  85. [
  86. next => {
  87. DBModule.runJob(
  88. "GET_DATA",
  89. {
  90. page,
  91. pageSize,
  92. properties,
  93. sort,
  94. queries,
  95. operator,
  96. modelName: "youtubeApiRequest",
  97. blacklistedProperties: [],
  98. specialProperties: {},
  99. specialQueries: {}
  100. },
  101. this
  102. )
  103. .then(response => {
  104. next(null, response);
  105. })
  106. .catch(err => {
  107. next(err);
  108. });
  109. }
  110. ],
  111. async (err, response) => {
  112. if (err && err !== true) {
  113. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  114. this.log("ERROR", "YOUTUBE_GET_API_REQUESTS", `Failed to get YouTube api requests. "${err}"`);
  115. return cb({ status: "error", message: err });
  116. }
  117. this.log("SUCCESS", "YOUTUBE_GET_API_REQUESTS", `Fetched YouTube api requests successfully.`);
  118. return cb({
  119. status: "success",
  120. message: "Successfully fetched YouTube api requests.",
  121. data: response
  122. });
  123. }
  124. );
  125. }),
  126. /**
  127. * Returns a specific api request
  128. *
  129. * @returns {{status: string, data: object}}
  130. */
  131. getApiRequest: isAdminRequired(function getApiRequest(session, apiRequestId, cb) {
  132. if (!mongoose.Types.ObjectId.isValid(apiRequestId))
  133. return cb({ status: "error", message: "Api request id is not a valid ObjectId." });
  134. return YouTubeModule.runJob("GET_API_REQUEST", { apiRequestId }, this)
  135. .then(response => {
  136. this.log(
  137. "SUCCESS",
  138. "YOUTUBE_GET_API_REQUEST",
  139. `Getting api request with id ${apiRequestId} was successful.`
  140. );
  141. return cb({ status: "success", data: { apiRequest: response.apiRequest } });
  142. })
  143. .catch(async err => {
  144. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  145. this.log(
  146. "ERROR",
  147. "YOUTUBE_GET_API_REQUEST",
  148. `Getting api request with id ${apiRequestId} failed. "${err}"`
  149. );
  150. return cb({ status: "error", message: err });
  151. });
  152. }),
  153. /**
  154. * Reset stored API requests
  155. *
  156. * @returns {{status: string, data: object}}
  157. */
  158. resetStoredApiRequests: isAdminRequired(async function resetStoredApiRequests(session, cb) {
  159. this.keepLongJob();
  160. this.publishProgress({
  161. status: "started",
  162. title: "Reset stored API requests",
  163. message: "Resetting stored API requests.",
  164. id: this.toString()
  165. });
  166. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  167. await CacheModule.runJob(
  168. "PUB",
  169. {
  170. channel: "longJob.added",
  171. value: { jobId: this.toString(), userId: session.userId }
  172. },
  173. this
  174. );
  175. YouTubeModule.runJob("RESET_STORED_API_REQUESTS", {}, this)
  176. .then(() => {
  177. this.log(
  178. "SUCCESS",
  179. "YOUTUBE_RESET_STORED_API_REQUESTS",
  180. `Resetting stored API requests was successful.`
  181. );
  182. this.publishProgress({
  183. status: "success",
  184. message: "Successfully reset stored YouTube API requests."
  185. });
  186. return cb({ status: "success", message: "Successfully reset stored YouTube API requests" });
  187. })
  188. .catch(async err => {
  189. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  190. this.log(
  191. "ERROR",
  192. "YOUTUBE_RESET_STORED_API_REQUESTS",
  193. `Resetting stored API requests failed. "${err}"`
  194. );
  195. this.publishProgress({
  196. status: "error",
  197. message: err
  198. });
  199. return cb({ status: "error", message: err });
  200. });
  201. }),
  202. /**
  203. * Remove stored API requests
  204. *
  205. * @returns {{status: string, data: object}}
  206. */
  207. removeStoredApiRequest: isAdminRequired(function removeStoredApiRequest(session, requestId, cb) {
  208. YouTubeModule.runJob("REMOVE_STORED_API_REQUEST", { requestId }, this)
  209. .then(() => {
  210. this.log(
  211. "SUCCESS",
  212. "YOUTUBE_REMOVE_STORED_API_REQUEST",
  213. `Removing stored API request "${requestId}" was successful.`
  214. );
  215. return cb({ status: "success", message: "Successfully removed stored YouTube API request" });
  216. })
  217. .catch(async err => {
  218. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  219. this.log(
  220. "ERROR",
  221. "YOUTUBE_REMOVE_STORED_API_REQUEST",
  222. `Removing stored API request "${requestId}" failed. "${err}"`
  223. );
  224. return cb({ status: "error", message: err });
  225. });
  226. }),
  227. /**
  228. * Gets videos, used in the admin youtube page by the AdvancedTable component
  229. *
  230. * @param {object} session - the session object automatically added by the websocket
  231. * @param page - the page
  232. * @param pageSize - the size per page
  233. * @param properties - the properties to return for each news item
  234. * @param sort - the sort object
  235. * @param queries - the queries array
  236. * @param operator - the operator for queries
  237. * @param cb
  238. */
  239. getVideos: isAdminRequired(async function getVideos(
  240. session,
  241. page,
  242. pageSize,
  243. properties,
  244. sort,
  245. queries,
  246. operator,
  247. cb
  248. ) {
  249. async.waterfall(
  250. [
  251. next => {
  252. DBModule.runJob(
  253. "GET_DATA",
  254. {
  255. page,
  256. pageSize,
  257. properties,
  258. sort,
  259. queries,
  260. operator,
  261. modelName: "youtubeVideo",
  262. blacklistedProperties: [],
  263. specialProperties: {
  264. songId: [
  265. // Fetch songs from songs collection with a matching youtubeId
  266. {
  267. $lookup: {
  268. from: "songs",
  269. localField: "youtubeId",
  270. foreignField: "youtubeId",
  271. as: "song"
  272. }
  273. },
  274. // Turn the array of songs returned in the last step into one object, since only one song should have been returned maximum
  275. {
  276. $unwind: {
  277. path: "$song",
  278. preserveNullAndEmptyArrays: true
  279. }
  280. },
  281. // Add new field songId, which grabs the song object's _id and tries turning it into a string
  282. {
  283. $addFields: {
  284. songId: {
  285. $convert: {
  286. input: "$song._id",
  287. to: "string",
  288. onError: "",
  289. onNull: ""
  290. }
  291. }
  292. }
  293. },
  294. // Cleanup, don't return the song object for any further steps
  295. {
  296. $project: {
  297. song: 0
  298. }
  299. }
  300. ]
  301. },
  302. specialQueries: {},
  303. specialFilters: {
  304. importJob: importJobId => [
  305. {
  306. $lookup: {
  307. from: "importjobs",
  308. let: { youtubeId: "$youtubeId" },
  309. pipeline: [
  310. {
  311. $match: {
  312. _id: mongoose.Types.ObjectId(importJobId)
  313. }
  314. },
  315. {
  316. $addFields: {
  317. importJob: {
  318. $in: ["$$youtubeId", "$response.successfulVideoIds"]
  319. }
  320. }
  321. },
  322. {
  323. $project: {
  324. importJob: 1,
  325. _id: 0
  326. }
  327. }
  328. ],
  329. as: "importJob"
  330. }
  331. },
  332. {
  333. $unwind: "$importJob"
  334. },
  335. {
  336. $set: {
  337. importJob: "$importJob.importJob"
  338. }
  339. }
  340. ]
  341. }
  342. },
  343. this
  344. )
  345. .then(response => {
  346. next(null, response);
  347. })
  348. .catch(err => {
  349. next(err);
  350. });
  351. }
  352. ],
  353. async (err, response) => {
  354. if (err && err !== true) {
  355. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  356. this.log("ERROR", "YOUTUBE_GET_VIDEOS", `Failed to get YouTube videos. "${err}"`);
  357. return cb({ status: "error", message: err });
  358. }
  359. this.log("SUCCESS", "YOUTUBE_GET_VIDEOS", `Fetched YouTube videos successfully.`);
  360. return cb({
  361. status: "success",
  362. message: "Successfully fetched YouTube videos.",
  363. data: response
  364. });
  365. }
  366. );
  367. }),
  368. /**
  369. * Get a YouTube video
  370. *
  371. * @returns {{status: string, data: object}}
  372. */
  373. getVideo: isLoginRequired(function getVideo(session, identifier, createMissing, cb) {
  374. YouTubeModule.runJob("GET_VIDEO", { identifier, createMissing }, this)
  375. .then(res => {
  376. this.log("SUCCESS", "YOUTUBE_GET_VIDEO", `Fetching video was successful.`);
  377. return cb({ status: "success", message: "Successfully fetched YouTube video", data: res.video });
  378. })
  379. .catch(async err => {
  380. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  381. this.log("ERROR", "YOUTUBE_GET_VIDEO", `Fetching video failed. "${err}"`);
  382. return cb({ status: "error", message: err });
  383. });
  384. }),
  385. /**
  386. * Remove YouTube videos
  387. *
  388. * @returns {{status: string, data: object}}
  389. */
  390. removeVideos: isAdminRequired(async function removeVideos(session, videoIds, cb) {
  391. this.keepLongJob();
  392. this.publishProgress({
  393. status: "started",
  394. title: "Bulk remove YouTube videos",
  395. message: "Bulk removing YouTube videos.",
  396. id: this.toString()
  397. });
  398. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  399. await CacheModule.runJob(
  400. "PUB",
  401. {
  402. channel: "longJob.added",
  403. value: { jobId: this.toString(), userId: session.userId }
  404. },
  405. this
  406. );
  407. YouTubeModule.runJob("REMOVE_VIDEOS", { videoIds }, this)
  408. .then(() => {
  409. this.log("SUCCESS", "YOUTUBE_REMOVE_VIDEOS", `Removing videos was successful.`);
  410. this.publishProgress({
  411. status: "success",
  412. message: "Successfully removed YouTube videos."
  413. });
  414. return cb({ status: "success", message: "Successfully removed YouTube videos" });
  415. })
  416. .catch(async err => {
  417. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  418. this.log("ERROR", "YOUTUBE_REMOVE_VIDEOS", `Removing videos failed. "${err}"`);
  419. this.publishProgress({
  420. status: "error",
  421. message: err
  422. });
  423. return cb({ status: "error", message: err });
  424. });
  425. }),
  426. /**
  427. * Requests a set of YouTube videos
  428. *
  429. * @param {object} session - the session object automatically added by the websocket
  430. * @param {string} url - the url of the the YouTube playlist
  431. * @param {boolean} musicOnly - whether to only get music from the playlist
  432. * @param {boolean} musicOnly - whether to return videos
  433. * @param {Function} cb - gets called with the result
  434. */
  435. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnVideos, cb) {
  436. YouTubeModule.runJob("REQUEST_SET", { url, musicOnly, returnVideos }, this)
  437. .then(response => {
  438. this.log(
  439. "SUCCESS",
  440. "REQUEST_SET",
  441. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  442. );
  443. return cb({
  444. status: "success",
  445. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  446. videos: returnVideos ? response.videos : null
  447. });
  448. })
  449. .catch(async err => {
  450. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  451. this.log(
  452. "ERROR",
  453. "REQUEST_SET",
  454. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  455. );
  456. return cb({ status: "error", message: err });
  457. });
  458. }),
  459. /**
  460. * Requests a set of YouTube videos as an admin
  461. *
  462. * @param {object} session - the session object automatically added by the websocket
  463. * @param {string} url - the url of the the YouTube playlist
  464. * @param {boolean} musicOnly - whether to only get music from the playlist
  465. * @param {boolean} musicOnly - whether to return videos
  466. * @param {Function} cb - gets called with the result
  467. */
  468. requestSetAdmin: isAdminRequired(async function requestSetAdmin(session, url, musicOnly, returnVideos, cb) {
  469. const importJobModel = await DBModule.runJob("GET_MODEL", { modelName: "importJob" }, this);
  470. this.keepLongJob();
  471. this.publishProgress({
  472. status: "started",
  473. title: "Import playlist",
  474. message: "Importing playlist.",
  475. id: this.toString()
  476. });
  477. await CacheModule.runJob("RPUSH", { key: `longJobs.${session.userId}`, value: this.toString() }, this);
  478. await CacheModule.runJob(
  479. "PUB",
  480. {
  481. channel: "longJob.added",
  482. value: { jobId: this.toString(), userId: session.userId }
  483. },
  484. this
  485. );
  486. async.waterfall(
  487. [
  488. next => {
  489. importJobModel.create(
  490. {
  491. type: "youtube",
  492. query: {
  493. url,
  494. musicOnly
  495. },
  496. status: "in-progress",
  497. response: {},
  498. requestedBy: session.userId,
  499. requestedAt: Date.now()
  500. },
  501. next
  502. );
  503. },
  504. (importJob, next) => {
  505. YouTubeModule.runJob("REQUEST_SET", { url, musicOnly, returnVideos }, this)
  506. .then(response => {
  507. next(null, importJob, response);
  508. })
  509. .catch(err => {
  510. next(err, importJob);
  511. });
  512. },
  513. (importJob, response, next) => {
  514. importJobModel.updateOne(
  515. { _id: importJob._id },
  516. {
  517. $set: {
  518. status: "success",
  519. response: {
  520. failed: response.failed,
  521. successful: response.successful,
  522. alreadyInDatabase: response.alreadyInDatabase,
  523. successfulVideoIds: response.successfulVideoIds,
  524. failedVideoIds: response.failedVideoIds
  525. }
  526. }
  527. },
  528. err => {
  529. if (err) next(err, importJob);
  530. else
  531. MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id })
  532. .then(() => next(null, importJob, response))
  533. .catch(error => next(error, importJob));
  534. }
  535. );
  536. }
  537. ],
  538. async (err, importJob, response) => {
  539. if (err) {
  540. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  541. this.log(
  542. "ERROR",
  543. "REQUEST_SET_ADMIN",
  544. `Importing a YouTube playlist to be requested failed for admin "${session.userId}". "${err}"`
  545. );
  546. importJobModel.updateOne({ _id: importJob._id }, { $set: { status: "error" } });
  547. MediaModule.runJob("UPDATE_IMPORT_JOBS", { jobIds: importJob._id });
  548. return cb({ status: "error", message: err });
  549. }
  550. this.log(
  551. "SUCCESS",
  552. "REQUEST_SET_ADMIN",
  553. `Successfully imported a YouTube playlist to be requested for admin "${session.userId}".`
  554. );
  555. this.publishProgress({
  556. status: "success",
  557. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`
  558. });
  559. return cb({
  560. status: "success",
  561. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  562. videos: returnVideos ? response.videos : null
  563. });
  564. }
  565. );
  566. })
  567. };