diff --git a/packages/music_server/src/api.js b/packages/music_server/src/api.js index 3cc1a8ae..24514000 100755 --- a/packages/music_server/src/api.js +++ b/packages/music_server/src/api.js @@ -12,11 +12,10 @@ import StorageClient from "@classes/StorageClient" import RoomServer from "./roomsServer" - import pkg from "../package.json" export default class Server { - static useMiddlewaresOrder = ["useLogger", "useCors", "useAuth"] + static useMiddlewaresOrder = ["useLogger", "useCors", "useAuth", "useErrorHandler"] eventBus = global.eventBus = new EventEmitter() @@ -166,15 +165,15 @@ export default class Server { await this.storage.initialize() // register controllers & middlewares + this.server.use(express.json({ extended: false })) + this.server.use(express.urlencoded({ extended: true })) + await this.__registerControllers() await this.__registerInternalMiddlewares() await this.__registerInternalRoutes() this.server.use(this.internalRouter) - this.server.use(express.json({ extended: false })) - this.server.use(express.urlencoded({ extended: true })) - await this._http.listen(this.options.listenPort, this.options.listenHost) // calculate elapsed time diff --git a/packages/music_server/src/controllers/lyrics/routes/get/:track_id.js b/packages/music_server/src/controllers/lyrics/routes/get/:track_id.js index 89045a66..4ea93ec6 100644 --- a/packages/music_server/src/controllers/lyrics/routes/get/:track_id.js +++ b/packages/music_server/src/controllers/lyrics/routes/get/:track_id.js @@ -67,7 +67,7 @@ export default async (req, res) => { console.log(track) - if (!track.lyricsEnabled){ + if (!track.lyricsEnabled) { return res.status(403).json({ error: "Lyrics disabled for this track", }) diff --git a/packages/music_server/src/controllers/playlists/get/:playlist_id.js b/packages/music_server/src/controllers/playlists/get/:playlist_id.js deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/music_server/src/controllers/playlists/index.js b/packages/music_server/src/controllers/playlists/index.js index 93eb188c..5599f191 100644 --- a/packages/music_server/src/controllers/playlists/index.js +++ b/packages/music_server/src/controllers/playlists/index.js @@ -1,11 +1,18 @@ import path from "path" import createRoutesFromDirectory from "@utils/createRoutesFromDirectory" +import getMiddlewares from "@utils/getMiddlewares" -export default (router) => { +export default async (router) => { // create a file based router const routesPath = path.resolve(__dirname, "routes") - // router = createRoutesFromDirectory("routes", routesPath, router) + const middlewares = await getMiddlewares(["withOptionalAuth"]) + + for (const middleware of middlewares) { + router.use(middleware) + } + + router = createRoutesFromDirectory("routes", routesPath, router) return { path: "/playlists", diff --git a/packages/music_server/src/controllers/playlists/routes/get/:playlist_id/data.js b/packages/music_server/src/controllers/playlists/routes/get/:playlist_id/data.js new file mode 100644 index 00000000..cc8fa8fb --- /dev/null +++ b/packages/music_server/src/controllers/playlists/routes/get/:playlist_id/data.js @@ -0,0 +1,41 @@ +import { Playlist, Track } from "@models" +import { NotFoundError } from "@classes/Errors" + +export default async (req, res) => { + const { playlist_id } = req.params + + let playlist = await Playlist.findOne({ + _id: playlist_id, + }).catch((err) => { + return false + }) + + playlist = playlist.toObject() + + if (playlist.public === false) { + if (req.session) { + if (req.session.user_id !== playlist.user_id) { + playlist = false + } + } else { + playlist = false + } + } + + if (!playlist) { + return new NotFoundError(req, res, "Playlist not found") + } + + const orderedIds = playlist.list + + playlist.list = await Track.find({ + _id: [...playlist.list], + public: true, + }) + + playlist.list = playlist.list.sort((a, b) => { + return orderedIds.findIndex((id) => id === a._id.toString()) - orderedIds.findIndex((id) => id === b._id.toString()) + }) + + return res.json(playlist) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/playlists/routes/get/search.js b/packages/music_server/src/controllers/playlists/routes/get/search.js new file mode 100644 index 00000000..e78e7130 --- /dev/null +++ b/packages/music_server/src/controllers/playlists/routes/get/search.js @@ -0,0 +1,44 @@ +import { Playlist, Track } from "@models" + +export default async (req, res) => { + const { keywords, limit = 5, offset = 0 } = req.query + + let results = { + playlists: [], + artists: [], + albums: [], + tracks: [], + } + + let searchQuery = { + public: true, + } + + if (keywords) { + searchQuery = { + ...searchQuery, + title: { + $regex: keywords, + $options: "i", + }, + } + } + + let playlists = await Playlist.find(searchQuery) + .limit(limit) + .skip(offset) + + if (playlists) { + results.playlists = playlists + } + + let tracks = await Track.find(searchQuery) + .limit(limit) + .skip(offset) + + if (tracks) { + results.tracks = tracks + } + + return res.json(results) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/playlists/routes/get/self.js b/packages/music_server/src/controllers/playlists/routes/get/self.js new file mode 100644 index 00000000..0a32bfa1 --- /dev/null +++ b/packages/music_server/src/controllers/playlists/routes/get/self.js @@ -0,0 +1,46 @@ +import { Playlist, Track } from "@models" +import { AuthorizationError, NotFoundError } from "@classes/Errors" + +export default async (req, res) => { + if (!req.session) { + return new AuthorizationError(req, res) + } + + const { keywords, limit = 10, offset = 0 } = req.query + const user_id = req.session.user_id.toString() + + let searchQuery = { + user_id, + } + + if (keywords) { + searchQuery = { + ...searchQuery, + title: { + $regex: keywords, + $options: "i", + }, + } + } + + let playlists = await Playlist.find(searchQuery) + .catch((err) => false) + //.limit(limit) + //.skip(offset) + + if (!playlists) { + return new NotFoundError("Playlists not found") + } + + playlists = await Promise.all(playlists.map(async (playlist) => { + playlist.list = await Track.find({ + _id: [ + ...playlist.list, + ] + }) + + return playlist + })) + + return res.json(playlists) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/playlists/routes/put/playlist.js b/packages/music_server/src/controllers/playlists/routes/put/playlist.js new file mode 100644 index 00000000..a7087326 --- /dev/null +++ b/packages/music_server/src/controllers/playlists/routes/put/playlist.js @@ -0,0 +1,131 @@ +import { Playlist, Track } from "@models" +import { AuthorizationError, NotFoundError, PermissionError, BadRequestError } from "@classes/Errors" + +const PlaylistAllowedUpdateFields = [ + "title", + "cover", + "album", + "artist", + "description", + "public", +] + +const TrackAllowedUpdateFields = [ + "title", + "album", + "artist", + "cover", + "explicit", + "metadata", + "public", + "spotifyId", + "lyricsEnabled", + "public", +] + +async function createOrUpdateTrack(payload) { + if (!payload.title || !payload.source || !payload.user_id) { + throw new Error("title and source and user_id are required") + } + + let track = null + + if (payload._id) { + track = await Track.findById(payload._id) + + if (!track) { + throw new Error("track not found") + } + + TrackAllowedUpdateFields.forEach((field) => { + if (typeof payload[field] !== "undefined") { + track[field] = payload[field] + } + }) + + track = await Track.findByIdAndUpdate(payload._id, track) + + if (!track) { + throw new Error("Failed to update track") + } + } else { + track = new Track(payload) + + await track.save() + } + + return track +} + +export default async (req, res) => { + if (!req.session) { + return new AuthorizationError(req, res) + } + + if (!req.body.title || !req.body.list) { + return new BadRequestError(req, res, "title and list are required") + } + + if (!Array.isArray(req.body.list)) { + return new BadRequestError(req, res, "list must be an array") + } + + let playlist = null + + if (!req.body._id) { + playlist = new Playlist({ + user_id: req.session.user_id.toString(), + created_at: Date.now(), + title: req.body.title ?? "Untitled", + description: req.body.description, + cover: req.body.cover, + explicit: req.body.explicit, + public: req.body.public, + list: req.body.list, + }) + + await playlist.save() + } else { + playlist = await Playlist.findById(req.body._id) + } + + if (!playlist) { + return new NotFoundError(req, res, "Playlist not found") + } + + if (playlist.user_id !== req.session.user_id.toString()) { + return new PermissionError(req, res, "You don't have permission to edit this playlist") + } + + playlist = playlist.toObject() + + playlist.list = await Promise.all(req.body.list.map(async (track, index) => { + if (typeof track !== "object") { + return track + } + + track.user_id = req.session.user_id.toString() + + const result = await createOrUpdateTrack(track) + + if (result) { + return result._id.toString() + } + })) + + PlaylistAllowedUpdateFields.forEach((field) => { + if (typeof req.body[field] !== "undefined") { + playlist[field] = req.body[field] + } + }) + + playlist = await Playlist.findByIdAndUpdate(req.body._id, playlist) + + if (!playlist) { + return new NotFoundError(req, res, "Playlist not updated") + } + + global.eventBus.emit(`playlist.${playlist._id}.updated`, playlist) + + return res.json(playlist) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/playlists/services/getTrackById.js b/packages/music_server/src/controllers/playlists/services/getTrackById.js new file mode 100644 index 00000000..5f45ec4d --- /dev/null +++ b/packages/music_server/src/controllers/playlists/services/getTrackById.js @@ -0,0 +1,19 @@ +import { Track } from "@models" + +export default async (_id) => { + if (!_id) { + throw new Error("Missing _id") + } + + let track = await Track.findById(_id).catch((err) => false) + + if (!track) { + throw new Error("Track not found") + } + + track = track.toObject() + + track.artist = track.artist ?? "Unknown artist" + + return track +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/tracks/index.js b/packages/music_server/src/controllers/tracks/index.js new file mode 100644 index 00000000..bbbbbf47 --- /dev/null +++ b/packages/music_server/src/controllers/tracks/index.js @@ -0,0 +1,21 @@ +import path from "path" +import createRoutesFromDirectory from "@utils/createRoutesFromDirectory" +import getMiddlewares from "@utils/getMiddlewares" + +export default async (router) => { + // create a file based router + const routesPath = path.resolve(__dirname, "routes") + + const middlewares = await getMiddlewares(["withOptionalAuth"]) + + for (const middleware of middlewares) { + router.use(middleware) + } + + router = createRoutesFromDirectory("routes", routesPath, router) + + return { + path: "/tracks", + router, + } +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/tracks/routes/get/:track_id/data.js b/packages/music_server/src/controllers/tracks/routes/get/:track_id/data.js new file mode 100644 index 00000000..23e0f368 --- /dev/null +++ b/packages/music_server/src/controllers/tracks/routes/get/:track_id/data.js @@ -0,0 +1,19 @@ +import { Track } from "@models" +import { NotFoundError } from "@classes/Errors" + +export default async (req, res) => { + const { track_id } = req.params + + let track = await Track.findOne({ + _id: track_id, + public: true, + }).catch((err) => { + return null + }) + + if (!track) { + return new NotFoundError(req, res, "Track not found") + } + + return res.json(track) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/tracks/routes/get/:track_id/stream.js b/packages/music_server/src/controllers/tracks/routes/get/:track_id/stream.js new file mode 100644 index 00000000..ed90a22b --- /dev/null +++ b/packages/music_server/src/controllers/tracks/routes/get/:track_id/stream.js @@ -0,0 +1,43 @@ +import { Track } from "@models" +import { NotFoundError, InternalServerError } from "@classes/Errors" + +import mimetypes from "mime-types" + +export default async (req, res) => { + const { track_id } = req.params + + let track = await Track.findOne({ + _id: track_id, + public: true, + }).catch((err) => { + return null + }) + + if (!track) { + return new NotFoundError(req, res, "Track not found") + } + + track = track.toObject() + + if (typeof track.stream_source === "undefined") { + return new NotFoundError(req, res, "Track doesn't have stream source") + } + + global.storage.getObject(process.env.S3_BUCKET, `tracks/${track.stream_source}`, (err, dataStream) => { + if (err) { + console.error(err) + return new InternalServerError(req, res, "Error while getting file from storage") + } + + const extname = mimetypes.lookup(track.stream_source) + + // send chunked response + res.status(200) + + // set headers + res.setHeader("Content-Type", extname) + res.setHeader("Accept-Ranges", "bytes") + + return dataStream.pipe(res) + }) +} \ No newline at end of file diff --git a/packages/music_server/src/controllers/tracks/routes/get/many.js b/packages/music_server/src/controllers/tracks/routes/get/many.js new file mode 100644 index 00000000..c809df76 --- /dev/null +++ b/packages/music_server/src/controllers/tracks/routes/get/many.js @@ -0,0 +1,23 @@ +import { Track } from "@models" + +export default async (req, res) => { + const { ids, limit = 20, offset = 0 } = req.query + + if (!ids) { + return res.status(400).json({ + message: "IDs is required", + }) + } + + let tracks = await Track.find({ + _id: [...ids], + public: true, + }) + .limit(limit) + .skip(offset) + .catch((err) => { + return [] + }) + + return res.json(tracks) +} \ No newline at end of file diff --git a/packages/music_server/src/models/track/index.js b/packages/music_server/src/models/track/index.js index 3179d131..583ce84e 100755 --- a/packages/music_server/src/models/track/index.js +++ b/packages/music_server/src/models/track/index.js @@ -2,10 +2,6 @@ export default { name: "Track", collection: "tracks", schema: { - user_id: { - type: String, - required: true, - }, title: { type: String, required: true, @@ -44,6 +40,10 @@ export default { lyricsEnabled: { type: Boolean, default: true, - } + }, + publisher: { + type: Object, + required: true, + }, } } \ No newline at end of file diff --git a/packages/music_server/src/services/getTrackById.js b/packages/music_server/src/services/getTrackById.js deleted file mode 100644 index b4c8e973..00000000 --- a/packages/music_server/src/services/getTrackById.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Track, User } from "@models" - -export default async (_id) => { - if (!_id) { - throw new Error("Missing _id") - } - - let track = await Track.findById(_id).catch((err) => false) - - if (!track) { - throw new Error("Track not found") - } - - track = track.toObject() - - if (!track.metadata) { - // TODO: Get metadata from source - } - - const userData = await User.findById(track.user_id).catch((err) => false) - - track.artist = track.artist ?? userData?.fullName ?? userData?.username ?? "Unknown artist" - - return track -} \ No newline at end of file diff --git a/packages/music_server/src/useMiddlewares/useLogger/index.js b/packages/music_server/src/useMiddlewares/useLogger/index.js index 5e9c64f4..1d398fc1 100644 --- a/packages/music_server/src/useMiddlewares/useLogger/index.js +++ b/packages/music_server/src/useMiddlewares/useLogger/index.js @@ -7,6 +7,11 @@ export default (req, res, next) => { res._responseTimeMs = elapsedTimeInMs + // cut req.url if is too long + if (req.url.length > 100) { + req.url = req.url.substring(0, 100) + "..." + } + console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`) }) diff --git a/packages/music_server/src/utils/createRoutesFromDirectory/index.js b/packages/music_server/src/utils/createRoutesFromDirectory/index.js index e3b30e1e..04681968 100644 --- a/packages/music_server/src/utils/createRoutesFromDirectory/index.js +++ b/packages/music_server/src/utils/createRoutesFromDirectory/index.js @@ -1,5 +1,24 @@ import fs from "fs" +function createRouteHandler(route, fn) { + if (typeof route !== "string") { + fn = route + route = "Unknown route" + } + + return async (req, res) => { + try { + await fn(req, res) + } catch (error) { + console.error(`[ERROR] (${route}) >`, error) + + return res.status(500).json({ + error: error.message, + }) + } + } +} + function createRoutesFromDirectory(startFrom, directoryPath, router) { const files = fs.readdirSync(directoryPath) @@ -39,7 +58,7 @@ function createRoutesFromDirectory(startFrom, directoryPath, router) { handler = handler.default || handler - router[method](route, handler) + router[method](route, createRouteHandler(route, handler)) router.routes.push({ method,