Browse Source

Merge branch 'staging'

Owen Diffey 1 month ago
parent
commit
a3ae7aa932
100 changed files with 1609 additions and 1854 deletions
  1. 1 0
      .env.example
  2. 3 2
      .wiki/Configuration.md
  3. 37 0
      CHANGELOG.md
  4. 2 2
      backend/Dockerfile
  5. 0 1
      backend/classes/Timer.class.js
  6. 1 0
      backend/config/custom-environment-variables.json
  7. 9 5
      backend/config/default.json
  8. 0 26
      backend/core.js
  9. 0 9
      backend/index.js
  10. 0 4
      backend/logic/actions/activities.js
  11. 0 11
      backend/logic/actions/apis.js
  12. 0 2
      backend/logic/actions/dataRequests.js
  13. 0 10
      backend/logic/actions/media.js
  14. 0 7
      backend/logic/actions/news.js
  15. 469 508
      backend/logic/actions/playlists.js
  16. 0 5
      backend/logic/actions/punishments.js
  17. 0 8
      backend/logic/actions/reports.js
  18. 0 20
      backend/logic/actions/songs.js
  19. 0 5
      backend/logic/actions/soundcloud.js
  20. 0 3
      backend/logic/actions/spotify.js
  21. 9 44
      backend/logic/actions/stations.js
  22. 43 84
      backend/logic/actions/users.js
  23. 0 1
      backend/logic/actions/utils.js
  24. 0 16
      backend/logic/actions/youtube.js
  25. 0 5
      backend/logic/activities.js
  26. 0 1
      backend/logic/api.js
  27. 0 3
      backend/logic/app.js
  28. 10 29
      backend/logic/cache/index.js
  29. 0 1
      backend/logic/cache/schemas/playlist.js
  30. 0 1
      backend/logic/cache/schemas/station.js
  31. 0 6
      backend/logic/db/index.js
  32. 1 0
      backend/logic/db/schemas/playlist.js
  33. 5 4
      backend/logic/hooks/hasPermission.js
  34. 0 3
      backend/logic/mail/index.js
  35. 2 3
      backend/logic/mail/schemas/dataRequest.js
  36. 0 1
      backend/logic/mail/schemas/passwordRequest.js
  37. 0 1
      backend/logic/mail/schemas/resetPasswordRequest.js
  38. 0 1
      backend/logic/mail/schemas/verifyEmail.js
  39. 0 9
      backend/logic/media.js
  40. 0 3
      backend/logic/migration/index.js
  41. 0 1
      backend/logic/migration/migrations/migration1.js
  42. 0 1
      backend/logic/migration/migrations/migration10.js
  43. 0 1
      backend/logic/migration/migrations/migration11.js
  44. 0 1
      backend/logic/migration/migrations/migration12.js
  45. 0 1
      backend/logic/migration/migrations/migration13.js
  46. 0 1
      backend/logic/migration/migrations/migration14.js
  47. 0 1
      backend/logic/migration/migrations/migration15.js
  48. 0 1
      backend/logic/migration/migrations/migration16.js
  49. 0 1
      backend/logic/migration/migrations/migration17.js
  50. 0 1
      backend/logic/migration/migrations/migration18.js
  51. 0 1
      backend/logic/migration/migrations/migration19.js
  52. 0 1
      backend/logic/migration/migrations/migration2.js
  53. 0 1
      backend/logic/migration/migrations/migration20.js
  54. 0 1
      backend/logic/migration/migrations/migration21.js
  55. 0 1
      backend/logic/migration/migrations/migration22.js
  56. 0 1
      backend/logic/migration/migrations/migration23.js
  57. 0 1
      backend/logic/migration/migrations/migration24.js
  58. 0 1
      backend/logic/migration/migrations/migration25.js
  59. 0 1
      backend/logic/migration/migrations/migration3.js
  60. 0 1
      backend/logic/migration/migrations/migration4.js
  61. 0 1
      backend/logic/migration/migrations/migration5.js
  62. 0 1
      backend/logic/migration/migrations/migration6.js
  63. 0 1
      backend/logic/migration/migrations/migration7.js
  64. 0 1
      backend/logic/migration/migrations/migration8.js
  65. 0 1
      backend/logic/migration/migrations/migration9.js
  66. 0 4
      backend/logic/musicbrainz.js
  67. 1 6
      backend/logic/notifications.js
  68. 11 29
      backend/logic/playlists.js
  69. 0 6
      backend/logic/punishments.js
  70. 0 16
      backend/logic/songs.js
  71. 0 14
      backend/logic/soundcloud.js
  72. 0 24
      backend/logic/spotify.js
  73. 0 26
      backend/logic/stations.js
  74. 0 10
      backend/logic/tasks.js
  75. 0 12
      backend/logic/utils.js
  76. 0 9
      backend/logic/wikidata.js
  77. 7 22
      backend/logic/ws.js
  78. 108 35
      backend/logic/youtube.js
  79. 382 219
      backend/package-lock.json
  80. 27 24
      backend/package.json
  81. 3 0
      docker-compose.yml
  82. 2 0
      frontend/Dockerfile
  83. 282 400
      frontend/package-lock.json
  84. 35 36
      frontend/package.json
  85. 22 7
      frontend/src/App.vue
  86. 4 4
      frontend/src/components/AdvancedTable.vue
  87. 1 1
      frontend/src/components/FloatingBox.vue
  88. 3 3
      frontend/src/components/LongJobs.spec.ts
  89. 1 1
      frontend/src/components/MainFooter.vue
  90. 9 5
      frontend/src/components/PlaylistTabBase.vue
  91. 7 11
      frontend/src/components/Queue.vue
  92. 4 3
      frontend/src/components/SoundcloudPlayer.vue
  93. 1 1
      frontend/src/components/SoundcloudTrackInfo.vue
  94. 6 5
      frontend/src/components/YoutubePlayer.vue
  95. 8 8
      frontend/src/components/__snapshots__/Modal.spec.ts.snap
  96. 2 2
      frontend/src/components/modals/ConvertSpotifySongs.vue
  97. 69 11
      frontend/src/components/modals/EditPlaylist/Tabs/Settings.vue
  98. 12 23
      frontend/src/components/modals/EditPlaylist/index.vue
  99. 4 3
      frontend/src/components/modals/EditSong/index.vue
  100. 6 5
      frontend/src/components/modals/ManageStation/index.vue

+ 1 - 0
.env.example

@@ -30,6 +30,7 @@ BACKUP_LOCATION=
 BACKUP_NAME=
 
 MUSARE_SITENAME=Musare
+MUSARE_PRIMARY_COLOR="#03a9f4"
 
 MUSARE_DEBUG_VERSION=true
 MUSARE_DEBUG_GIT_REMOTE=false

+ 3 - 2
.wiki/Configuration.md

@@ -50,6 +50,7 @@ machine, even though the application within the container is listening on `21017
 | `BACKUP_LOCATION` | Directory to store musare.sh backups. Defaults to `/backups` in script location. |
 | `BACKUP_NAME` | Name of musare.sh backup files. Defaults to `musare-$(date +"%Y-%m-%d-%s").dump`. |
 | `MUSARE_SITENAME` | Should be the name of the site. [^1] |
+| `MUSARE_PRIMARY_COLOR` | Primary color of the application, in hex format. [^1] |
 | `MUSARE_DEBUG_VERSION` | Log/expose the current package.json version. [^1] |
 | `MUSARE_DEBUG_GIT_REMOTE` | Log/expose the current Git repository's remote. [^1] |
 | `MUSARE_DEBUG_GIT_REMOTE_URL` | Log/expose the current Git repository's remote URL. [^1] |
@@ -134,16 +135,16 @@ For more information on configuration files please refer to the
 | `mongo.port` | MongoDB port. |
 | `mongo.database` | MongoDB database name. |
 | `blacklistedCommunityStationNames` | Array of blacklisted community station names. |
-| `featuredPlaylists` | Array of featured playlist id's. Playlist privacy must be public. |
 | `messages.accountRemoval` | Message to display to users when they request their account to be removed. |
 | `siteSettings.christmas` | Whether to enable christmas theme. |
 | `footerLinks` | Add custom links to footer by specifying `"title": "url"`, e.g. `"GitHub": "https://github.com/Musare/Musare"`. You can disable about, team and news links (but not the pages themselves) by setting them to false, e.g. `"about": false`. |
 | `shortcutOverrides` | Overwrite keyboard shortcuts, for example `"editSong.useAllDiscogs": { "keyCode": 68, "ctrl": true, "alt": true, "shift": false, "preventDefault": true }`. |
+| `primaryColor` | Primary color of the application, in hex format. |
 | `registrationDisabled` | If set to `true`, users can't register accounts. |
 | `sendDataRequestEmails` | If `true` all admin users will be sent an email if a data request is received. Requires mail to be enabled and configured. |
 | `skipConfigVersionCheck` | Skips checking if the config version is outdated or not. Should almost always be set to false. |
 | `skipDbDocumentsVersionCheck` | Skips checking if there are any DB documents outdated or not. Should almost always be set to false. |
-| `debug.stationIssue` | If set to `true` it will enable the `/debug_station` API endpoint on the backend, which provides information useful to debugging stations not skipping, as well as capture all jobs specified in `debug.captureJobs`.
+| `debug.stationIssue` | If set to `true` it will enable the `/debug_station` API endpoint on the backend, which provides information useful to debugging stations not skipping, as well as capture all jobs specified in `debug.captureJobs`. |
 | `debug.traceUnhandledPromises` | Enables the trace-unhandled package, which provides detailed information when a promise is unhandled. |
 | `debug.captureJobs` | Array of jobs to capture for `debug.stationIssue`. |
 | `debug.git.remote` | Log/expose the current Git repository's remote. |

+ 37 - 0
CHANGELOG.md

@@ -1,5 +1,42 @@
 # Changelog
 
+## [v3.11.0] - 2024-03-02
+
+This release includes all changes from v3.11.0-rc1, in addition to the following.
+Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).
+
+### Fixed
+
+- fix: Regression where certain YouTube video URL's could no longer be parsed
+
+### Changed
+
+- refactor: Further improve station system time difference calculation
+
+## [v3.11.0-rc1] - 2024-02-24
+
+Upgrade instructions can be found at [.wiki/Upgrading](.wiki/Upgrading.md).
+
+### Added
+
+- feat: Added primary color configuration
+- feat: Added featured playlist toggle within Edit Playlist modal
+- feat: Added station paused overlay message to player
+
+### Changed
+
+- refactor: Improved support for importing YouTube mixes and non mixes
+- refactor: Improved queue and playlist client-side reordering
+- refactor: Improved station-system time difference handling
+
+### Fixed
+
+- fix: Station player plays 0.1s of video when paused on socket reconnection
+- fix: Station autoplay warning message not displayed
+- fix: Station autoplay warning increasing volume on click
+- fix: Unable to add YouTube video by URL with other query parameters
+- fix: Keyboard shortcut floating box styling
+
 ## [v3.10.0] - 2023-05-21
 
 This release includes all changes from v3.10.0-rc1, v3.10.0-rc2 and v3.10.0-rc3,

+ 2 - 2
backend/Dockerfile

@@ -5,7 +5,7 @@ WORKDIR /opt/app
 
 COPY backend/package.json backend/package-lock.json /opt/app/
 
-RUN npm install --silent
+RUN npm install
 
 FROM node:18 AS musare_backend
 
@@ -20,7 +20,7 @@ COPY types /opt/types
 COPY backend /opt/app
 COPY --from=backend_node_modules /opt/app/node_modules node_modules
 
-ENTRYPOINT bash -c '([[ "${CONTAINER_MODE}" == "development" ]] && npm install --silent); npm run docker:dev'
+ENTRYPOINT bash -c '([[ "${CONTAINER_MODE}" == "development" ]] && npm install); npm run docker:dev'
 
 EXPOSE 8080/tcp
 EXPOSE 8080/udp

+ 0 - 1
backend/classes/Timer.class.js

@@ -57,7 +57,6 @@ export default class Timer {
 
 	/**
 	 * Gets the amount of time the timer has been paused
-	 *
 	 * @returns {Date} - the amount of time the timer has been paused
 	 */
 	getTimePaused() {

+ 1 - 0
backend/config/custom-environment-variables.json

@@ -1,5 +1,6 @@
 {
 	"sitename": "MUSARE_SITENAME",
+	"primaryColor": "MUSARE_PRIMARY_COLOR",
 	"redis": {
 		"password": "REDIS_PASSWORD"
 	},

+ 9 - 5
backend/config/default.json

@@ -32,7 +32,7 @@
 					"limit": 3000000
 				}
 			],
-			"maxPlaylistPages": 20
+			"maxPlaylistPages": 1000
 		},
 		"spotify": {
 			"enabled": false,
@@ -91,8 +91,9 @@
 		"port": 27017,
 		"database": "musare"
 	},
-	"blacklistedCommunityStationNames": ["musare"],
-	"featuredPlaylists": [],
+	"blacklistedCommunityStationNames": [
+		"musare"
+	],
 	"messages": {
 		"accountRemoval": "Your account will be deactivated instantly and your data will shortly be deleted by an admin."
 	},
@@ -103,6 +104,7 @@
 		"news": true,
 		"GitHub": "https://github.com/Musare/Musare"
 	},
+	"primaryColor": "#03a9f4",
 	"shortcutOverrides": {},
 	"registrationDisabled": false,
 	"sendDataRequestEmails": true,
@@ -122,7 +124,9 @@
 		"version": true
 	},
 	"defaultLogging": {
-		"hideType": ["INFO"],
+		"hideType": [
+			"INFO"
+		],
 		"blacklistedTerms": []
 	},
 	"customLoggingPerModule": {
@@ -151,4 +155,4 @@
 		"soundcloud": false,
 		"spotify": false
 	}
-}
+}

+ 0 - 26
backend/core.js

@@ -51,7 +51,6 @@ class Queue {
 
 	/**
 	 * Returns the amount of jobs in the queue.
-	 *
 	 * @returns {number} - amount of jobs in queue
 	 */
 	lengthQueue() {
@@ -60,7 +59,6 @@ class Queue {
 
 	/**
 	 * Returns the amount of running jobs.
-	 *
 	 * @returns {number} - amount of running jobs
 	 */
 	lengthRunning() {
@@ -69,7 +67,6 @@ class Queue {
 
 	/**
 	 * Returns the amount of running jobs.
-	 *
 	 * @returns {number} - amount of running jobs
 	 */
 	lengthPaused() {
@@ -78,7 +75,6 @@ class Queue {
 
 	/**
 	 * Adds a job to the queue, with a given priority.
-	 *
 	 * @param {object} job - the job that is to be added
 	 * @param {object} options - custom options e.g. isQuiet. Optional.
 	 * @param {number} priority - the priority of the to be added job
@@ -92,7 +88,6 @@ class Queue {
 
 	/**
 	 * Removes a job currently running from the queue.
-	 *
 	 * @param {object} job - the job to be removed
 	 */
 	removeRunningJob(job) {
@@ -101,7 +96,6 @@ class Queue {
 
 	/**
 	 * Pauses a job currently running from the queue.
-	 *
 	 * @param {object} job - the job to be pauses
 	 */
 	pauseRunningJob(job) {
@@ -118,7 +112,6 @@ class Queue {
 
 	/**
 	 * Resumes a job currently paused, adding the job back to the front of the queue
-	 *
 	 * @param {object} job - the job to be pauses
 	 */
 	resumeRunningJob(job) {
@@ -157,7 +150,6 @@ class Queue {
 
 	/**
 	 * Handles a task, calling the handleTaskFunction provided in the constructor
-	 *
 	 * @param {object} task - the task to be handled
 	 */
 	_handleTask(task) {
@@ -198,7 +190,6 @@ class Job {
 
 	/**
 	 * Adds a child job to this job
-	 *
 	 * @param {object} childJob - the child job
 	 */
 	addChildJob(childJob) {
@@ -207,7 +198,6 @@ class Job {
 
 	/**
 	 * Sets the job status
-	 *
 	 * @param {string} status - the new status
 	 */
 	setStatus(status) {
@@ -216,7 +206,6 @@ class Job {
 
 	/**
 	 * Sets the task for a job
-	 *
 	 * @param {string} task - the job task
 	 */
 	setTask(task) {
@@ -225,7 +214,6 @@ class Job {
 
 	/**
 	 * Returns the UUID of the job, allowing you to compare jobs with toString
-	 *
 	 * @returns {string} - the job's UUID/uniqueId
 	 */
 	toString() {
@@ -234,7 +222,6 @@ class Job {
 
 	/**
 	 * Sets the response that will be provided to the onFinish DeferredPromise resolve/reject function, as soon as the job is done if it has no parent, or when the parent job is resumed
-	 *
 	 * @param {object} response - the response
 	 */
 	setResponse(response) {
@@ -243,7 +230,6 @@ class Job {
 
 	/**
 	 * Sets the response type that is paired with the response. If it is RESOLVE/REJECT, then it will resolve/reject with the response. If it is RESOLVED/REJECTED, then it has already resolved/rejected with the response.
-	 *
 	 * @param {string} responseType - the response type, so RESOLVE/REJECT/RESOLVED/REJECTED
 	 */
 	setResponseType(responseType) {
@@ -259,7 +245,6 @@ class Job {
 
 	/**
 	 * Logs to the module of the job
-	 *
 	 * @param  {any} args - Anything to be added to the log e.g. log type, log message
 	 */
 	log(...args) {
@@ -284,7 +269,6 @@ class Job {
 
 	/**
 	 * Update and emit progress of job
-	 *
 	 * @param {data} data - Data to publish upon progress
 	 * @param {boolean} notALongJob - Whether job is not a long job
 	 */
@@ -331,7 +315,6 @@ class MovingAverageCalculator {
 
 	/**
 	 * Updates the mean average
-	 *
 	 * @param {number} newValue - the new time it took to complete a job
 	 */
 	update(newValue) {
@@ -342,7 +325,6 @@ class MovingAverageCalculator {
 
 	/**
 	 * Returns the mean average
-	 *
 	 * @returns {number} - returns the mean average
 	 */
 	get mean() {
@@ -386,7 +368,6 @@ export default class CoreClass {
 
 	/**
 	 * Sets the status of a module
-	 *
 	 * @param {string} status - the new status of a module
 	 */
 	setStatus(status) {
@@ -398,7 +379,6 @@ export default class CoreClass {
 
 	/**
 	 * Returns the status of a module
-	 *
 	 * @returns {string} - the status of a module
 	 */
 	getStatus() {
@@ -407,7 +387,6 @@ export default class CoreClass {
 
 	/**
 	 * Changes the current stage of a module
-	 *
 	 * @param {string} stage - the new stage of a module
 	 */
 	setStage(stage) {
@@ -416,7 +395,6 @@ export default class CoreClass {
 
 	/**
 	 * Returns the current stage of a module
-	 *
 	 * @returns {string} - the current stage of a module
 	 */
 	getStage() {
@@ -443,7 +421,6 @@ export default class CoreClass {
 
 	/**
 	 * Creates a new log message
-	 *
 	 * @param {...any} args - anything to be included in the log message, the first argument is the type of log
 	 */
 	log(...args) {
@@ -502,7 +479,6 @@ export default class CoreClass {
 
 	/**
 	 * Runs a job
-	 *
 	 * @param {string} name - the name of the job e.g. GET_PLAYLIST
 	 * @param {object} payload - any expected payload for the job itself
 	 * @param {object} parentJob - the parent job, if any
@@ -590,7 +566,6 @@ export default class CoreClass {
 
 	/**
 	 * UNKNOWN
-	 *
 	 * @param {object} moduleManager - UNKNOWN
 	 */
 	setModuleManager(moduleManager) {
@@ -599,7 +574,6 @@ export default class CoreClass {
 
 	/**
 	 * Actually runs the job? UNKNOWN
-	 *
 	 * @param {object} job - object containing details of the job
 	 * @param {string} job.name - the name of the job e.g. GET_PLAYLIST
 	 * @param {string} job.payload - any expected payload for the job itself

+ 0 - 9
backend/index.js

@@ -84,7 +84,6 @@ class JobManager {
 
 	/**
 	 * Adds a job to the list of jobs
-	 *
 	 * @param {object} job - the job object
 	 */
 	addJob(job) {
@@ -94,7 +93,6 @@ class JobManager {
 
 	/**
 	 * Removes a job from the list of running jobs (after it's completed)
-	 *
 	 * @param {object} job - the job object
 	 */
 	removeJob(job) {
@@ -104,7 +102,6 @@ class JobManager {
 
 	/**
 	 * Returns detail about a job via a identifier
-	 *
 	 * @param {string} uuid - the job identifier
 	 * @returns {object} - the job object
 	 */
@@ -137,7 +134,6 @@ class ModuleManager {
 
 	/**
 	 * Adds a new module to the backend server/module manager
-	 *
 	 * @param {string} moduleName - the name of the module (also needs to be the same as the filename of a module located in the logic folder or "logic/moduleName/index.js")
 	 */
 	async addModule(moduleName) {
@@ -175,7 +171,6 @@ class ModuleManager {
 
 	/**
 	 * Called when a module is initialised
-	 *
 	 * @param {object} module - the module object/class
 	 */
 	onInitialize(module) {
@@ -195,7 +190,6 @@ class ModuleManager {
 
 	/**
 	 * Called when a module fails to initialise
-	 *
 	 * @param {object} module - the module object/class
 	 */
 	onFail(module) {
@@ -214,7 +208,6 @@ class ModuleManager {
 
 	/**
 	 * Creates a new log message
-	 *
 	 * @param {...any} args - anything to be included in the log message, the first argument is the type of log
 	 */
 	log(...args) {
@@ -283,7 +276,6 @@ moduleManager.initialize();
 
 /**
  * Prints a job
- *
  * @param {object} job - the job
  * @param {number} layer - the layer
  */
@@ -299,7 +291,6 @@ function printJob(job, layer) {
 
 /**
  * Prints a task
- *
  * @param {object} task - the task
  * @param {number} layer - the layer
  */

+ 0 - 4
backend/logic/actions/activities.js

@@ -33,7 +33,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Returns how many activities there are for a user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the id of the user in question
 	 * @param {Function} cb - callback
@@ -71,7 +70,6 @@ export default {
 
 	/**
 	 * Gets a set of activities
-	 *
 	 * @param {object} session - user session
 	 * @param {string} userId - the user whose activities we are looking for
 	 * @param {number} set - the set number to return
@@ -127,7 +125,6 @@ export default {
 
 	/**
 	 * Hides an activity for a user
-	 *
 	 * @param session
 	 * @param {string} activityId - the activity which should be hidden
 	 * @param cb
@@ -165,7 +162,6 @@ export default {
 
 	/**
 	 * Removes all activities logged for a logged-in user
-	 *
 	 * @param session
 	 * @param cb
 	 */

+ 0 - 11
backend/logic/actions/apis.js

@@ -16,7 +16,6 @@ const SpotifyModule = moduleManager.modules.spotify;
 export default {
 	/**
 	 * Fetches a list of songs from Youtube's API
-	 *
 	 * @param {object} session - user session
 	 * @param {string} query - the query we'll pass to youtubes api
 	 * @param {Function} cb - callback
@@ -37,7 +36,6 @@ export default {
 
 	/**
 	 * Fetches a specific page of search results from Youtube's API
-	 *
 	 * @param {object} session - user session
 	 * @param {string} query - the query we'll pass to youtubes api
 	 * @param {string} pageToken - identifies a specific page in the result set that should be retrieved
@@ -67,7 +65,6 @@ export default {
 
 	/**
 	 * Gets Discogs data
-	 *
 	 * @param session
 	 * @param query - the query
 	 * @param {Function} cb
@@ -120,7 +117,6 @@ export default {
 
 	/**
 	 * Gets alternative media sources for list of Spotify tracks (media sources)
-	 *
 	 * @param session
 	 * @param trackId - the trackId
 	 * @param {Function} cb
@@ -185,7 +181,6 @@ export default {
 
 	/**
 	 * Gets alternative album sources (such as YouTube playlists) for a list of Spotify album ids
-	 *
 	 * @param session
 	 * @param trackId - the trackId
 	 * @param {Function} cb
@@ -249,7 +244,6 @@ export default {
 
 	/**
 	 * Gets a list of alternative artist sources (such as YouTube channels) for a list of Spotify artist ids
-	 *
 	 * @param session
 	 * @param trackId - the trackId
 	 * @param {Function} cb
@@ -316,7 +310,6 @@ export default {
 
 	/**
 	 * Joins a room
-	 *
 	 * @param {object} session - user session
 	 * @param {string} room - the room to join
 	 * @param {Function} cb - callback
@@ -363,7 +356,6 @@ export default {
 
 	/**
 	 * Leaves a room
-	 *
 	 * @param {object} session - user session
 	 * @param {string} room - the room to leave
 	 * @param {Function} cb - callback
@@ -393,7 +385,6 @@ export default {
 
 	/**
 	 * Joins an admin room
-	 *
 	 * @param {object} session - user session
 	 * @param {string} page - the admin room to join
 	 * @param {Function} cb - callback
@@ -434,7 +425,6 @@ export default {
 
 	/**
 	 * Leaves all rooms
-	 *
 	 * @param {object} session - user session
 	 * @param {Function} cb - callback
 	 */
@@ -446,7 +436,6 @@ export default {
 
 	/**
 	 * Returns current date
-	 *
 	 * @param {object} session - user session
 	 * @param {Function} cb - callback
 	 */

+ 0 - 2
backend/logic/actions/dataRequests.js

@@ -29,7 +29,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Gets data requests, used in the admin users page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -88,7 +87,6 @@ export default {
 
 	/**
 	 * Resolves a data request
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} dataRequestId - the id of the data request to resolve
 	 * @param {boolean} resolved - whether to set to resolved to true or false

+ 0 - 10
backend/logic/actions/media.js

@@ -125,7 +125,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Recalculates all ratings
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
@@ -188,7 +187,6 @@ export default {
 
 	/**
 	 * Like
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -316,7 +314,6 @@ export default {
 
 	/**
 	 * Dislike
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -444,7 +441,6 @@ export default {
 
 	/**
 	 * Undislike
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -575,7 +571,6 @@ export default {
 
 	/**
 	 * Unlike
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -706,7 +701,6 @@ export default {
 
 	/**
 	 * Get ratings
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -754,7 +748,6 @@ export default {
 
 	/**
 	 * Gets user's own ratings
-	 *
 	 * @param session
 	 * @param mediaSource - the media source
 	 * @param cb
@@ -839,7 +832,6 @@ export default {
 
 	/**
 	 * Gets importJobs, used in the admin import page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -898,7 +890,6 @@ export default {
 
 	/**
 	 * Remove import jobs
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	removeImportJobs: useHasPermission("media.removeImportJobs", function removeImportJobs(session, jobIds, cb) {
@@ -917,7 +908,6 @@ export default {
 
 	/**
 	 * Gets an array of media from media sources
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	getMediaFromMediaSources: isLoginRequired(function getMediaFromMediaSources(session, mediaSources, cb) {

+ 0 - 7
backend/logic/actions/news.js

@@ -59,7 +59,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Gets news items, used in the admin news page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -164,7 +163,6 @@ export default {
 
 	/**
 	 * Gets all news items that are published
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -192,7 +190,6 @@ export default {
 
 	/**
 	 * Gets a news item by id
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} newsId - the news item id
 	 * @param {Function} cb - gets called with the result
@@ -221,7 +218,6 @@ export default {
 	},
 	/**
 	 * Creates a news item
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} data - the object of the news data
 	 * @param {Function} cb - gets called with the result
@@ -257,7 +253,6 @@ export default {
 
 	/**
 	 * Gets the latest news item
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {boolean} newUser - whether the user requesting the newest news is a new user
 	 * @param {Function} cb - gets called with the result
@@ -283,7 +278,6 @@ export default {
 
 	/**
 	 * Removes a news item
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} newsId - the id of the news item we want to remove
 	 * @param {Function} cb - gets called with the result
@@ -327,7 +321,6 @@ export default {
 
 	/**
 	 * Updates a news item
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} newsId - the id of the news item
 	 * @param {object} item - the news item object

File diff suppressed because it is too large
+ 469 - 508
backend/logic/actions/playlists.js


+ 0 - 5
backend/logic/actions/punishments.js

@@ -30,7 +30,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Gets punishments, used in the admin punishments page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -200,7 +199,6 @@ export default {
 
 	/**
 	 * Gets all punishments for a user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the id of the user
 	 * @param {Function} cb - gets called with the result
@@ -231,7 +229,6 @@ export default {
 
 	/**
 	 * Returns a punishment by id
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} punishmentId - the punishment id
 	 * @param {Function} cb - gets called with the result
@@ -256,7 +253,6 @@ export default {
 
 	/**
 	 * Bans an IP address
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} value - the ip address that is going to be banned
 	 * @param {string} reason - the reason for the ban
@@ -357,7 +353,6 @@ export default {
 
 	/**
 	 * Deactivates a punishment
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} punishmentId - the MongoDB id of the punishment
 	 * @param {Function} cb - gets called with the result

+ 0 - 8
backend/logic/actions/reports.js

@@ -90,7 +90,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Gets reports, used in the admin reports page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -195,7 +194,6 @@ export default {
 
 	/**
 	 * Gets a specific report
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report to return
 	 * @param {Function} cb - gets called with the result
@@ -244,7 +242,6 @@ export default {
 
 	/**
 	 * Gets all reports for a songId
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the id of the song to index reports for
 	 * @param {Function} cb - gets called with the result
@@ -306,7 +303,6 @@ export default {
 
 	/**
 	 * Gets all a users reports for a specific songId
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the id of the song
 	 * @param {Function} cb - gets called with the result
@@ -380,7 +376,6 @@ export default {
 
 	/**
 	 * Resolves a report as a whole
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
 	 * @param {boolean} resolved - whether to set to resolved to true or false
@@ -445,7 +440,6 @@ export default {
 
 	/**
 	 * Resolves/Unresolves an issue within a report
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
 	 * @param {string} issueId - the id of the issue within the report
@@ -509,7 +503,6 @@ export default {
 
 	/**
 	 * Creates a new report
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} report - the object of the report data
 	 * @param {string} report.mediaSource - the media source of the song that is being reported
@@ -595,7 +588,6 @@ export default {
 
 	/**
 	 * Removes a report
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} reportId - the id of the report item we want to remove
 	 * @param {Function} cb - gets called with the result

+ 0 - 20
backend/logic/actions/songs.js

@@ -44,7 +44,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Returns the length of the songs list
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
@@ -70,7 +69,6 @@ export default {
 
 	/**
 	 * Gets songs, used in the admin songs page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -209,7 +207,6 @@ export default {
 
 	/**
 	 * Updates all songs
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
@@ -265,7 +262,6 @@ export default {
 
 	/**
 	 * Gets a song from the Musare song id
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the song id
 	 * @param {Function} cb
@@ -294,7 +290,6 @@ export default {
 	/**
 	 * Gets multiple songs from the Musare song ids
 	 * At this time only used in bulk EditSong
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} mediaSources - the song media sources
 	 * @param {Function} cb
@@ -341,7 +336,6 @@ export default {
 
 	/**
 	 * Creates a song
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} newSong - the song object
 	 * @param {Function} cb
@@ -377,7 +371,6 @@ export default {
 
 	/**
 	 * Updates a song
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} songId - the song id
 	 * @param {object} song - the updated song object
@@ -455,7 +448,6 @@ export default {
 
 	/**
 	 * Removes a song
-	 *
 	 * @param session
 	 * @param songId - the song id
 	 * @param cb
@@ -728,7 +720,6 @@ export default {
 
 	/**
 	 * Removes many songs
-	 *
 	 * @param session
 	 * @param songIds - array of song ids
 	 * @param cb
@@ -828,7 +819,6 @@ export default {
 
 	/**
 	 * Searches through official songs
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} query - the query
 	 * @param {string} page - the page
@@ -871,7 +861,6 @@ export default {
 
 	/**
 	 * Verifies a song
-	 *
 	 * @param session
 	 * @param songId - the song id
 	 * @param cb
@@ -930,7 +919,6 @@ export default {
 
 	/**
 	 * Verify many songs
-	 *
 	 * @param session
 	 * @param songIds - array of song ids
 	 * @param cb
@@ -1026,7 +1014,6 @@ export default {
 
 	/**
 	 * Un-verifies a song
-	 *
 	 * @param session
 	 * @param songId - the song id
 	 * @param cb
@@ -1091,7 +1078,6 @@ export default {
 
 	/**
 	 * Unverify many songs
-	 *
 	 * @param session
 	 * @param songIds - array of song ids
 	 * @param cb
@@ -1195,7 +1181,6 @@ export default {
 
 	/**
 	 * Gets a list of all genres
-	 *
 	 * @param session
 	 * @param cb
 	 */
@@ -1231,7 +1216,6 @@ export default {
 
 	/**
 	 * Bulk update genres for selected songs
-	 *
 	 * @param session
 	 * @param method Whether to add, remove or replace genres
 	 * @param genres Array of genres to apply
@@ -1328,7 +1312,6 @@ export default {
 
 	/**
 	 * Gets a list of all artists
-	 *
 	 * @param session
 	 * @param cb
 	 */
@@ -1364,7 +1347,6 @@ export default {
 
 	/**
 	 * Bulk update artists for selected songs
-	 *
 	 * @param session
 	 * @param method Whether to add, remove or replace artists
 	 * @param artists Array of artists to apply
@@ -1461,7 +1443,6 @@ export default {
 
 	/**
 	 * Gets a list of all tags
-	 *
 	 * @param session
 	 * @param cb
 	 */
@@ -1497,7 +1478,6 @@ export default {
 
 	/**
 	 * Bulk update tags for selected songs
-	 *
 	 * @param session
 	 * @param method Whether to add, remove or replace tags
 	 * @param tags Array of tags to apply

+ 0 - 5
backend/logic/actions/soundcloud.js

@@ -14,7 +14,6 @@ const CacheModule = moduleManager.modules.cache;
 export default {
 	/**
 	 * Fetches new SoundCloud API key
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	fetchNewApiKey: useHasPermission("soundcloud.fetchNewApiKey", async function fetchNewApiKey(session, cb) {
@@ -60,7 +59,6 @@ export default {
 
 	/**
 	 * Tests SoundCloud API key
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	testApiKey: useHasPermission("soundcloud.testApiKey", async function testApiKey(session, cb) {
@@ -110,7 +108,6 @@ export default {
 
 	/**
 	 * Get a Soundcloud artist from ID
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	getArtist: useHasPermission("soundcloud.getArtist", function getArtist(session, userPermalink, cb) {
@@ -137,7 +134,6 @@ export default {
 
 	/**
 	 * Gets videos, used in the admin youtube page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -243,7 +239,6 @@ export default {
 
 	/**
 	 * Get a SoundCloud track
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param identifier - the identifier of the SoundCloud track
 	 * @param createMissing - whether to create/fetch the SoundCloud track if it's missing

+ 0 - 3
backend/logic/actions/spotify.js

@@ -9,7 +9,6 @@ const SpotifyModule = moduleManager.modules.spotify;
 export default {
 	/**
 	 * Fetches tracks from media sources
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} mediaSources - the media sources to get tracks for
 	 * @returns {{status: string, data: object}}
@@ -40,7 +39,6 @@ export default {
 
 	/**
 	 * Fetches albums from ids
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} albumIds - the ids of the Spotify albums to get
 	 * @returns {{status: string, data: object}}
@@ -60,7 +58,6 @@ export default {
 
 	/**
 	 * Fetches artists from ids
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} artistIds - the ids of the Spotify artists to get
 	 * @returns {{status: string, data: object}}

+ 9 - 44
backend/logic/actions/stations.js

@@ -195,18 +195,18 @@ CacheModule.runJob("SUB", {
 });
 
 CacheModule.runJob("SUB", {
-	channel: "station.repositionSongInQueue",
+	channel: "station.changeQueueOrder",
 	cb: res => {
 		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `station.${res.stationId}`,
-			args: ["event:station.queue.song.repositioned", { data: { song: res.song } }]
+			args: ["event:station.queue.order.changed", { data: { queueOrder: res.queueOrder } }]
 		});
 
 		WSModule.runJob("EMIT_TO_ROOM", {
 			room: `manage-station.${res.stationId}`,
 			args: [
-				"event:manageStation.queue.song.repositioned",
-				{ data: { stationId: res.stationId, song: res.song } }
+				"event:manageStation.queue.order.changed",
+				{ data: { stationId: res.stationId, queueOrder: res.queueOrder } }
 			]
 		});
 	}
@@ -355,7 +355,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Get a list of all the stations
-	 *
 	 * @param {object} session - user session
 	 * @param {boolean} adminFilter - whether to filter out stations admins do not own
 	 * @param {Function} cb - callback
@@ -459,7 +458,6 @@ export default {
 
 	/**
 	 * Gets stations, used in the admin stations page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -562,7 +560,6 @@ export default {
 
 	/**
 	 * Obtains basic metadata of a station in order to format an activity
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -606,7 +603,6 @@ export default {
 
 	/**
 	 * Verifies that a station exists from its name
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationName - the station name
 	 * @param {Function} cb - callback
@@ -651,7 +647,6 @@ export default {
 
 	/**
 	 * Verifies that a station exists from its id
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -696,7 +691,6 @@ export default {
 
 	/**
 	 * Gets the official playlist for a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -779,7 +773,6 @@ export default {
 
 	/**
 	 * Joins the station by its name
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationIdentifier - the station name or station id
 	 * @param {Function} cb - callback
@@ -908,7 +901,6 @@ export default {
 
 	/**
 	 * Gets a station by id
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -998,7 +990,6 @@ export default {
 
 	/**
 	 * Gets station history
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -1209,7 +1200,6 @@ export default {
 
 	/**
 	 * Toggle votes to skip a station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param stationId - the station id
 	 *  @param {Function} cb - gets called with the result
@@ -1293,7 +1283,6 @@ export default {
 
 	/**
 	 * Force skips a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -1338,7 +1327,6 @@ export default {
 
 	/**
 	 * Leaves the user's current station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - id of station to leave
 	 * @param {Function} cb - callback
@@ -1382,7 +1370,6 @@ export default {
 
 	/**
 	 * Updates a station's settings
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {object} newStation - updated station object
@@ -1497,7 +1484,6 @@ export default {
 
 	/**
 	 * Pauses a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -1566,7 +1552,6 @@ export default {
 
 	/**
 	 * Resumes a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -1642,7 +1627,6 @@ export default {
 
 	/**
 	 * Removes a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -1726,7 +1710,6 @@ export default {
 
 	/**
 	 * Create a station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param data - the station data
 	 *  @param {Function} cb - gets called with the result
@@ -1910,7 +1893,6 @@ export default {
 
 	/**
 	 * Adds song to station queue
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param stationId - the station id
 	 * @param mediaSource - the song id
@@ -2000,7 +1982,6 @@ export default {
 
 	/**
 	 * Removes song from station queue
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {string} mediaSource - the media source
@@ -2049,7 +2030,6 @@ export default {
 
 	/**
 	 * Gets the queue from a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - callback
@@ -2103,7 +2083,6 @@ export default {
 
 	/**
 	 * Reposition a song in station queue
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {object} song - contains details about the song that is to be repositioned
@@ -2153,7 +2132,7 @@ export default {
 						.catch(next);
 				}
 			],
-			async err => {
+			async (err, station) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
@@ -2170,14 +2149,12 @@ export default {
 					`Repositioned song ${song.mediaSource} in queue of station "${stationId}" successfully.`
 				);
 
+				const queueOrder = station.queue.map(song => song.mediaSource);
+
 				CacheModule.runJob("PUB", {
-					channel: "station.repositionSongInQueue",
+					channel: "station.changeQueueOrder",
 					value: {
-						song: {
-							mediaSource: song.mediaSource,
-							oldIndex: song.oldIndex,
-							newIndex: song.newIndex
-						},
+						queueOrder,
 						stationId
 					}
 				});
@@ -2192,7 +2169,6 @@ export default {
 
 	/**
 	 * Autofill a playlist in a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {string} playlistId - the playlist id
@@ -2289,7 +2265,6 @@ export default {
 
 	/**
 	 * Remove autofilled playlist from a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {string} playlistId - the playlist id
@@ -2362,7 +2337,6 @@ export default {
 
 	/**
 	 * Blacklist a playlist in a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {string} playlistId - the playlist id
@@ -2435,7 +2409,6 @@ export default {
 
 	/**
 	 * Remove blacklisted a playlist from a station
-	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {string} playlistId - the playlist id
@@ -2507,7 +2480,6 @@ export default {
 
 	/**
 	 * Favorites a station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station to favorite
 	 * @param {Function} cb - gets called with the result
@@ -2581,7 +2553,6 @@ export default {
 
 	/**
 	 * Unfavorites a station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station to unfavorite
 	 * @param {Function} cb - gets called with the result
@@ -2642,7 +2613,6 @@ export default {
 
 	/**
 	 * Clears every station queue
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2697,7 +2667,6 @@ export default {
 
 	/**
 	 * Reset a station queue
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station id
 	 * @param {Function} cb - gets called with the result
@@ -2731,7 +2700,6 @@ export default {
 
 	/**
 	 * Gets skip votes for a station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station id
 	 * @param {string} songId - the song id to get skipvotes for
@@ -2783,7 +2751,6 @@ export default {
 
 	/**
 	 * Add DJ to station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station id
 	 * @param {string} userId - the dj user id
@@ -2818,7 +2785,6 @@ export default {
 
 	/**
 	 * Remove DJ from station
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} stationId - the station id
 	 * @param {string} userId - the dj user id
@@ -2853,7 +2819,6 @@ export default {
 
 	/**
 	 * Sets the state of the current user session
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} newStationState - the new state
 	 * @param {Function} cb - gets called with the result

+ 43 - 84
backend/logic/actions/users.js

@@ -247,7 +247,6 @@ CacheModule.runJob("SUB", {
 export default {
 	/**
 	 * Gets users, used in the admin users page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -328,7 +327,6 @@ export default {
 
 	/**
 	 * Removes all data held on a user, including their ability to login
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -556,7 +554,6 @@ export default {
 
 	/**
 	 * Removes all data held on a user, including their ability to login, by userId
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the user id that is going to be banned
 	 * @param {Function} cb - gets called with the result
@@ -782,7 +779,6 @@ export default {
 
 	/**
 	 * Logs user in
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} identifier - the username or email of the user
 	 * @param {string} password - the plaintext of the user
@@ -861,7 +857,6 @@ export default {
 
 	/**
 	 * Registers a new user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} username - the username for the new user
 	 * @param {string} email - the email for the new user
@@ -1086,7 +1081,6 @@ export default {
 
 	/**
 	 * Logs out a user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1137,7 +1131,6 @@ export default {
 
 	/**
 	 * Checks if user's password is correct (e.g. before a sensitive action)
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} password - the password the user entered that we need to validate
 	 * @param {Function} cb - gets called with the result
@@ -1208,7 +1201,6 @@ export default {
 
 	/**
 	 * Checks if user's github access token has expired or not (ie. if their github account is still linked)
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1266,7 +1258,6 @@ export default {
 
 	/**
 	 * Removes all sessions for a user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the id of the user we are trying to delete the sessions of
 	 * @param {Function} cb - gets called with the result
@@ -1352,67 +1343,63 @@ export default {
 
 	/**
 	 * Updates the order of a user's favorite stations
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} favoriteStations - array of station ids (with a specific order)
 	 * @param {Function} cb - gets called with the result
 	 */
-	updateOrderOfFavoriteStations: isLoginRequired(async function updateOrderOfFavoriteStations(
-		session,
-		favoriteStations,
-		cb
-	) {
-		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+	updateOrderOfFavoriteStations: isLoginRequired(
+		async function updateOrderOfFavoriteStations(session, favoriteStations, cb) {
+			const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
-		async.waterfall(
-			[
-				next => {
-					userModel.updateOne(
-						{ _id: session.userId },
-						{ $set: { favoriteStations } },
-						{ runValidators: true },
-						next
-					);
-				}
-			],
-			async err => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+			async.waterfall(
+				[
+					next => {
+						userModel.updateOne(
+							{ _id: session.userId },
+							{ $set: { favoriteStations } },
+							{ runValidators: true },
+							next
+						);
+					}
+				],
+				async err => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+						this.log(
+							"ERROR",
+							"UPDATE_ORDER_OF_USER_FAVORITE_STATIONS",
+							`Couldn't update order of favorite stations for user "${session.userId}" to "${favoriteStations}". "${err}"`
+						);
+
+						return cb({ status: "error", message: err });
+					}
+
+					CacheModule.runJob("PUB", {
+						channel: "user.updateOrderOfFavoriteStations",
+						value: {
+							favoriteStations,
+							userId: session.userId
+						}
+					});
 
 					this.log(
-						"ERROR",
+						"SUCCESS",
 						"UPDATE_ORDER_OF_USER_FAVORITE_STATIONS",
-						`Couldn't update order of favorite stations for user "${session.userId}" to "${favoriteStations}". "${err}"`
+						`Updated order of favorite stations for user "${session.userId}" to "${favoriteStations}".`
 					);
 
-					return cb({ status: "error", message: err });
+					return cb({
+						status: "success",
+						message: "Order of favorite stations successfully updated"
+					});
 				}
-
-				CacheModule.runJob("PUB", {
-					channel: "user.updateOrderOfFavoriteStations",
-					value: {
-						favoriteStations,
-						userId: session.userId
-					}
-				});
-
-				this.log(
-					"SUCCESS",
-					"UPDATE_ORDER_OF_USER_FAVORITE_STATIONS",
-					`Updated order of favorite stations for user "${session.userId}" to "${favoriteStations}".`
-				);
-
-				return cb({
-					status: "success",
-					message: "Order of favorite stations successfully updated"
-				});
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Updates the order of a user's playlists
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} orderOfPlaylists - array of playlist ids (with a specific order)
 	 * @param {Function} cb - gets called with the result
@@ -1468,7 +1455,6 @@ export default {
 
 	/**
 	 * Updates a user's preferences
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} preferences - object containing preferences
 	 * @param {boolean} preferences.nightmode - whether or not the user is using the night mode theme
@@ -1571,7 +1557,6 @@ export default {
 
 	/**
 	 * Retrieves a user's preferences
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1619,7 +1604,6 @@ export default {
 
 	/**
 	 * Gets user object from ObjectId or username (only a few properties)
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} identifier - the ObjectId or username of the user we are trying to find
 	 * @param {Function} cb - gets called with the result
@@ -1669,7 +1653,6 @@ export default {
 
 	/**
 	 * Gets a list of long jobs, including onprogress events when those long jobs have progress
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -1745,7 +1728,6 @@ export default {
 
 	/**
 	 * Gets a specific long job, including onprogress events when that long job has progress
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} jobId - the if id the long job
 	 * @param {Function} cb - gets called with the result
@@ -1819,7 +1801,6 @@ export default {
 
 	/**
 	 * Removes active long job for a user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} jobId - array of playlist ids (with a specific order)
 	 * @param {Function} cb - gets called with the result
@@ -1880,7 +1861,6 @@ export default {
 
 	/**
 	 * Gets a user from a userId
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the userId of the person we are trying to get the username from
 	 * @param {Function} cb - gets called with the result
@@ -1934,7 +1914,6 @@ export default {
 
 	/**
 	 * Gets user info from session
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2002,7 +1981,6 @@ export default {
 
 	/**
 	 * Updates a user's username
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newUsername - the new username
@@ -2090,7 +2068,6 @@ export default {
 
 	/**
 	 * Updates a user's email
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newEmail - the new email
@@ -2198,7 +2175,6 @@ export default {
 
 	/**
 	 * Updates a user's name
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newBio - the new name
@@ -2268,7 +2244,6 @@ export default {
 
 	/**
 	 * Updates a user's location
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newLocation - the new location
@@ -2344,7 +2319,6 @@ export default {
 
 	/**
 	 * Updates a user's bio
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newBio - the new bio
@@ -2408,7 +2382,6 @@ export default {
 
 	/**
 	 * Updates a user's avatar
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newAvatar - the new avatar object
@@ -2476,7 +2449,6 @@ export default {
 
 	/**
 	 * Updates a user's role
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} updatingUserId - the updating user's id
 	 * @param {string} newRole - the new role
@@ -2549,7 +2521,6 @@ export default {
 
 	/**
 	 * Updates a user's password
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} previousPassword - the previous password
 	 * @param {string} newPassword - the new password
@@ -2626,7 +2597,6 @@ export default {
 
 	/**
 	 * Requests a password for a session
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
@@ -2704,7 +2674,6 @@ export default {
 
 	/**
 	 * Verifies a password code
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password code
 	 * @param {Function} cb - gets called with the result
@@ -2748,7 +2717,6 @@ export default {
 
 	/**
 	 * Adds a password to a user with a code
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password code
 	 * @param {string} newPassword - the new password code
@@ -2833,7 +2801,6 @@ export default {
 
 	/**
 	 * Unlinks password from user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2886,7 +2853,6 @@ export default {
 
 	/**
 	 * Unlinks GitHub from user
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -2938,7 +2904,6 @@ export default {
 
 	/**
 	 * Requests a password reset for an email
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} email - the email of the user that requests a password reset
 	 * @param {Function} cb - gets called with the result
@@ -3019,7 +2984,6 @@ export default {
 
 	/**
 	 * Requests a password reset for a a user as an admin
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} email - the email of the user for which the password reset is intended
 	 * @param {Function} cb - gets called with the result
@@ -3097,7 +3061,6 @@ export default {
 
 	/**
 	 * Verifies a reset code
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password reset code
 	 * @param {Function} cb - gets called with the result
@@ -3136,7 +3099,6 @@ export default {
 
 	/**
 	 * Changes a user's password with a reset code
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} code - the password reset code
 	 * @param {string} newPassword - the new password reset code
@@ -3209,7 +3171,6 @@ export default {
 
 	/**
 	 * Resends the verify email email
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} userId - the user id of the person to resend the email to
 	 * @param {Function} cb - gets called with the result
@@ -3262,7 +3223,6 @@ export default {
 
 	/**
 	 * Bans a user by userId
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} value - the user id that is going to be banned
 	 * @param {string} reason - the reason for the ban
@@ -3367,7 +3327,6 @@ export default {
 
 	/**
 	 * Search for a user by username or name
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} query - the query
 	 * @param {string} page - page

+ 0 - 1
backend/logic/actions/utils.js

@@ -113,7 +113,6 @@ export default {
 
 	/**
 	 * Get permissions
-	 *
 	 * @param {object} session - the session object automatically added by socket.io
 	 * @param {string} stationId - optional, the station id
 	 * @param {Function} cb - gets called with the result

+ 0 - 16
backend/logic/actions/youtube.js

@@ -16,7 +16,6 @@ const MediaModule = moduleManager.modules.media;
 export default {
 	/**
 	 * Returns details about the YouTube quota usage
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	getQuotaStatus: useHasPermission("admin.view.youtube", function getQuotaStatus(session, fromDate, cb) {
@@ -34,7 +33,6 @@ export default {
 
 	/**
 	 * Returns YouTube quota chart data
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param timePeriod - either hours or days
 	 * @param startDate - beginning date
@@ -64,7 +62,6 @@ export default {
 
 	/**
 	 * Gets api requests, used in the admin youtube page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -123,7 +120,6 @@ export default {
 
 	/**
 	 * Returns a specific api request
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	getApiRequest: useHasPermission("youtube.getApiRequest", function getApiRequest(session, apiRequestId, cb) {
@@ -152,7 +148,6 @@ export default {
 
 	/**
 	 * Reset stored API requests
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	resetStoredApiRequests: useHasPermission(
@@ -206,7 +201,6 @@ export default {
 
 	/**
 	 * Remove stored API requests
-	 *
 	 * @returns {{status: string, data: object}}
 	 */
 	removeStoredApiRequest: useHasPermission(
@@ -236,7 +230,6 @@ export default {
 
 	/**
 	 * Gets videos, used in the admin youtube page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -380,7 +373,6 @@ export default {
 
 	/**
 	 * Gets channels, used in the admin youtube page by the AdvancedTable component
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param page - the page
 	 * @param pageSize - the size per page
@@ -440,7 +432,6 @@ export default {
 
 	/**
 	 * Get a YouTube video
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} identifier - the identifier of the video to get
 	 * @param {string} createMissing - whether to create the video if it doesn't exist yet
@@ -463,7 +454,6 @@ export default {
 
 	/**
 	 * Get a YouTube channel from ID
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} channelId - the YouTube channel id to get
 	 * @param {Function} cb - gets called with the result
@@ -493,7 +483,6 @@ export default {
 
 	/**
 	 * Remove YouTube videos
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Array} videoIds - the YouTube video ids to remove
 	 * @param {Function} cb - gets called with the result
@@ -539,7 +528,6 @@ export default {
 
 	/**
 	 * Gets missing YouTube video's from all playlists, stations and songs
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 * @returns {{status: string, data: object}}
@@ -584,7 +572,6 @@ export default {
 
 	/**
 	 * Updates YouTube video's from version 1 to version 2, by re-fetching the video's
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 * @returns {{status: string, data: object}}
@@ -629,7 +616,6 @@ export default {
 
 	/**
 	 * Requests a set of YouTube videos
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} url - the url of the the YouTube playlist
 	 * @param {boolean} musicOnly - whether to only get music from the playlist
@@ -663,7 +649,6 @@ export default {
 
 	/**
 	 * Requests a set of YouTube videos as an admin
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} url - the url of the the YouTube playlist
 	 * @param {boolean} musicOnly - whether to only get music from the playlist
@@ -782,7 +767,6 @@ export default {
 
 	/**
 	 * Gets missing YouTube channels
-	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
 	 * @returns {{status: string, data: object}}

+ 0 - 5
backend/logic/activities.js

@@ -19,7 +19,6 @@ class _ActivitiesModule extends CoreClass {
 
 	/**
 	 * Initialises the activities module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -36,7 +35,6 @@ class _ActivitiesModule extends CoreClass {
 
 	/**
 	 * Adds a new activity to the database
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the id of the user who's activity is to be added
 	 * @param {string} payload.type - the type of activity (enum specified in schema)
@@ -178,7 +176,6 @@ class _ActivitiesModule extends CoreClass {
 
 	/**
 	 * Merges activities about adding/removing songs from a playlist within a 5-minute period to prevent spam
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the id of the user to check for duplicates
 	 * @param {object} payload.playlist - object that contains info about the relevant playlist
@@ -285,7 +282,6 @@ class _ActivitiesModule extends CoreClass {
 
 	/**
 	 * Removes any references to a station, playlist or song in activities
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.type - type of reference. enum: ["mediaSource", "stationId", "playlistId", "playlistId"]
 	 * @param {string} payload.stationId - (optional) the id of a station
@@ -402,7 +398,6 @@ class _ActivitiesModule extends CoreClass {
 
 	/**
 	 * Hides any activities of the same type within a 15-minute period to prevent spam
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the id of the user to check for duplicates
 	 * @param {string} payload.type - the type of activity to check for duplicates

+ 0 - 1
backend/logic/api.js

@@ -23,7 +23,6 @@ class _APIModule extends CoreClass {
 
 	/**
 	 * Initialises the api module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {

+ 0 - 3
backend/logic/app.js

@@ -29,7 +29,6 @@ class _AppModule extends CoreClass {
 
 	/**
 	 * Initialises the app module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -520,7 +519,6 @@ class _AppModule extends CoreClass {
 
 	/**
 	 * Returns the express server
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	SERVER() {
@@ -531,7 +529,6 @@ class _AppModule extends CoreClass {
 
 	/**
 	 * Returns the app object
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_APP() {

+ 10 - 29
backend/logic/cache/index.js

@@ -22,7 +22,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Initialises the cache/redis module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -95,7 +94,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Quits redis client
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	QUIT() {
@@ -111,12 +109,11 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Sets a single value
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the key to set
 	 * @param {*} payload.value - the value we want to set
 	 * @param {number} payload.ttl -  ttl of the key in seconds
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	SET(payload) {
@@ -153,12 +150,11 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Sets a single value in a table
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.table - name of the table we want to set a key of (table === redis hash)
 	 * @param {string} payload.key -  name of the key to set
 	 * @param {*} payload.value - the value we want to set
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HSET(payload) {
@@ -179,10 +175,9 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Gets a single value
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key - name of the key to fetch
-	 * @param {boolean} [payload.parseJson=true] - attempt to parse returned data as JSON
+	 * @param {boolean} [payload.parseJson] - attempt to parse returned data as JSON
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET(payload) {
@@ -215,11 +210,10 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Gets a single value from a table
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.table - name of the table to get the value from (table === redis hash)
 	 * @param {string} payload.key - name of the key to fetch
-	 * @param {boolean} [payload.parseJson=true] - attempt to parse returned data as JSON
+	 * @param {boolean} [payload.parseJson] - attempt to parse returned data as JSON
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HGET(payload) {
@@ -254,7 +248,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Deletes a single value from a table
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.table - name of the table to delete the value from (table === redis hash)
 	 * @param {string} payload.key - name of the key to delete
@@ -284,10 +277,9 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Returns all the keys for a table
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.table - name of the table to get the values from (table === redis hash)
-	 * @param {boolean} [payload.parseJson=true] - attempts to parse all values as JSON by default
+	 * @param {boolean} [payload.parseJson] - attempts to parse all values as JSON by default
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	HGETALL(payload) {
@@ -313,7 +305,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Deletes a single value
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key - name of the key to delete
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -338,11 +329,10 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Publish a message to a channel, caches the redis client connection
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.channel - the name of the channel we want to publish a message to
 	 * @param {*} payload.value - the value we want to send
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	PUB(payload) {
@@ -369,10 +359,9 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Subscribe to a channel, caches the redis client connection
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.channel - name of the channel to subscribe to
-	 * @param {boolean} [payload.parseJson=true] - parse the message as JSON
+	 * @param {boolean} [payload.parseJson] - parse the message as JSON
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	SUB(payload) {
@@ -411,7 +400,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Gets a full list from Redis
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key - name of the table to get the value from (table === redis hash)
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -435,11 +423,10 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Adds a value to a list in Redis
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the list
 	 * @param {*} payload.value - the value we want to set
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	RPUSH(payload) {
@@ -459,11 +446,10 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Adds a value to a list in Redis using LPUSH
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the list
 	 * @param {*} payload.value - the value we want to set
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	LPUSH(payload) {
@@ -483,7 +469,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Gets the length of a Redis list
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the list
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -501,7 +486,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Removes an item from a list using RPOP
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the list
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -519,11 +503,10 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Removes a value from a list in Redis
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.key -  name of the list
 	 * @param {*} payload.value - the value we want to remove
-	 * @param {boolean} [payload.stringifyJson=true] - stringify 'value' if it's an Object or Array
+	 * @param {boolean} [payload.stringifyJson] - stringify 'value' if it's an Object or Array
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	LREM(payload) {
@@ -543,7 +526,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Gets a list of keys in Redis with a matching pattern
-	 *
 	 * @param {object} payload - object containing payload
 	 * @param {string} payload.pattern -  pattern to search for
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -561,7 +543,6 @@ class _CacheModule extends CoreClass {
 
 	/**
 	 * Returns a redis schema
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.schemaName - the name of the schema to get
 	 * @returns {Promise} - returns promise (reject, resolve)

+ 0 - 1
backend/logic/cache/schemas/playlist.js

@@ -2,7 +2,6 @@
  * Schema for a playlist stored / cached in redis,
  * gets created when a playlist is in use
  * and therefore is put into the redis cache
- *
  * @param {object} playlist - object containing the playlist
  * @returns {object} - returns same object
  */

+ 0 - 1
backend/logic/cache/schemas/station.js

@@ -2,7 +2,6 @@
  * Schema for a station stored / cached in redis,
  * gets created when a station is in use
  * and therefore is put into the redis cache
- *
  * @param {object} station -  object containing the station
  * @returns {object} - returns same object
  */

+ 0 - 6
backend/logic/db/index.js

@@ -53,7 +53,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Initialises the database module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -333,7 +332,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Checks if all documents have the correct document version
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	CHECK_DOCUMENT_VERSIONS() {
@@ -379,7 +377,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Returns a database model
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {object} payload.modelName - name of the model to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -392,7 +389,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Returns a database schema
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {object} payload.schemaName - name of the schema to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -405,7 +401,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Gets data
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.page - the page
 	 * @param {string} payload.pageSize - the page size
@@ -618,7 +613,6 @@ class _DBModule extends CoreClass {
 
 	/**
 	 * Checks if a password to be stored in the database has a valid length
-	 *
 	 * @param {object} password - the password itself
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */

+ 1 - 0
backend/logic/db/schemas/playlist.js

@@ -26,5 +26,6 @@ export default {
 			replacedAt: { type: Date, required: true }
 		}
 	],
+	featured: { type: Boolean, default: false },
 	documentVersion: { type: Number, default: 7, required: true }
 };

+ 5 - 4
backend/logic/hooks/hasPermission.js

@@ -43,6 +43,7 @@ permissions.moderator = {
 	"playlists.create.admin": true,
 	"playlists.get": true,
 	"playlists.update.displayName": true,
+	"playlists.update.featured": true,
 	"playlists.update.privacy": true,
 	"playlists.songs.add": true,
 	"playlists.songs.remove": true,
@@ -71,7 +72,7 @@ permissions.moderator = {
 				"admin.view.soundcloudTracks": true,
 				"admin.view.soundcloud": true,
 				"soundcloud.getArtist": true
-		  }
+			}
 		: {}),
 	...(config.get("experimental.spotify")
 		? {
@@ -84,7 +85,7 @@ permissions.moderator = {
 				"spotify.getAlternativeMediaSourcesForTracks": true,
 				"admin.view.youtubeChannels": true,
 				"youtube.getChannel": true
-		  }
+			}
 		: {})
 };
 permissions.admin = {
@@ -122,12 +123,12 @@ permissions.admin = {
 		? {
 				"soundcloud.fetchNewApiKey": true,
 				"soundcloud.testApiKey": true
-		  }
+			}
 		: {}),
 	...(config.get("experimental.spotify")
 		? {
 				"youtube.getMissingChannels": true
-		  }
+			}
 		: {})
 };
 

+ 0 - 3
backend/logic/mail/index.js

@@ -16,7 +16,6 @@ class _MailModule extends CoreClass {
 
 	/**
 	 * Initialises the mail module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -43,7 +42,6 @@ class _MailModule extends CoreClass {
 
 	/**
 	 * Sends an email
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.data - information such as to, from in order to send the email
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -76,7 +74,6 @@ class _MailModule extends CoreClass {
 
 	/**
 	 * Returns an email schema
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.schemaName - name of the schema to get
 	 * @returns {Promise} - returns promise (reject, resolve)

+ 2 - 3
backend/logic/mail/schemas/dataRequest.js

@@ -4,7 +4,6 @@ import mail from "../index";
 
 /**
  * Sends an email to all admins that a user has submitted a data request
- *
  * @param {string} to - an array of email addresses of admins
  * @param {string} userId - the id of the user the data request is for
  * @param {string} type - the type of data request e.g. remove
@@ -23,8 +22,8 @@ export default (to, userId, type, cb) => {
 				<br>
 				This request can be viewed and resolved in the
 				<a href="${config.get("url.secure") ? "https" : "http"}://${config.get(
-			"url.host"
-		)}/admin/users">Users tab of the admin page</a>. Note: All admins will be sent the same message.
+					"url.host"
+				)}/admin/users">Users tab of the admin page</a>. Note: All admins will be sent the same message.
 			`
 	};
 

+ 0 - 1
backend/logic/mail/schemas/passwordRequest.js

@@ -2,7 +2,6 @@ import mail from "../index";
 
 /**
  * Sends a request password email
- *
  * @param {string} to - the email address of the recipient
  * @param {string} username - the username of the recipient
  * @param {string} code - the password code of the recipient

+ 0 - 1
backend/logic/mail/schemas/resetPasswordRequest.js

@@ -2,7 +2,6 @@ import mail from "../index";
 
 /**
  * Sends a request password reset email
- *
  * @param {string} to - the email address of the recipient
  * @param {string} username - the username of the recipient
  * @param {string} code - the password reset code of the recipient

+ 0 - 1
backend/logic/mail/schemas/verifyEmail.js

@@ -3,7 +3,6 @@ import mail from "../index";
 
 /**
  * Sends a verify email email
- *
  * @param {string} to - the email address of the recipient
  * @param {string} username - the username of the recipient
  * @param {string} code - the email reset code of the recipient

+ 0 - 9
backend/logic/media.js

@@ -22,7 +22,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Initialises the media module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -135,7 +134,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Recalculates dislikes and likes
-	 *
 	 * @param {object} payload - returns an object containing the payload
 	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -204,7 +202,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Recalculates all dislikes and likes
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	RECALCULATE_ALL_RATINGS() {
@@ -260,7 +257,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Gets ratings by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.mediaSource - the media source
 	 * @param {string} payload.createMissing - whether to create missing ratings
@@ -314,7 +310,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Remove ratings by id from the cache and Mongo
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.mediaSources - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -358,7 +353,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Get song or youtube video by mediaSource
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.mediaSource - the media source of the song/video
 	 * @param {string} payload.userId - the user id
@@ -489,7 +483,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Gets media from media sources
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.mediaSources - the media sources
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -580,7 +573,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Remove import job by id from Mongo
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.jobIds - the job ids
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -625,7 +617,6 @@ class _MediaModule extends CoreClass {
 
 	/**
 	 * Remove import job by id from Mongo
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.jobIds - the job ids
 	 * @returns {Promise} - returns a promise (resolve, reject)

+ 0 - 3
backend/logic/migration/index.js

@@ -26,7 +26,6 @@ class _MigrationModule extends CoreClass {
 
 	/**
 	 * Initialises the migration module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -100,7 +99,6 @@ class _MigrationModule extends CoreClass {
 
 	/**
 	 * Returns a database model
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {object} payload.modelName - name of the model to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -113,7 +111,6 @@ class _MigrationModule extends CoreClass {
 
 	/**
 	 * Runs migrations
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {object} payload.index - migration index
 	 * @returns {Promise} - returns promise (reject, resolve)

+ 0 - 1
backend/logic/migration/migrations/migration1.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 1
  *
  * This migration is used to set the documentVersion to 1 for all documents that don't have a documentVersion yet, meaning they were created before the migration system
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration10.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 10
  *
  * Migration for changes in how the order of songs in a playlist is handled
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration11.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 11
  *
  * Migration for changing language of verifying a song from 'accepted' to 'verified' for songs
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration12.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 12
  *
  * Migration for updated style of reports
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration13.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 13
  *
  * Migration for allowing titles, descriptions and individual resolving for report issues
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration14.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 14
  *
  * Migration for removing some data from stations
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration15.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 15
  *
  * Migration for setting user name to username if not set
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration16.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 16
  *
  * Migration for playlists to remove isUserModifiable
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration17.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 17
  *
  * Migration for songs to add tags property
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration18.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 18
  *
  * Migration for song status property.
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration19.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 19
  *
  * Migration for news showToNewUsers property.
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration2.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 2
  *
  * Updates the document version 1 stations to add the includedPlaylists and excludedPlaylists properties, and to create a station playlist and link that playlist with the playlist2 property.
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration20.js

@@ -5,7 +5,6 @@ import mongoose from "mongoose";
  * Migration 20
  *
  * Migration for station overhaul and preventing migration18 from always running
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration21.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 21
  *
  * Migration for song ratings
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration22.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 22
  *
  * Migration to fix issues in a previous migration (12), where report categories were not turned into lowercase
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration23.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 23
  *
  * Migration for renaming default user role from default to user
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration24.js

@@ -2,7 +2,6 @@
  * Migration 24
  *
  * Migration for setting station skip vote threshold
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration25.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 25
  *
  * Migration for changing youtubeId to mediaSource
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration3.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 3
  *
  * Clean up station object from playlist2 property (replacing old playlist property with playlist2 property), adding a playMode property and removing genres/blacklisted genres
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration4.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 4
  *
  * Migration for song merging. Merges queueSongs into songs database, and adds verified property to all songs.
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration5.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 5
  *
  * Migration for song status property.
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration6.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 6
  *
  * Migration for adding activityWatch preference to user object
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration7.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 7
  *
  * Migration for adding anonymous song requests preference to user object
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration8.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 8
  *
  * Migration for replacing songId with youtubeId whereever it is used, and using songId for any song's _id uses
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 1
backend/logic/migration/migrations/migration9.js

@@ -4,7 +4,6 @@ import async from "async";
  * Migration 9
  *
  * Migration for news
- *
  * @param {object} MigrationModule - the MigrationModule
  * @returns {Promise} - returns promise
  */

+ 0 - 4
backend/logic/musicbrainz.js

@@ -6,7 +6,6 @@ import { MUSARE_VERSION } from "..";
 class RateLimitter {
 	/**
 	 * Constructor
-	 *
 	 * @param {number} timeBetween - The time between each allowed MusicBrainz request
 	 */
 	constructor(timeBetween) {
@@ -16,7 +15,6 @@ class RateLimitter {
 
 	/**
 	 * Returns a promise that resolves whenever the ratelimit of a MusicBrainz request is done
-	 *
 	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
 	 */
 	continue() {
@@ -49,7 +47,6 @@ class _MusicBrainzModule extends CoreClass {
 
 	/**
 	 * Initialises the MusicBrainz module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -67,7 +64,6 @@ class _MusicBrainzModule extends CoreClass {
 
 	/**
 	 * Perform MusicBrainz API call
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - request url
 	 * @param {object} payload.params - request parameters

+ 1 - 6
backend/logic/notifications.js

@@ -19,7 +19,6 @@ class _NotificationsModule extends CoreClass {
 
 	/**
 	 * Initialises the notifications module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -126,7 +125,6 @@ class _NotificationsModule extends CoreClass {
 	 * Schedules a notification to be dispatched in a specific amount of milliseconds,
 	 * notifications are unique by name, and the first one is always kept, as in
 	 * attempting to schedule a notification that already exists won't do anything
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.name - the name of the notification we want to schedule
 	 * @param {number} payload.time - how long in milliseconds until the notification should be fired
@@ -158,7 +156,6 @@ class _NotificationsModule extends CoreClass {
 
 	/**
 	 * Subscribes a callback function to be called when a notification gets called
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.name - the name of the notification we want to subscribe to
 	 * @param {boolean} payload.unique - only subscribe if another subscription with the same name doesn't already exist
@@ -204,9 +201,8 @@ class _NotificationsModule extends CoreClass {
 
 	/**
 	 * Remove a notification subscription
-	 *
 	 * @param {object} payload - object containing the payload
-	 * @param {object} payload.subscription - the subscription object returned by {@link subscribe}
+	 * @param {object} payload.subscription - the subscription object returned by subscribe
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	REMOVE(payload) {
@@ -220,7 +216,6 @@ class _NotificationsModule extends CoreClass {
 
 	/**
 	 * Unschedules a notification by name (each notification has a unique name)
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.name - the name of the notification we want to schedule
 	 * @returns {Promise} - returns a promise (resolve, reject)

+ 11 - 29
backend/logic/playlists.js

@@ -21,7 +21,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Initialises the playlists module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -43,7 +42,17 @@ class _PlaylistsModule extends CoreClass {
 			cb: async data => {
 				PlaylistsModule.playlistModel.findOne(
 					{ _id: data.playlistId },
-					["_id", "displayName", "type", "privacy", "songs", "createdBy", "createdAt", "createdFor"],
+					[
+						"_id",
+						"displayName",
+						"type",
+						"privacy",
+						"songs",
+						"createdBy",
+						"createdAt",
+						"createdFor",
+						"featured"
+					],
 					(err, playlist) => {
 						const newPlaylist = {
 							...playlist._doc,
@@ -143,7 +152,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Returns a list of playlists that include a specific song
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.songId - the song id
 	 * @param {string} payload.includeSongs - include the songs
@@ -161,7 +169,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Returns a list of youtube ids in all user playlists of the specified user
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the user id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -187,7 +194,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Creates a playlist owned by a user
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the id of the user to create the playlist for
 	 * @param {string} payload.displayName - the display name of the playlist
@@ -215,7 +221,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Creates a playlist that contains all songs of a specific genre
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.genre - the genre
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -249,7 +254,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets all genre playlists
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.includeSongs - include the songs
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -266,7 +270,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets all station playlists
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.includeSongs - include the songs
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -283,7 +286,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a genre playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.genre - the genre
 	 * @param {string} payload.includeSongs - include the songs
@@ -306,7 +308,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets all missing genre playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_MISSING_GENRE_PLAYLISTS() {
@@ -348,7 +349,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Creates all missing genre playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	CREATE_MISSING_GENRE_PLAYLISTS() {
@@ -382,7 +382,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a station playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.staationId - the station id
 	 * @param {string} payload.includeSongs - include the songs
@@ -405,7 +404,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Adds a song to a playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @param {string} payload.mediaSource - the media source
@@ -506,7 +504,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Replaces a song in a playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @param {string} payload.newMediaSource - the new media source
@@ -625,7 +622,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Remove from playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @param {string} payload.mediaSource - the media source
@@ -712,7 +708,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Deletes a song from a playlist based on the media source
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @param {string} payload.mediaSource - the media source
@@ -739,7 +734,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Fills a genre playlist with songs
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.genre - the genre
 	 * @param {string} payload.createPlaylist - create playlist if it doesn't exist, default false
@@ -846,7 +840,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets orphaned genre playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_ORPHANED_GENRE_PLAYLISTS() {
@@ -888,7 +881,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Deletes all orphaned genre playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	DELETE_ORPHANED_GENRE_PLAYLISTS() {
@@ -923,7 +915,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a orphaned station playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_ORPHANED_STATION_PLAYLISTS() {
@@ -962,7 +953,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Deletes all orphaned station playlists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	DELETE_ORPHANED_STATION_PLAYLISTS() {
@@ -997,7 +987,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Fills a station playlist with songs
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1136,7 +1125,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the id of the playlist we are trying to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1207,7 +1195,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a playlist from id from Mongo and updates the cache with it
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the id of the playlist we are trying to update
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1255,7 +1242,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Deletes playlist from id from Mongo and cache
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the id of the playlist we are trying to delete
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1303,7 +1289,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Searches through playlists
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.query - the query
 	 * @param {string} payload.includePrivate - include private playlists
@@ -1394,7 +1379,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Clears and refills a station playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1439,7 +1423,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Clears and refills a genre playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1484,7 +1467,6 @@ class _PlaylistsModule extends CoreClass {
 
 	/**
 	 * Gets a list of all media sources from playlist songs
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async GET_ALL_MEDIA_SOURCES() {

+ 0 - 6
backend/logic/punishments.js

@@ -18,7 +18,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Initialises the punishments module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -106,7 +105,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Gets all punishments in the cache that are active, and removes those that have expired
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_PUNISHMENTS() {
@@ -176,7 +174,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Gets a punishment by id
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.id - the id of the punishment we are trying to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -232,7 +229,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Gets all punishments from a userId
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.userId - the userId of the punishment(s) we are trying to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -265,7 +261,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Adds a new punishment to the database
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.reason - the reason for the punishment e.g. spam
 	 * @param {string} payload.type - the type of punishment (enum: ["banUserId", "banUserIp"])
@@ -318,7 +313,6 @@ class _PunishmentsModule extends CoreClass {
 
 	/**
 	 * Deactivates a punishment
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.punishmentId - the MongoDB id of the punishment
 	 * @returns {Promise} - returns promise (reject, resolve)

+ 0 - 16
backend/logic/songs.js

@@ -24,7 +24,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Initialises the songs module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -126,7 +125,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.songId - the id of the song we are trying to get
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -172,7 +170,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets songs by id from Mongo
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.mediaSources - the media sources of the songs we are trying to get
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -305,7 +302,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Create song
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.song - the song object
 	 * @param {string} payload.userId - the user id of the person requesting the song
@@ -373,7 +369,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a song from id from Mongo and updates the cache with it
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.songId - the id of the song we are trying to update
 	 * @param {string} payload.oldStatus - old status of song being updated (optional)
@@ -564,7 +559,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets multiple songs from id from Mongo and updates the cache with it
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.songIds - the ids of the songs we are trying to update
 	 * @param {string} payload.oldStatus - old status of song being updated (optional)
@@ -860,7 +854,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Updates all songs
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	UPDATE_ALL_SONGS() {
@@ -985,7 +978,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Searches through songs
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.query - the query
 	 * @param {string} payload.includeUnverified - include unverified songs
@@ -1093,7 +1085,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets an array of all genres
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET_ALL_GENRES() {
@@ -1128,7 +1119,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets an array of all songs with a specific genre
-	 *
 	 * @param {object} payload - returns an object containing the payload
 	 * @param {string} payload.genre - the genre
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -1159,7 +1149,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a orphaned playlist songs
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_ORPHANED_PLAYLIST_SONGS() {
@@ -1201,7 +1190,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Requests all orphaned playlist songs, adding them to the database
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	REQUEST_ORPHANED_PLAYLIST_SONGS() {
@@ -1308,7 +1296,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a list of all genres
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_GENRES() {
@@ -1329,7 +1316,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a list of all artists
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_ARTISTS() {
@@ -1350,7 +1336,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a list of all tags
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_TAGS() {
@@ -1371,7 +1356,6 @@ class _SongsModule extends CoreClass {
 
 	/**
 	 * Gets a list of all media sources
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async GET_ALL_MEDIA_SOURCES() {

+ 0 - 14
backend/logic/soundcloud.js

@@ -69,7 +69,6 @@ const soundcloudTrackObjectToMusareTrackObject = soundcloudTrackObject => {
 class RateLimitter {
 	/**
 	 * Constructor
-	 *
 	 * @param {number} timeBetween - The time between each allowed YouTube request
 	 */
 	constructor(timeBetween) {
@@ -79,7 +78,6 @@ class RateLimitter {
 
 	/**
 	 * Returns a promise that resolves whenever the ratelimit of a YouTube request is done
-	 *
 	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
 	 */
 	continue() {
@@ -107,7 +105,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Initialises the soundcloud module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -152,7 +149,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Generates/fetches a new SoundCloud API key
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GENERATE_SOUNDCLOUD_API_KEY() {
@@ -195,7 +191,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Tests the stored SoundCloud API key
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	TEST_SOUNDCLOUD_API_KEY() {
@@ -226,7 +221,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Perform SoundCloud API get track request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.trackId - the SoundCloud track id to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -253,7 +247,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Perform SoundCloud API call
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - request url
 	 * @param {object} payload.params - request parameters
@@ -290,7 +283,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Create SoundCloud track
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {object} payload.soundcloudTrack - the soundcloudTrack object
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -336,7 +328,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Get SoundCloud track
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.identifier - the soundcloud track ObjectId or track id
 	 * @param {boolean} payload.createMissing - attempt to fetch and create track if not in db
@@ -390,7 +381,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Tries to get a SoundCloud track from a URL
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.identifier - the SoundCloud track URL
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -469,7 +459,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Returns an array of songs taken from a SoundCloud playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - the url of the SoundCloud playlist
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -529,7 +518,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Returns an array of songs taken from a SoundCloud artist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.artistId - the id of the SoundCloud artist
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -602,7 +590,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Get Soundcloud artists
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.userPermalinks - an array of Soundcloud user permalinks
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -678,7 +665,6 @@ class _SoundCloudModule extends CoreClass {
 
 	/**
 	 * Calls the API_CALL with the proper URL to get artist/user tracks
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.artistId - the id of the SoundCloud artist
 	 * @param {string} payload.nextHref - the next url to call

+ 0 - 24
backend/logic/spotify.js

@@ -37,7 +37,6 @@ const spotifyTrackObjectToMusareTrackObject = spotifyTrackObject => ({
 class RateLimitter {
 	/**
 	 * Constructor
-	 *
 	 * @param {number} timeBetween - The time between each allowed YouTube request
 	 */
 	constructor(timeBetween) {
@@ -47,7 +46,6 @@ class RateLimitter {
 
 	/**
 	 * Returns a promise that resolves whenever the ratelimit of a YouTube request is done
-	 *
 	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
 	 */
 	continue() {
@@ -75,7 +73,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Initialises the spotify module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -119,7 +116,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Fetches a Spotify API token from either the cache, or Spotify using the client id and secret from the config
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GET_API_TOKEN() {
@@ -185,7 +181,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Perform Spotify API get albums request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.albumIds - the album ids to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -215,7 +210,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Perform Spotify API get artists request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.artistIds - the artist ids to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -245,7 +239,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Perform Spotify API get track request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.trackId - the Spotify track id to get
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -272,7 +265,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Perform Spotify API get playlist request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the Spotify playlist id to get songs from
 	 * @param {string} payload.nextUrl - the next URL to use
@@ -300,7 +292,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Perform Spotify API call
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - request url
 	 * @param {object} payload.params - request parameters
@@ -344,7 +335,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Create Spotify track
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.spotifyTracks - the spotifyTracks
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -403,7 +393,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Create Spotify albums
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.spotifyAlbums - the Spotify albums
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -433,7 +422,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Create Spotify artists
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.spotifyArtists - the Spotify artists
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -463,7 +451,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Gets tracks from media sources
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.mediaSources - the media sources to get tracks from
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -510,7 +497,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Gets albums from Spotify album ids
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.albumIds - the Spotify album ids
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -553,7 +539,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Gets Spotify artists from Spotify artist ids
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.artistIds - the Spotify artist ids
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -596,7 +581,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Get Spotify track
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.identifier - the spotify track ObjectId or track id
 	 * @param {boolean} payload.createMissing - attempt to fetch and create track if not in db
@@ -652,7 +636,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Get Spotify album
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.identifier - the spotify album ObjectId or track id
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -671,7 +654,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Returns an array of songs taken from a Spotify playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - the id of the Spotify playlist
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -767,7 +749,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative artists sources for a list of Spotify artist ids
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.artistIds - the Spotify artist ids to try and get alternative artist sources for
 	 * @param {boolean} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
@@ -812,7 +793,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative artist sources for a Spotify artist id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.artistId - the Spotify artist id to try and get alternative artist sources for
 	 * @param {boolean} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
@@ -858,7 +838,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative album sources for a list of Spotify album ids
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.albumIds - the Spotify album ids to try and get alternative album sources for
 	 * @param {boolean} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
@@ -903,7 +882,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative album sources for a Spotify album id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.albumId - the Spotify album id to try and get alternative album sources for
 	 * @param {boolean} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
@@ -933,7 +911,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative track sources for a list of Spotify track media sources
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.mediaSources - the Spotify media sources to try and get alternative track sources for
 	 * @param {boolean} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found
@@ -978,7 +955,6 @@ class _SpotifyModule extends CoreClass {
 
 	/**
 	 * Tries to get alternative track sources for a Spotify track media source
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.mediaSource - the Spotify media source to try and get alternative track sources for
 	 * @param {boolean} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found

+ 0 - 26
backend/logic/stations.js

@@ -25,7 +25,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Initialises the stations module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -254,7 +253,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Initialises a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - id of the station to initialise
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -387,7 +385,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - id of the station
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -439,7 +436,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Attempts to get a station by name, firstly from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationName - the unique name of the station
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -474,7 +470,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Updates the station in cache from mongo or deletes station in cache if no longer in mongo.
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station to update
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -531,7 +526,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Autofill station queue from station playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station
 	 * @param {string} payload.ignoreExistingQueue - ignore the existing queue songs, replacing the old queue with a completely fresh one
@@ -765,7 +759,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Gets next station song
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -840,7 +833,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Removes first station queue song
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -884,7 +876,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Process vote to skips for a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station to process
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -988,7 +979,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Creates a station history item
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.stationId - the station id to create the history item for
 	 * @param {object} payload.currentSong - the song to create the history item for
@@ -1031,7 +1021,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Skips a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the id of the station to skip
 	 * @param {string} payload.natural - whether to skip naturally or forcefully
@@ -1329,7 +1318,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Checks if a user can view/access a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.station - the station object of the station in question
 	 * @param {string} payload.userId - the id of the user in question
@@ -1373,7 +1361,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Checks if a user has favorited a station or not
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.stationId - the id of the station in question
 	 * @param {string} payload.userId - the id of the user in question
@@ -1409,7 +1396,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Returns a list of sockets in a room that can and can't know about a station
-	 *
 	 * @param {object} payload - the payload object
 	 * @param {object} payload.station - the station object
 	 * @param {string} payload.room - the websockets room to get the sockets from
@@ -1477,7 +1463,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Adds a playlist to autofill a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.stationId - the id of the station
 	 * @param {object} payload.playlistId - the id of the playlist
@@ -1551,7 +1536,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Removes a playlist from autofill
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.stationId - the id of the station
 	 * @param {object} payload.playlistId - the id of the playlist
@@ -1617,7 +1601,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Add a playlist to station blacklist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.stationId - the id of the station
 	 * @param {object} payload.playlistId - the id of the playlist
@@ -1693,7 +1676,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Remove a playlist from station blacklist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.stationId - the id of the station
 	 * @param {object} payload.playlistId - the id of the playlist
@@ -1759,7 +1741,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Removes autofilled or blacklisted playlist from a station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1805,7 +1786,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Gets stations that autofill or blacklist a specific playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1834,7 +1814,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Clears every queue
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	CLEAR_EVERY_STATION_QUEUE() {
@@ -1886,7 +1865,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Resets a station queue
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -1945,7 +1923,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Add to a station queue
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @param {string} payload.mediaSource - the media source
@@ -2163,7 +2140,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Remove from a station queue
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @param {string} payload.mediaSource - the media source
@@ -2235,7 +2211,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Add DJ to station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @param {string} payload.userId - the dj user id
@@ -2293,7 +2268,6 @@ class _StationsModule extends CoreClass {
 
 	/**
 	 * Remove DJ from station
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
 	 * @param {string} payload.userId - the dj user id

+ 0 - 10
backend/logic/tasks.js

@@ -39,7 +39,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Initialises the tasks module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -86,7 +85,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Creates a new task
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.name - the name of the task
 	 * @param {string} payload.fn - the function the task will run
@@ -114,7 +112,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Pauses a task
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.taskName - the name of the task to pause
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -130,7 +127,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Resumes a task
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.name - the name of the task to resume
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -144,7 +140,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Runs a task's function and restarts the timer
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.name - the name of the task to run
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -168,7 +163,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Periodically checks if any stations need to be skipped
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	checkStationSkipTask() {
@@ -210,7 +204,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Periodically checks if any sessions are out of date and need to be cleared
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	sessionClearingTask() {
@@ -298,7 +291,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Periodically warns about the size of any log files
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	logFileSizeCheckTask() {
@@ -353,7 +345,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Periodically collect users in stations
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async collectStationUsersTask() {
@@ -500,7 +491,6 @@ class _TasksModule extends CoreClass {
 
 	/**
 	 * Periodically removes any old history documents
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async historyClearTask() {

+ 0 - 12
backend/logic/utils.js

@@ -13,7 +13,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Initialises the utils module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	initialize() {
@@ -24,7 +23,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Parses the cookie into a readable object
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.cookieString - the cookie string
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -62,7 +60,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Removes a cookie by name
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.cookieString - the cookie string
 	 * @param {string} payload.cookieName - the unique name of the cookie
@@ -93,7 +90,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Replaces any html reserved characters in a string with html entities
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.str - the string to replace characters with html entities
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -112,7 +108,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Generates a random string of a specified length
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {number} payload.length - the length the random string should be
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -148,7 +143,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Creates a random number within a range
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {number} payload.min - the minimum number the result should be
 	 * @param {number} payload.max - the maximum number the result should be
@@ -163,7 +157,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Converts ISO8601 time format (YouTube API) to HH:MM:SS
-	 *
 	 * @param  {object} payload - object contaiing the payload
 	 * @param {string} payload.duration - string in the format of ISO8601
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -216,7 +209,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Creates a random identifier for e.g. sessionId
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	GUID() {
@@ -237,7 +229,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Shuffles an array
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.array - an array of songs that should be shuffled
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -270,7 +261,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Shuffles an array of songs by their position property
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.array - an array of songs that should be shuffled
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -312,7 +302,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Creates an error
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.error - object that contains the error
 	 * @param {string} payload.message - possible error message
@@ -333,7 +322,6 @@ class _UtilsModule extends CoreClass {
 
 	/**
 	 * Creates the gravatar url for a specified email address
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.email - the email address
 	 * @returns {Promise} - returns promise (reject, resolve)

+ 0 - 9
backend/logic/wikidata.js

@@ -5,7 +5,6 @@ import CoreClass from "../core";
 class RateLimitter {
 	/**
 	 * Constructor
-	 *
 	 * @param {number} timeBetween - The time between each allowed WikiData request
 	 */
 	constructor(timeBetween) {
@@ -15,7 +14,6 @@ class RateLimitter {
 
 	/**
 	 * Returns a promise that resolves whenever the ratelimit of a WikiData request is done
-	 *
 	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
 	 */
 	continue() {
@@ -48,7 +46,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Initialises the activities module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -66,7 +63,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Get WikiData data from entity url
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.entityUrl - entity url
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -96,7 +92,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Get WikiData data from work id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.workId - work id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -135,7 +130,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Get WikiData data from release group id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.releaseGroupId - release group id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -174,7 +168,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Get WikiData data from Spotify album id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.spotifyAlbumId - Spotify album id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -213,7 +206,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Get WikiData data from Spotify artist id
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.spotifyArtistId - Spotify artist id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -254,7 +246,6 @@ class _WikiDataModule extends CoreClass {
 
 	/**
 	 * Perform WikiData API call
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.url - request url
 	 * @param {object} payload.params - request parameters

+ 7 - 22
backend/logic/ws.js

@@ -19,14 +19,13 @@ let PunishmentsModule;
 class _WSModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
-		super("ws");
+		super("ws", { concurrency: 2 });
 
 		WSModule = this;
 	}
 
 	/**
 	 * Initialises the ws module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -92,7 +91,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Returns the websockets variable
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	WS() {
@@ -103,7 +101,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Obtains socket object for a specified socket id
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.socketId - the id of the socket
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -125,7 +122,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Gets all sockets for a specified session id
-	 *
 	 * @param {object} payload - object containing the payload
 	 * @param {string} payload.sessionId - user session id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -154,7 +150,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Returns any sockets for a specific user
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the user id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -190,7 +185,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Returns any sockets from a specific ip address
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.ip - the ip address in question
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -220,7 +214,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Returns any sockets from a specific user without using redis/cache
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.userId - the id of the user in question
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -249,7 +242,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows a socket to leave any rooms they are connected to
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket which should leave all their rooms
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -267,7 +259,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows a socket to leave a specific room they are connected to
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket which should leave a room
 	 * @param {string} payload.room - the room
@@ -287,7 +278,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows a socket to join a specified room (this will remove them from any rooms they are currently in)
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket which should join the room
 	 * @param {string} payload.room - the name of the room
@@ -307,7 +297,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Emits arguments to any sockets that are in a specified a room
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.room - the name of the room to emit arguments
 	 * @param {object} payload.args - any arguments to be emitted to the sockets in the specific room
@@ -332,7 +321,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Emits arguments to any sockets that are in specified rooms
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.rooms - array of strings with the name of each room e.g. ["station-page", "song.1234"]
 	 * @param {object} payload.args - any arguments to be emitted to the sockets in the specific room
@@ -353,7 +341,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows a socket to join a 'song' room
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket which should join the room
 	 * @param {string} payload.room - the name of the room
@@ -376,7 +363,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows multiple sockets to join a 'song' room
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.sockets - array of socketIds
 	 * @param {object} payload.room - the name of the room
@@ -394,7 +380,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Allows multiple sockets to leave any 'song' rooms they are in
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.sockets - array of socketIds
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -416,7 +401,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Gets any sockets connected to a room
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.room - the name of the room
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -430,7 +414,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Gets any rooms a socket is connected to
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.socketId - the id of the socket to check the rooms for
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -449,7 +432,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Handles use of websockets
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
@@ -541,7 +523,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Handles a websocket connection
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.socket - socket itself
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -598,6 +579,7 @@ class _WSModule extends CoreClass {
 					messages: config.get("messages"),
 					christmas: config.get("christmas"),
 					footerLinks: config.get("footerLinks"),
+					primaryColor: config.get("primaryColor"),
 					shortcutOverrides: config.get("shortcutOverrides"),
 					registrationDisabled: config.get("registrationDisabled"),
 					mailEnabled: config.get("mail.enabled"),
@@ -647,6 +629,11 @@ class _WSModule extends CoreClass {
 				if (data.length === 0) return socket.dispatch("ERROR", "Not enough arguments specified.");
 				if (typeof data[0] !== "string") return socket.dispatch("ERROR", "First argument must be a string.");
 
+				if (data[0] === "ping" && data.length === 2) {
+					const [, CB_REF] = data;
+					return socket.dispatch("CB_REF", CB_REF.CB_REF, Date.now());
+				}
+
 				const namespaceAction = data[0];
 				if (
 					!namespaceAction ||
@@ -696,7 +683,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Runs an action
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
@@ -769,7 +755,6 @@ class _WSModule extends CoreClass {
 
 	/**
 	 * Runs an action
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */

+ 108 - 35
backend/logic/youtube.js

@@ -7,10 +7,12 @@ import axios from "axios";
 
 import CoreClass from "../core";
 
+const YOUTUBE_OFFICIAL_CHANNEL_ID = "UCBR8-60-B28hp2BmDPdntcQ";
+const YOUTUBE_MIX_PLAYLIST_TITLE_PREFIX = "Mix - ";
+
 class RateLimitter {
 	/**
 	 * Constructor
-	 *
 	 * @param {number} timeBetween - The time between each allowed YouTube request
 	 */
 	constructor(timeBetween) {
@@ -20,7 +22,6 @@ class RateLimitter {
 
 	/**
 	 * Returns a promise that resolves whenever the ratelimit of a YouTube request is done
-	 *
 	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
 	 */
 	continue() {
@@ -97,7 +98,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Initialises the activities module
-	 *
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async initialize() {
@@ -189,7 +189,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Fetches a list of songs from Youtube's API
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.query - the query we'll pass to youtubes api
 	 * @param {string} payload.pageToken - (optional) if this exists, will search search youtube for a specific page reference
@@ -228,7 +227,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Returns details about the YouTube quota usage
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.fromDate - date to select requests up to
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -289,7 +287,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Returns YouTube quota chart data
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.timePeriod - either hours or days
 	 * @param {string} payload.startDate - beginning date
@@ -470,7 +467,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Gets the id of the channel upload playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.id - the id of the YouTube channel. Optional: can be left out if specifying a username.
 	 * @param {string} payload.username - the username of the YouTube channel. Only gets used if no id is specified.
@@ -513,7 +509,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Gets the id of the channel from the custom URL
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.customUrl - the customUrl of the YouTube channel
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -607,7 +602,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Returns an array of songs taken from a YouTube playlist
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {boolean} payload.musicOnly - whether to return music videos or all videos in the playlist
 	 * @param {string} payload.url - the url of the YouTube playlist
@@ -624,28 +618,67 @@ class _YouTubeModule extends CoreClass {
 				return;
 			}
 			const playlistId = splitQuery[1];
-			const maxPages = Number.parseInt(config.get("apis.youtube.maxPlaylistPages"));
 
 			let currentPage = 0;
 
 			async.waterfall(
 				[
 					next => {
+						YouTubeModule.runJob(
+							"GET_PLAYLIST_INFO",
+							{
+								playlistId
+							},
+							this
+						)
+							.then(playlistInfo => {
+								next(null, playlistInfo);
+							})
+							.catch(err => {
+								next(err);
+							});
+					},
+
+					(playlistInfo, next) => {
+						if (playlistInfo.privacyStatus === "private") return next(new Error("Playlist is private."));
+
+						const maxPages = playlistInfo.isMix
+							? 4
+							: Number.parseInt(config.get("apis.youtube.maxPlaylistPages"));
+
+						return next(null, maxPages, playlistInfo.isMix);
+					},
+
+					(maxPages, isMix, next) => {
 						let songs = [];
 						let nextPageToken = "";
 
 						async.whilst(
 							next => {
+								if (nextPageToken === undefined) return next(null, false);
+
+								if (currentPage >= maxPages) {
+									YouTubeModule.log(
+										isMix ? "INFO" : "ERROR",
+										`Playlist ${playlistId}${
+											isMix ? " (mix)" : ""
+										} for job (${this.toString()}) has reached the max page limit.`
+									);
+									return next(null, false);
+								}
+
+								return next(null, true);
+							},
+							next => {
+								currentPage += 1;
+
 								YouTubeModule.log(
 									"INFO",
 									`Getting playlist progress for job (${this.toString()}): ${
 										songs.length
-									} songs gotten so far. Is there a next page: ${nextPageToken !== undefined}.`
+									} songs gotten so far. Current page: ${currentPage}`
 								);
-								next(null, nextPageToken !== undefined && currentPage < maxPages);
-							},
-							next => {
-								currentPage += 1;
+
 								// Add 250ms delay between each job request
 								setTimeout(() => {
 									YouTubeModule.runJob("GET_PLAYLIST_PAGE", { playlistId, nextPageToken }, this)
@@ -686,9 +719,45 @@ class _YouTubeModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Returns playlist info
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the playlist id
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async GET_PLAYLIST_INFO(payload) {
+		const { playlistId } = payload;
+		const part = ["id", "snippet", "status", "localizations"].join(",");
+		const params = {
+			part,
+			id: playlistId
+		};
+
+		const { response } = await YouTubeModule.runJob("API_GET_PLAYLIST", { params }, this);
+		const [playlistInfo] = response.data.items;
+
+		const channelId = playlistInfo?.snippet?.channelId;
+		const title = playlistInfo?.snippet?.title;
+		const enTitle = playlistInfo?.localizations?.en?.title;
+		const privacyStatus = playlistInfo?.status?.privacyStatus;
+
+		// Another way to possibly check for mix is if the first two letters of the playlist ID starts with RD
+		const isMix =
+			channelId === YOUTUBE_OFFICIAL_CHANNEL_ID &&
+			(title?.startsWith(YOUTUBE_MIX_PLAYLIST_TITLE_PREFIX) ||
+				enTitle?.startsWith(YOUTUBE_MIX_PLAYLIST_TITLE_PREFIX));
+
+		return {
+			channelId,
+			title,
+			enTitle,
+			privacyStatus,
+			isMix
+		};
+	}
+
 	/**
 	 * Returns a a page from a YouTube playlist. Is used internally by GET_PLAYLIST and GET_CHANNEL_VIDEOS.
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {boolean} payload.playlistId - the playlist id to get videos from
 	 * @param {boolean} payload.nextPageToken - the nextPageToken to use
@@ -732,7 +801,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Filters a list of YouTube videos so that they only contains videos with music. Is used internally by GET_PLAYLIST
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {Array} payload.videoIds - an array of YouTube videoIds to filter through
 	 * @param {Array} payload.page - the current page/set of video's to get, starting at 0. If left null, 0 is assumed. Will recurse.
@@ -788,7 +856,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Returns an array of songs taken from a YouTube channel
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {boolean} payload.musicOnly - whether to return music videos or all videos in the channel
 	 * @param {boolean} payload.disableSearch - whether to allow searching for custom url/username
@@ -912,7 +979,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Perform YouTube API get videos request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.params - request parameters
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -944,7 +1010,29 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Perform YouTube API get playlist items request
-	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {object} payload.params - request parameters
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	async API_GET_PLAYLIST(payload) {
+		const { params } = payload;
+
+		return YouTubeModule.runJob(
+			"API_CALL",
+			{
+				url: "https://www.googleapis.com/youtube/v3/playlists",
+				params: {
+					key: config.get("apis.youtube.key"),
+					...params
+				},
+				quotaCost: 1
+			},
+			this
+		);
+	}
+
+	/**
+	 * Perform YouTube API get playlist items request
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.params - request parameters
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -976,7 +1064,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Perform YouTube API get channels request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.params - request parameters
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1008,7 +1095,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Perform YouTube API search request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.params - request parameters
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1045,7 +1131,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Perform YouTube API call
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.url - request url
 	 * @param {object} payload.params - request parameters
@@ -1104,7 +1189,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Fetch all api requests
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.fromDate - data to fetch requests up to
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1127,7 +1211,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Fetch an api request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.apiRequestId - the api request id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1188,7 +1271,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Removed all stored api requests from mongo and redis
-	 *
 	 * 	 @returns {Promise} - returns promise (reject, resolve)
 	 */
 	RESET_STORED_API_REQUESTS() {
@@ -1262,7 +1344,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Remove a stored api request
-	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {object} payload.requestId - the api request id
 	 * @returns {Promise} - returns promise (reject, resolve)
@@ -1325,7 +1406,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Create YouTube videos
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array | object} payload.youtubeVideos - the youtubeVideo object or array of
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -1371,7 +1451,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Get YouTube videos
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.identifiers - an array of YouTube video ObjectId's or YouTube ID's
 	 * @param {boolean} payload.createMissing - attempt to fetch and create video's if not in db
@@ -1483,7 +1562,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Get YouTube channels
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {Array} payload.channelIds - an array of YouTube channel id's
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -1555,7 +1633,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Remove YouTube videos
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.videoIds - Array of youtubeVideo ObjectIds
 	 * @returns {Promise} - returns a promise (resolve, reject)
@@ -1738,7 +1815,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Request a set of YouTube videos
-	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.url - the url of the the YouTube playlist or channel
 	 * @param {boolean} payload.musicOnly - whether to only get music from the playlist/channel
@@ -1791,7 +1867,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Gets missing YouTube video's from all playlists, stations and songs
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	async GET_MISSING_VIDEOS() {
@@ -1828,7 +1903,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Updates videos from version 1 to version 2
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	async UPDATE_VIDEOS_V1_TO_V2() {
@@ -1847,7 +1921,6 @@ class _YouTubeModule extends CoreClass {
 
 	/**
 	 * Gets missing YouTube channels based on cached YouTube video's
-	 *
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	async GET_MISSING_CHANNELS() {

File diff suppressed because it is too large
+ 382 - 219
backend/package-lock.json


+ 27 - 24
backend/package.json

@@ -1,7 +1,7 @@
 {
   "name": "musare-backend",
   "private": true,
-  "version": "3.10.0",
+  "version": "3.11.0",
   "type": "module",
   "description": "An open-source collaborative music listening and catalogue curation application. Currently supporting YouTube based content.",
   "main": "index.js",
@@ -16,40 +16,43 @@
     "typescript": "tsc --noEmit --skipLibCheck"
   },
   "dependencies": {
-    "async": "^3.2.4",
-    "axios": "^1.3.4",
-    "bcrypt": "^5.1.0",
+    "async": "^3.2.5",
+    "axios": "^1.6.7",
+    "bcrypt": "^5.1.1",
     "bluebird": "^3.7.2",
     "body-parser": "^1.20.2",
-    "config": "^3.3.9",
+    "config": "^3.3.11",
     "cookie-parser": "^1.4.6",
     "cors": "^2.8.5",
     "express": "^4.18.2",
-    "moment": "^2.29.4",
-    "mongoose": "^6.6.5",
-    "nodemailer": "^6.9.1",
+    "moment": "^2.30.1",
+    "mongoose": "^6.12.6",
+    "nodemailer": "^6.9.10",
     "oauth": "^0.10.0",
-    "redis": "^4.6.5",
-    "retry-axios": "^3.0.0",
+    "redis": "^4.6.13",
+    "retry-axios": "^3.1.3",
     "sha256": "^0.2.0",
-    "socks": "^2.7.1",
+    "socks": "^2.8.1",
     "soundcloud-key-fetch": "^1.0.13",
     "underscore": "^1.13.6",
-    "ws": "^8.13.0"
+    "ws": "^8.16.0"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^5.54.1",
-    "@typescript-eslint/parser": "^5.54.1",
-    "eslint": "^8.36.0",
+    "@typescript-eslint/eslint-plugin": "^7.0.2",
+    "@typescript-eslint/parser": "^7.0.2",
+    "eslint": "^8.56.0",
     "eslint-config-airbnb-base": "^15.0.0",
-    "eslint-config-prettier": "^8.7.0",
-    "eslint-plugin-import": "^2.27.5",
-    "eslint-plugin-jsdoc": "^40.0.1",
-    "eslint-plugin-prettier": "^4.2.1",
-    "nodemon": "^2.0.21",
-    "prettier": "2.8.4",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-import": "^2.29.1",
+    "eslint-plugin-jsdoc": "^48.2.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "nodemon": "^3.1.0",
+    "prettier": "3.2.5",
     "trace-unhandled": "^2.0.1",
-    "ts-node": "^10.9.1",
-    "typescript": "^4.9.5"
+    "ts-node": "^10.9.2",
+    "typescript": "^5.3.3"
+  },
+  "overrides": {
+    "@aws-sdk/credential-providers": "npm:dry-uninstall"
   }
-}
+}

+ 3 - 0
docker-compose.yml

@@ -12,6 +12,7 @@ services:
     environment:
       - CONTAINER_MODE=${CONTAINER_MODE:-production}
       - MUSARE_SITENAME=${MUSARE_SITENAME:-Musare}
+      - MUSARE_PRIMARY_COLOR=${MUSARE_PRIMARY_COLOR:-#03a9f4}
       - MUSARE_DEBUG_VERSION=${MUSARE_DEBUG_VERSION:-true}
       - MUSARE_DEBUG_GIT_REMOTE=${MUSARE_DEBUG_GIT_REMOTE:-false}
       - MUSARE_DEBUG_GIT_REMOTE_URL=${MUSARE_DEBUG_GIT_REMOTE_URL:-false}
@@ -36,6 +37,7 @@ services:
         FRONTEND_MODE: "${FRONTEND_MODE:-production}"
         FRONTEND_PROD_DEVTOOLS: "${FRONTEND_PROD_DEVTOOLS:-false}"
         MUSARE_SITENAME: "${MUSARE_SITENAME:-Musare}"
+        MUSARE_PRIMARY_COLOR: "${MUSARE_PRIMARY_COLOR:-#03a9f4}"
         MUSARE_DEBUG_VERSION: "${MUSARE_DEBUG_VERSION:-true}"
         MUSARE_DEBUG_GIT_REMOTE: "${MUSARE_DEBUG_GIT_REMOTE:-false}"
         MUSARE_DEBUG_GIT_REMOTE_URL: "${MUSARE_DEBUG_GIT_REMOTE_URL:-false}"
@@ -54,6 +56,7 @@ services:
       - FRONTEND_DEV_PORT=${FRONTEND_DEV_PORT:-81}
       - FRONTEND_PROD_DEVTOOLS=${FRONTEND_PROD_DEVTOOLS:-false}
       - MUSARE_SITENAME=${MUSARE_SITENAME:-Musare}
+      - MUSARE_PRIMARY_COLOR=${MUSARE_PRIMARY_COLOR:-#03a9f4}
       - MUSARE_DEBUG_VERSION=${MUSARE_DEBUG_VERSION:-true}
       - MUSARE_DEBUG_GIT_REMOTE=${MUSARE_DEBUG_GIT_REMOTE:-false}
       - MUSARE_DEBUG_GIT_REMOTE_URL=${MUSARE_DEBUG_GIT_REMOTE_URL:-false}

+ 2 - 0
frontend/Dockerfile

@@ -12,6 +12,7 @@ FROM node:18 AS musare_frontend
 ARG FRONTEND_MODE=production
 ARG FRONTEND_PROD_DEVTOOLS=false
 ARG MUSARE_SITENAME=Musare
+ARG MUSARE_PRIMARY_COLOR="#03a9f4"
 ARG MUSARE_DEBUG_VERSION=true
 ARG MUSARE_DEBUG_GIT_REMOTE=false
 ARG MUSARE_DEBUG_GIT_REMOTE_URL=false
@@ -22,6 +23,7 @@ ARG MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT=true
 ENV FRONTEND_MODE=${FRONTEND_MODE} \
     FRONTEND_PROD_DEVTOOLS=${FRONTEND_PROD_DEVTOOLS} \
     MUSARE_SITENAME=${MUSARE_SITENAME} \
+    MUSARE_PRIMARY_COLOR=${MUSARE_PRIMARY_COLOR} \
     MUSARE_DEBUG_VERSION=${MUSARE_DEBUG_VERSION} \
     MUSARE_DEBUG_GIT_REMOTE=${MUSARE_DEBUG_GIT_REMOTE} \
     MUSARE_DEBUG_GIT_REMOTE_URL=${MUSARE_DEBUG_GIT_REMOTE_URL} \

File diff suppressed because it is too large
+ 282 - 400
frontend/package-lock.json


+ 35 - 36
frontend/package.json

@@ -5,7 +5,7 @@
     "*.vue"
   ],
   "private": true,
-  "version": "3.10.0",
+  "version": "3.11.0",
   "description": "An open-source collaborative music listening and catalogue curation application. Currently supporting YouTube based content.",
   "main": "main.js",
   "author": "Musare Team",
@@ -20,48 +20,47 @@
     "coverage": "vitest run --coverage"
   },
   "devDependencies": {
-    "@pinia/testing": "^0.0.15",
-    "@types/can-autoplay": "^3.0.1",
-    "@types/dompurify": "^2.4.0",
-    "@types/marked": "^4.0.8",
-    "@typescript-eslint/eslint-plugin": "^5.54.1",
-    "@typescript-eslint/parser": "^5.54.1",
-    "@vitest/coverage-c8": "^0.29.2",
-    "@vue/test-utils": "^2.3.1",
-    "eslint": "^8.36.0",
-    "eslint-config-prettier": "^8.7.0",
-    "eslint-plugin-import": "^2.27.5",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.9.0",
-    "jsdom": "^21.1.0",
-    "less": "^4.1.3",
-    "prettier": "^2.8.4",
-    "vite-plugin-dynamic-import": "^1.2.7",
-    "vitest": "^0.29.2",
-    "vue-eslint-parser": "^9.1.0",
-    "vue-tsc": "^1.2.0"
+    "@pinia/testing": "^0.1.3",
+    "@types/can-autoplay": "^3.0.4",
+    "@types/dompurify": "^3.0.5",
+    "@typescript-eslint/eslint-plugin": "^7.0.2",
+    "@typescript-eslint/parser": "^7.0.2",
+    "@vitest/coverage-v8": "^1.3.1",
+    "@vue/test-utils": "^2.4.4",
+    "eslint": "^8.56.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-import": "^2.29.1",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-vue": "^9.22.0",
+    "jsdom": "^24.0.0",
+    "less": "^4.2.0",
+    "prettier": "^3.2.5",
+    "vite-plugin-dynamic-import": "^1.5.0",
+    "vitest": "^1.3.1",
+    "vue-eslint-parser": "^9.4.2",
+    "vue-tsc": "^1.8.27"
   },
   "dependencies": {
-    "@intlify/unplugin-vue-i18n": "^0.9.1",
-    "@vitejs/plugin-vue": "^4.0.0",
+    "@intlify/unplugin-vue-i18n": "^2.0.0",
+    "@vitejs/plugin-vue": "^5.0.4",
     "can-autoplay": "^3.0.2",
-    "chart.js": "^4.2.1",
-    "date-fns": "^2.29.3",
-    "dompurify": "^3.0.1",
+    "chart.js": "^4.4.1",
+    "date-fns": "^3.3.1",
+    "dompurify": "^3.0.9",
     "eslint-config-airbnb-base": "^15.0.0",
-    "marked": "^4.2.12",
+    "marked": "^12.0.0",
     "normalize.css": "^8.0.1",
-    "pinia": "^2.0.33",
+    "pinia": "^2.1.7",
     "toasters": "^2.3.1",
-    "typescript": "^4.9.5",
-    "vite": "^4.1.4",
-    "vue": "^3.2.47",
-    "vue-chartjs": "^5.2.0",
+    "typescript": "^5.3.3",
+    "vite": "^5.1.4",
+    "vue": "^3.3.2",
+    "vue-chartjs": "^5.3.0",
     "vue-content-loader": "^2.0.1",
     "vue-draggable-list": "^0.1.3",
-    "vue-i18n": "^9.2.2",
-    "vue-json-pretty": "^2.2.3",
-    "vue-router": "^4.1.6",
-    "vue-tippy": "^6.0.0"
+    "vue-i18n": "^9.9.1",
+    "vue-json-pretty": "^2.3.0",
+    "vue-router": "^4.3.0",
+    "vue-tippy": "^6.4.1"
   }
 }

+ 22 - 7
frontend/src/App.vue

@@ -105,6 +105,9 @@ watch(christmas, enabled => {
 });
 
 onMounted(async () => {
+	document.getElementsByTagName("html")[0].style.cssText =
+		`--primary-color: ${configStore.primaryColor}`;
+
 	window
 		.matchMedia("(prefers-color-scheme: dark)")
 		.addEventListener("change", e => {
@@ -122,6 +125,9 @@ onMounted(async () => {
 	socket.onConnect(() => {
 		socketConnected.value = true;
 
+		document.getElementsByTagName("html")[0].style.cssText =
+			`--primary-color: ${configStore.primaryColor}`;
+
 		if (!loggedIn.value) {
 			broadcastChannel.value.user_login = new BroadcastChannel(
 				`${configStore.cookie}.user_login`
@@ -410,7 +416,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 200;
 	src: url("/fonts/nunito-v16-latin-200.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/nunito-v16-latin-200.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/nunito-v16-latin-200.woff2") format("woff2"),
@@ -428,7 +435,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 400;
 	src: url("/fonts/nunito-v16-latin-regular.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/nunito-v16-latin-regular.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/nunito-v16-latin-regular.woff2")
@@ -447,7 +455,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 600;
 	src: url("/fonts/nunito-v16-latin-600.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/nunito-v16-latin-600.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/nunito-v16-latin-600.woff2") format("woff2"),
@@ -465,7 +474,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 700;
 	src: url("/fonts/nunito-v16-latin-700.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/nunito-v16-latin-700.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/nunito-v16-latin-700.woff2") format("woff2"),
@@ -483,7 +493,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 800;
 	src: url("/fonts/nunito-v16-latin-800.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/nunito-v16-latin-800.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/nunito-v16-latin-800.woff2") format("woff2"),
@@ -501,7 +512,8 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 400;
 	src: url("/fonts/pacifico-v17-latin-regular.eot"); /* IE9 Compat Modes */
-	src: local(""),
+	src:
+		local(""),
 		url("/fonts/pacifico-v17-latin-regular.eot?#iefix")
 			format("embedded-opentype"),
 		/* IE6-IE8 */ url("/fonts/pacifico-v17-latin-regular.woff2")
@@ -519,7 +531,9 @@ onMounted(async () => {
 	font-style: normal;
 	font-weight: 400;
 	src: url(/fonts/MaterialIcons-Regular.ttf); /* For IE6-8 */
-	src: local("Material Icons"), local("MaterialIcons-Regular"),
+	src:
+		local("Material Icons"),
+		local("MaterialIcons-Regular"),
 		url(/fonts/MaterialIcons-Regular.ttf) format("truetype");
 }
 
@@ -2070,6 +2084,7 @@ h4.section-title {
 		border-radius: 34px;
 
 		&.disabled {
+			filter: grayscale(1);
 			cursor: not-allowed;
 		}
 	}

+ 4 - 4
frontend/src/components/AdvancedTable.vue

@@ -63,7 +63,7 @@ const props = defineProps({
 	name: { type: String, default: null },
 	maxWidth: { type: Number, default: 1880 },
 	query: { type: Boolean, default: true },
-	keyboardShortcuts: { type: Boolean, default: true },
+	hasKeyboardShortcuts: { type: Boolean, default: true },
 	events: { type: Object as PropType<TableEvents>, default: () => {} },
 	bulkActions: {
 		type: Object as PropType<TableBulkActions>,
@@ -979,7 +979,7 @@ onMounted(async () => {
 			removeData(index);
 		});
 
-	if (props.keyboardShortcuts) {
+	if (props.hasKeyboardShortcuts) {
 		// Navigation section
 
 		// Page navigation section
@@ -1136,7 +1136,7 @@ onUnmounted(() => {
 	if (columnOrderChangedDebounceTimeout.value)
 		clearTimeout(columnOrderChangedDebounceTimeout.value);
 
-	if (props.keyboardShortcuts) {
+	if (props.hasKeyboardShortcuts) {
 		const shortcutNames = [
 			// Navigation
 			"advancedTable.previousPage",
@@ -1770,7 +1770,7 @@ watch(selectedRows, (newSelectedRows, oldSelectedRows) => {
 															] !== undefined
 																? previous[
 																		current
-																  ]
+																	]
 																: null,
 														item
 													) !== null

+ 1 - 1
frontend/src/components/FloatingBox.vue

@@ -43,7 +43,7 @@ const saveBox = () => {
 				? Math.max(
 						document.body.clientHeight - 10 - dragBox.value.height,
 						0
-				  )
+					)
 				: 10,
 		left: 10
 	});

+ 3 - 3
frontend/src/components/LongJobs.spec.ts

@@ -36,11 +36,11 @@ describe("LongJobs component", async () => {
 											message: "Successfully edited tags."
 										}
 									}
-							  }
+								}
 							: {
 									status: "error",
 									message: "Long job not found."
-							  },
+								},
 					"users.removeLongJob": () => ({
 						status: "success"
 					})
@@ -61,7 +61,7 @@ describe("LongJobs component", async () => {
 										status: "update",
 										message: "Updating tags in MongoDB."
 									}
-							  ]
+								]
 							: []
 				},
 				on: {

+ 1 - 1
frontend/src/components/MainFooter.vue

@@ -25,7 +25,7 @@ const getLink = title =>
 		<div class="container">
 			<div class="footer-content">
 				<div id="footer-copyright">
-					<p>© Copyright Musare 2015 - 2023</p>
+					<p>© Copyright Musare 2015 - 2024</p>
 				</div>
 				<router-link id="footer-logo" to="/">
 					<img

+ 9 - 5
frontend/src/components/PlaylistTabBase.vue

@@ -317,10 +317,14 @@ onMounted(() => {
 			orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
 		});
 
-		socket.dispatch("playlists.indexFeaturedPlaylists", res => {
-			if (res.status === "success")
-				featuredPlaylists.value = res.data.playlists;
-		});
+		socket.dispatch(
+			"playlists.indexFeaturedPlaylists",
+			station.value.type === "community",
+			res => {
+				if (res.status === "success")
+					featuredPlaylists.value = res.data.playlists;
+			}
+		);
 
 		if (props.type === "autofill")
 			socket.dispatch(
@@ -1038,7 +1042,7 @@ onMounted(() => {
 														'future',
 														null,
 														true
-												  )} songs from this playlist`
+													)} songs from this playlist`
 												: 'Your preferences are set to skip disliked songs'
 										"
 										v-tippy

+ 7 - 11
frontend/src/components/Queue.vue

@@ -85,15 +85,15 @@ const removeFromQueue = mediaSource => {
 	);
 };
 
-const repositionSongInQueue = ({ moved }) => {
+const repositionSongInQueue = ({ moved, song }) => {
 	const { oldIndex, newIndex } = moved;
 	if (oldIndex === newIndex) return; // we only need to update when song is moved
-	const song = queue.value[newIndex];
+	const _song = song ?? queue.value[newIndex];
 	socket.dispatch(
 		"stations.repositionSongInQueue",
 		station.value._id,
 		{
-			...song,
+			..._song,
 			oldIndex,
 			newIndex
 		},
@@ -111,27 +111,23 @@ const repositionSongInQueue = ({ moved }) => {
 
 const moveSongToTop = index => {
 	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
-	queue.value.splice(0, 0, queue.value.splice(index, 1)[0]);
 	repositionSongInQueue({
 		moved: {
 			oldIndex: index,
 			newIndex: 0
-		}
+		},
+		song: queue.value[index]
 	});
 };
 
 const moveSongToBottom = index => {
 	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
-	queue.value.splice(
-		queue.value.length - 1,
-		0,
-		queue.value.splice(index, 1)[0]
-	);
 	repositionSongInQueue({
 		moved: {
 			oldIndex: index,
 			newIndex: queue.value.length - 1
-		}
+		},
+		song: queue.value[index]
 	});
 };
 

+ 4 - 3
frontend/src/components/SoundcloudPlayer.vue

@@ -2,6 +2,7 @@
 import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import Toast from "toasters";
 import { useSoundcloudPlayer } from "@/composables/useSoundcloudPlayer";
+import { useConfigStore } from "@/stores/config";
 import { useStationStore } from "@/stores/station";
 
 import aw from "@/aw";
@@ -33,6 +34,7 @@ const {
 	soundcloudUnload
 } = useSoundcloudPlayer();
 
+const configStore = useConfigStore();
 const stationStore = useStationStore();
 const { updateMediaModalPlayingAudio } = stationStore;
 
@@ -169,7 +171,7 @@ const drawCanvas = () => {
 
 	const widthCurrentTime = (currentTime / videoDuration) * width;
 
-	const durationColor = "#03A9F4";
+	const durationColor = configStore.primaryColor;
 	const afterDurationColor = "#41E841";
 	const currentDurationColor = "#3b25e8";
 
@@ -425,8 +427,7 @@ onBeforeUnmount(() => {
 
 	.player-container {
 		position: relative;
-		padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
-		height: 0;
+		aspect-ratio: 16/9;
 		overflow: hidden;
 
 		:deep(iframe) {

+ 1 - 1
frontend/src/components/SoundcloudTrackInfo.vue

@@ -64,7 +64,7 @@ defineProps<{
 						track.soundcloudCreatedAt
 							? utils.getDateFormatted(
 									new Date(track.soundcloudCreatedAt)
-							  )
+								)
 							: "Unknown"
 					}}</span
 				>

+ 6 - 5
frontend/src/components/YoutubePlayer.vue

@@ -2,6 +2,7 @@
 import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import Toast from "toasters";
 import { useYoutubePlayer } from "@/composables/useYoutubePlayer";
+import { useConfigStore } from "@/stores/config";
 import { useStationStore } from "@/stores/station";
 
 import aw from "@/aw";
@@ -25,6 +26,7 @@ const {
 	setPlaybackRate: youtubeSetPlaybackRate
 } = useYoutubePlayer();
 
+const configStore = useConfigStore();
 const { updateMediaModalPlayingAudio } = useStationStore();
 
 const interval = ref(null);
@@ -132,7 +134,7 @@ const drawCanvas = () => {
 
 	const widthCurrentTime = (currentTime / videoDuration) * width;
 
-	const durationColor = "#03A9F4";
+	const durationColor = configStore.primaryColor;
 	const afterDurationColor = "#41E841";
 	const currentDurationColor = "#3b25e8";
 
@@ -534,8 +536,8 @@ onBeforeUnmount(() => {
 							youtubePlayer.muted
 								? "volume_mute"
 								: youtubePlayer.volume >= 50
-								? "volume_up"
-								: "volume_down"
+									? "volume_up"
+									: "volume_down"
 						}}</i
 					>
 					<input
@@ -575,8 +577,7 @@ onBeforeUnmount(() => {
 
 	.player-container {
 		position: relative;
-		padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
-		height: 0;
+		aspect-ratio: 16/9;
 		overflow: hidden;
 
 		:deep(iframe) {

+ 8 - 8
frontend/src/components/__snapshots__/Modal.spec.ts.snap

@@ -1,21 +1,21 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`Modal component > renders slots 1`] = `
-"<div class=\\"modal is-active\\">
-  <div class=\\"modal-background\\"></div>
+"<div class="modal is-active">
+  <div class="modal-background"></div>
   <div>Sidebar Slot</div>
-  <div class=\\"modal-card\\">
-    <header class=\\"modal-card-head\\">
+  <div class="modal-card">
+    <header class="modal-card-head">
       <div>Toggle Mobile Sidebar Slot</div>
-      <h2 class=\\"modal-card-title is-marginless\\">Modal</h2><span class=\\"delete material-icons\\">highlight_off</span>
-      <transition-stub appear=\\"false\\" persisted=\\"false\\" css=\\"true\\">
+      <h2 class="modal-card-title is-marginless">Modal</h2><span class="delete material-icons">highlight_off</span>
+      <transition-stub appear="false" persisted="false" css="true">
         <!--v-if-->
       </transition-stub>
     </header>
-    <section class=\\"modal-card-body\\">
+    <section class="modal-card-body">
       <div>Body Slot</div>
     </section>
-    <footer class=\\"modal-card-foot\\">
+    <footer class="modal-card-foot">
       <div>Footer Slot</div>
     </footer>
   </div>

+ 2 - 2
frontend/src/components/modals/ConvertSpotifySongs.vue

@@ -99,7 +99,7 @@ const replaceSongUrlMap = reactive({});
 const showReplacementInputs = ref(false);
 
 const youtubeVideoUrlRegex =
-	/^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;
+	/^(?:https?:\/\/)?(?:www\.)?(m\.)?(?:music\.)?(?:youtube\.com|youtu\.be)\/(?:watch\/?\?v=)?(?:.*&v=)?(?<youtubeId>[\w-]{11}).*$/;
 const youtubeVideoIdRegex = /^([\w-]{11})$/;
 
 const youtubePlaylistUrlRegex = /[\\?&]list=([^&#]*)/;
@@ -113,7 +113,7 @@ const filteredSpotifySongs = computed(() =>
 					(alternativeMediaPerTrack[spotifySong.mediaSource] &&
 						alternativeMediaPerTrack[spotifySong.mediaSource]
 							.mediaSources.length > 0)
-		  )
+			)
 		: spotifySongs.value
 );
 

+ 69 - 11
frontend/src/components/modals/EditPlaylist/Tabs/Settings.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import { onBeforeUnmount, onMounted, watch } from "vue";
+import { ref, onBeforeUnmount, onMounted, watch } from "vue";
 import validation from "@/validation";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
@@ -24,18 +24,27 @@ const { playlist } = storeToRefs(editPlaylistStore);
 
 const { preventCloseUnsaved } = useModalsStore();
 
+const featured = ref(playlist.value.featured);
+
 const isOwner = () =>
 	loggedIn.value && userId.value === playlist.value.createdBy;
 
-const isEditable = permission =>
-	((playlist.value.type === "user" ||
-		playlist.value.type === "user-liked" ||
-		playlist.value.type === "user-disliked" ||
-		playlist.value.type === "admin") &&
-		(isOwner() || hasPermission(permission))) ||
-	(playlist.value.type === "genre" &&
-		permission === "playlists.update.privacy" &&
-		hasPermission(permission));
+const isEditable = permission => {
+	if (permission === "playlists.update.featured")
+		return playlist.value.type !== "station" && hasPermission(permission);
+	if (
+		["user", "user-liked", "user-disliked", "admin"].includes(
+			playlist.value.type
+		)
+	)
+		return isOwner() || hasPermission(permission);
+	if (
+		playlist.value.type === "genre" &&
+		permission === "playlists.update.privacy"
+	)
+		return hasPermission(permission);
+	return false;
+};
 
 const {
 	inputs: displayNameInputs,
@@ -100,6 +109,7 @@ const {
 				values.privacy,
 				res => {
 					playlist.value.privacy = values.privacy;
+					if (values.privacy !== "public") featured.value = false;
 					if (res.status === "success") {
 						resolve();
 						new Toast(res.message);
@@ -117,11 +127,28 @@ const {
 	}
 );
 
+const toggleFeatured = () => {
+	if (playlist.value.privacy !== "public") return;
+	featured.value = !featured.value;
+	socket.dispatch(
+		"playlists.updateFeatured",
+		playlist.value._id,
+		featured.value,
+		res => {
+			playlist.value.featured = featured.value;
+			new Toast(res.message);
+		}
+	);
+};
+
 watch(playlist, (value, oldValue) => {
 	if (value.displayName !== oldValue.displayName)
 		setDisplayName({ displayName: value.displayName });
-	if (value.privacy !== oldValue.privacy)
+	if (value.privacy !== oldValue.privacy) {
 		setPrivacy({ privacy: value.privacy });
+		if (value.privacy !== "public") featured.value = false;
+	}
+	if (value.featured !== oldValue.featured) featured.value = value.featured;
 });
 
 onMounted(() => {
@@ -187,10 +214,41 @@ onBeforeUnmount(() => {
 				</p>
 			</div>
 		</div>
+
+		<div
+			v-if="isEditable('playlists.update.featured')"
+			class="control is-expanded checkbox-control"
+		>
+			<label class="switch">
+				<input
+					type="checkbox"
+					id="featured"
+					:checked="featured"
+					@click="toggleFeatured"
+					:disabled="playlist.privacy !== 'public'"
+				/>
+				<span
+					v-if="playlist.privacy === 'public'"
+					class="slider round"
+				></span>
+				<span
+					v-else
+					class="slider round disabled"
+					content="Only public playlists can be featured"
+					v-tippy
+				></span>
+			</label>
+
+			<label class="label" for="featured">Featured Playlist</label>
+		</div>
 	</div>
 </template>
 
 <style lang="less" scoped>
+.checkbox-control label.label {
+	margin-left: 10px;
+}
+
 @media screen and (max-width: 1300px) {
 	.section {
 		max-width: 100% !important;

+ 12 - 23
frontend/src/components/modals/EditPlaylist/index.vue

@@ -71,7 +71,7 @@ const {
 	addSong,
 	removeSong,
 	replaceSong,
-	repositionedSong
+	reorderSongsList
 } = editPlaylistStore;
 
 const { closeCurrentModal, openModal } = useModalsStore();
@@ -97,52 +97,41 @@ const isEditable = permission =>
 		permission === "playlists.update.privacy" &&
 		hasPermission(permission));
 
-const repositionSong = ({ moved }) => {
+const repositionSong = ({ moved, song }) => {
 	const { oldIndex, newIndex } = moved;
 	if (oldIndex === newIndex) return; // we only need to update when song is moved
-	const song = playlistSongs.value[newIndex];
+	const _song = song ?? playlistSongs.value[newIndex];
 	socket.dispatch(
 		"playlists.repositionSong",
 		playlist.value._id,
 		{
-			...song,
+			..._song,
 			oldIndex,
 			newIndex
 		},
-		res => {
-			if (res.status !== "success")
-				repositionedSong({
-					...song,
-					newIndex: oldIndex,
-					oldIndex: newIndex
-				});
-		}
+		() => {}
 	);
 };
 
 const moveSongToTop = index => {
 	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
-	playlistSongs.value.splice(0, 0, playlistSongs.value.splice(index, 1)[0]);
 	repositionSong({
 		moved: {
 			oldIndex: index,
 			newIndex: 0
-		}
+		},
+		song: playlistSongs.value[index]
 	});
 };
 
 const moveSongToBottom = index => {
 	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
-	playlistSongs.value.splice(
-		playlistSongs.value.length - 1,
-		0,
-		playlistSongs.value.splice(index, 1)[0]
-	);
 	repositionSong({
 		moved: {
 			oldIndex: index,
 			newIndex: playlistSongs.value.length - 1
-		}
+		},
+		song: playlistSongs.value[index]
 	});
 };
 
@@ -320,13 +309,13 @@ onMounted(() => {
 	);
 
 	socket.on(
-		"event:playlist.song.repositioned",
+		"event:playlist.changeOrder",
 		res => {
 			if (playlist.value._id === res.data.playlistId) {
-				const { song, playlistId } = res.data;
+				const { playlistId, playlistOrder } = res.data;
 
 				if (playlist.value._id === playlistId) {
-					repositionedSong(song);
+					reorderSongsList(playlistOrder);
 				}
 			}
 		},

+ 4 - 3
frontend/src/components/modals/EditSong/index.vue

@@ -82,6 +82,7 @@ const {
 const { openModal, closeCurrentModal, preventCloseCbs } = useModalsStore();
 const { hasPermission } = userAuthStore;
 
+// eslint-disable-next-line vue/no-dupe-keys
 const {
 	tab,
 	video,
@@ -732,7 +733,7 @@ const drawCanvas = async () => {
 	const widthCurrentTime = (currentTime / videoDuration) * width;
 
 	const skipDurationColor = "#F42003";
-	const durationColor = "#03A9F4";
+	const durationColor = configStore.primaryColor;
 	const afterDurationColor = "#41E841";
 	const currentDurationColor = "#3b25e8";
 
@@ -2250,8 +2251,8 @@ onBeforeUnmount(() => {
 												muted
 													? "volume_mute"
 													: volumeSliderValue >= 50
-													? "volume_up"
-													: "volume_down"
+														? "volume_up"
+														: "volume_down"
 											}}</i
 										>
 										<input

+ 6 - 5
frontend/src/components/modals/ManageStation/index.vue

@@ -46,6 +46,7 @@ const { socket } = useWebsocketsStore();
 const manageStationStore = useManageStationStore({
 	modalUuid: props.modalUuid
 });
+// eslint-disable-next-line vue/no-dupe-keys
 const {
 	stationId,
 	sector,
@@ -64,7 +65,7 @@ const {
 	clearStation,
 	updateSongsList,
 	updateStationPlaylist,
-	repositionSongInList,
+	reorderSongsList,
 	updateStationPaused,
 	updateCurrentSong,
 	updateStation,
@@ -321,10 +322,10 @@ onMounted(() => {
 		);
 
 		socket.on(
-			"event:manageStation.queue.song.repositioned",
+			"event:manageStation.queue.order.changed",
 			res => {
 				if (res.data.stationId === stationId.value)
-					repositionSongInList(res.data.song);
+					reorderSongsList(res.data.queueOrder);
 			},
 			{ modalUuid: props.modalUuid }
 		);
@@ -482,8 +483,8 @@ onBeforeUnmount(() => {
 			sector === 'home' && !hasPermission('stations.view.manage')
 				? 'View Queue'
 				: !hasPermission('stations.view.manage')
-				? 'Add Song to Queue'
-				: 'Manage Station'
+					? 'Add Song to Queue'
+					: 'Manage Station'
 		"
 		:style="`--primary-color: var(--${station.theme})`"
 		class="manage-station-modal"

Some files were not shown because too many files changed in this diff