diff --git a/packages/server/services/music/classes/release/index.js b/packages/server/services/music/classes/release/index.js index d1c10676..b4740bbb 100644 --- a/packages/server/services/music/classes/release/index.js +++ b/packages/server/services/music/classes/release/index.js @@ -1,86 +1,144 @@ -import { MusicRelease, User } from "@db_models" +import { MusicRelease, Track } from "@db_models" +import TrackClass from "../track" const AllowedUpdateFields = [ - "title", - "cover", - "album", - "artist", - "type", - "public", - "list", + "title", + "cover", + "album", + "artist", + "type", + "public", + "items", ] export default class Release { - static async create(payload) { - console.log(payload) - if (!payload.title) { - throw new OperationError(400, "Release title is required") - } + // TODO: implement pagination + static async data(id, { user_id = null, limit = 10, offset = 0 } = {}) { + let release = await MusicRelease.findOne({ + _id: id, + }) - if (!payload.list) { - throw new OperationError(400, "Release list is required") - } + if (!release) { + throw new OperationError(404, "Release not found") + } - // ensure list is an array of strings with tracks ids only - payload.list = payload.list.map((item) => { - if (typeof item !== "string") { - item = item._id - } + release = release.toObject() - return item - }) + const items = release.items ?? release.list - const release = new MusicRelease({ - user_id: payload.user_id, - created_at: Date.now(), - title: payload.title, - cover: payload.cover, - explicit: payload.explicit, - type: payload.type, - public: payload.public, - list: payload.list, - public: payload.public, - }) + const totalTracks = await Track.countDocuments({ + _id: items, + }) - await release.save() + const tracks = await TrackClass.get(items, { + user_id: user_id, + onlyList: true, + }) - return release - } + release.total_items = totalTracks + release.items = tracks - static async update(id, payload) { - let release = await MusicRelease.findById(id).catch((err) => { - return false - }) + return release + } - if (!release) { - throw new OperationError(404, "Release not found") - } + static async create(payload) { + if (!payload.title) { + throw new OperationError(400, "Release title is required") + } - if (release.user_id !== payload.user_id) { - throw new PermissionError(403, "You dont have permission to edit this release") - } + if (!payload.items) { + throw new OperationError(400, "Release items is required") + } - for (const field of AllowedUpdateFields) { - if (payload[field]) { - release[field] = payload[field] - } - } + // ensure list is an array of strings with tracks ids only + payload.items = payload.items.map((item) => { + return item._id ?? item + }) - // ensure list is an array of strings with tracks ids only - release.list = release.list.map((item) => { - if (typeof item !== "string") { - item = item._id - } + const release = new MusicRelease({ + user_id: payload.user_id, + created_at: Date.now(), + title: payload.title, + cover: payload.cover, + explicit: payload.explicit, + type: payload.type, + public: payload.public, + items: payload.items, + public: payload.public, + }) - return item - }) + await release.save() - release = await MusicRelease.findByIdAndUpdate(id, release) + return release + } - return release - } + static async update(id, payload) { + let release = await MusicRelease.findById(id).catch((err) => { + return false + }) - static async fullfillItemData(release) { - return release - } -} \ No newline at end of file + if (!release) { + throw new OperationError(404, "Release not found") + } + + if (release.user_id !== payload.user_id) { + throw new PermissionError( + 403, + "You dont have permission to edit this release", + ) + } + + for (const field of AllowedUpdateFields) { + if (typeof payload[field] !== "undefined") { + release[field] = payload[field] + } + } + + // ensure list is an array of strings with tracks ids only + release.items = release.items.map((item) => { + return item._id ?? item + }) + + await MusicRelease.findByIdAndUpdate(id, release) + + return release + } + + static async delete(id, payload = {}) { + let release = await MusicRelease.findById(id).catch((err) => { + return false + }) + + if (!release) { + throw new OperationError(404, "Release not found") + } + + // check permission + if (release.user_id !== payload.user_id) { + throw new PermissionError( + 403, + "You dont have permission to edit this release", + ) + } + + const items = release.items ?? release.list + + const items_ids = items.map((item) => item._id) + + // delete all releated tracks + await Track.deleteMany({ + _id: { $in: items_ids }, + }) + + // delete release + await MusicRelease.deleteOne({ + _id: id, + }) + + return release + } + + static async fullfillItemData(release) { + return release + } +} diff --git a/packages/server/services/music/classes/track/methods/create.js b/packages/server/services/music/classes/track/methods/create.js index 2faca6aa..86d7daae 100644 --- a/packages/server/services/music/classes/track/methods/create.js +++ b/packages/server/services/music/classes/track/methods/create.js @@ -1,58 +1,52 @@ import { Track } from "@db_models" import requiredFields from "@shared-utils/requiredFields" -import MusicMetadata from "music-metadata" -import axios from "axios" +import * as FFMPEGLib from "@shared-classes/FFMPEGLib" import ModifyTrack from "./modify" export default async (payload = {}) => { - requiredFields(["title", "source", "user_id"], payload) + if (typeof payload.title !== "string") { + payload.title = undefined + } - let stream = null - let headers = null + if (typeof payload.album !== "string") { + payload.album = undefined + } + + if (typeof payload.artist !== "string") { + payload.artist = undefined + } + + if (typeof payload.cover !== "string") { + payload.cover = undefined + } + + if (typeof payload.source !== "string") { + payload.source = undefined + } + + if (typeof payload.user_id !== "string") { + payload.user_id = undefined + } + + requiredFields(["title", "source", "user_id"], payload) if (typeof payload._id === "string") { return await ModifyTrack(payload._id, payload) } - let metadata = Object() + const probe = await FFMPEGLib.Utils.probe(payload.source) - try { - const sourceStream = await axios({ - url: payload.source, - method: "GET", - responseType: "stream", - }) - - stream = sourceStream.data - headers = sourceStream.headers - - const streamMetadata = await MusicMetadata.parseStream(stream, { - mimeType: headers["content-type"], - }) - - metadata = { - ...metadata, - format: streamMetadata.format.codec, - channels: streamMetadata.format.numberOfChannels, - sampleRate: streamMetadata.format.sampleRate, - bits: streamMetadata.format.bitsPerSample, - lossless: streamMetadata.format.lossless, - duration: streamMetadata.format.duration, - - title: streamMetadata.common.title, - artists: streamMetadata.common.artists, - album: streamMetadata.common.album, - } - } catch (error) { - // sowy :( - } - - if (typeof payload.metadata === "object") { - metadata = { - ...metadata, - ...payload.metadata, - } + let metadata = { + format: probe.streams[0].codec_name, + channels: probe.streams[0].channels, + bitrate: probe.streams[0].bit_rate ?? probe.format.bit_rate, + sampleRate: probe.streams[0].sample_rate, + bits: + probe.streams[0].bits_per_sample ?? + probe.streams[0].bits_per_raw_sample, + duration: probe.format.duration, + tags: probe.format.tags ?? {}, } if (metadata.format) { @@ -68,53 +62,28 @@ export default async (payload = {}) => { } const obj = { - title: payload.title, - album: payload.album, - cover: payload.cover, - artists: [], + title: payload.title ?? metadata.tags["Title"], + album: payload.album ?? metadata.tags["Album"], + artist: payload.artist ?? metadata.tags["Artist"], + cover: + payload.cover ?? + "https://storage.ragestudio.net/comty-static-assets/default_song.png", source: payload.source, metadata: metadata, - lyrics_enabled: payload.lyrics_enabled, } if (Array.isArray(payload.artists)) { - obj.artists = payload.artists + obj.artist = payload.artists.join(", ") } - if (typeof payload.artists === "string") { - obj.artists.push(payload.artists) - } + let track = new Track({ + ...obj, + publisher: { + user_id: payload.user_id, + }, + }) - if (typeof payload.artist === "string") { - obj.artists.push(payload.artist) - } + await track.save() - if (obj.artists.length === 0 || !obj.artists) { - obj.artists = metadata.artists - } - - let track = null - - if (payload._id) { - track = await Track.findById(payload._id) - - if (!track) { - throw new OperationError(404, "Track not found, cannot update") - } - - throw new OperationError(501, "Not implemented") - } else { - track = new Track({ - ...obj, - publisher: { - user_id: payload.user_id, - }, - }) - - await track.save() - } - - track = track.toObject() - - return track + return track.toObject() } diff --git a/packages/server/services/music/classes/track/methods/modify.js b/packages/server/services/music/classes/track/methods/modify.js index d8714f91..ba7eea50 100644 --- a/packages/server/services/music/classes/track/methods/modify.js +++ b/packages/server/services/music/classes/track/methods/modify.js @@ -1,25 +1,32 @@ import { Track } from "@db_models" +const allowedFields = ["title", "artist", "album", "cover"] + export default async (track_id, payload) => { - if (!track_id) { - throw new OperationError(400, "Missing track_id") - } + if (!track_id) { + throw new OperationError(400, "Missing track_id") + } - const track = await Track.findById(track_id) + const track = await Track.findById(track_id) - if (!track) { - throw new OperationError(404, "Track not found") - } + if (!track) { + throw new OperationError(404, "Track not found") + } - if (track.publisher.user_id !== payload.user_id) { - throw new PermissionError(403, "You dont have permission to edit this track") - } + if (track.publisher.user_id !== payload.user_id) { + throw new PermissionError( + 403, + "You dont have permission to edit this track", + ) + } - for (const field of Object.keys(payload)) { - track[field] = payload[field] - } + for (const field of allowedFields) { + if (payload[field] !== undefined) { + track[field] = payload[field] + } + } - track.modified_at = Date.now() + track.modified_at = Date.now() - return await track.save() -} \ No newline at end of file + return await track.save() +} diff --git a/packages/server/services/music/classes/track/methods/toggleFavourite.js b/packages/server/services/music/classes/track/methods/toggleFavourite.js index 3f60fff9..4c7051a3 100644 --- a/packages/server/services/music/classes/track/methods/toggleFavourite.js +++ b/packages/server/services/music/classes/track/methods/toggleFavourite.js @@ -1,62 +1,65 @@ import { Track, TrackLike } from "@db_models" export default async (user_id, track_id, to) => { - if (!user_id) { - throw new OperationError(400, "Missing user_id") - } + if (!user_id) { + throw new OperationError(400, "Missing user_id") + } - if (!track_id) { - throw new OperationError(400, "Missing track_id") - } + if (!track_id) { + throw new OperationError(400, "Missing track_id") + } - const track = await Track.findById(track_id) + const track = await Track.findById(track_id) - if (!track) { - throw new OperationError(404, "Track not found") - } + if (!track) { + throw new OperationError(404, "Track not found") + } - let trackLike = await TrackLike.findOne({ - user_id: user_id, - track_id: track_id, - }).catch(() => null) + let trackLike = await TrackLike.findOne({ + user_id: user_id, + track_id: track_id, + }).catch(() => null) - if (typeof to === "undefined") { - to = !!!trackLike - } + if (typeof to === "undefined") { + to = !!!trackLike + } - if (to) { - if (!trackLike) { - trackLike = new TrackLike({ - user_id: user_id, - track_id: track_id, - created_at: Date.now(), - }) + if (to) { + if (!trackLike) { + trackLike = new TrackLike({ + user_id: user_id, + track_id: track_id, + created_at: Date.now(), + }) - await trackLike.save() - } - } else { - if (trackLike) { - await TrackLike.deleteOne({ - user_id: user_id, - track_id: track_id, - }) + await trackLike.save() + } + } else { + if (trackLike) { + await TrackLike.deleteOne({ + user_id: user_id, + track_id: track_id, + }) - trackLike = null - } - } + trackLike = null + } + } - const targetSocket = await global.websocket.find.socketByUserId(user_id) + if (global.websockets) { + const targetSocket = + await global.websockets.find.clientsByUserId(user_id) - if (targetSocket) { - await targetSocket.emit("music:track:toggle:like", { - track_id: track_id, - action: trackLike ? "liked" : "unliked" - }) - } + if (targetSocket) { + await targetSocket.emit("music:track:toggle:like", { + track_id: track_id, + action: trackLike ? "liked" : "unliked", + }) + } + } - return { - liked: trackLike ? true : false, - track_like_id: trackLike ? trackLike._id : null, - track_id: track._id.toString(), - } -} \ No newline at end of file + return { + liked: trackLike ? true : false, + track_like_id: trackLike ? trackLike._id : null, + track_id: track._id.toString(), + } +} diff --git a/packages/server/services/music/music.service.js b/packages/server/services/music/music.service.js index 4e24f38a..15f1fec2 100755 --- a/packages/server/services/music/music.service.js +++ b/packages/server/services/music/music.service.js @@ -2,12 +2,14 @@ import { Server } from "linebridge" import DbManager from "@shared-classes/DbManager" import SSEManager from "@shared-classes/SSEManager" +import RedisClient from "@shared-classes/RedisClient" import SharedMiddlewares from "@shared-middlewares" import LimitsClass from "@shared-classes/Limits" export default class API extends Server { static refName = "music" + static useEngine = "hyper-express-ng" static enableWebsockets = true static routesPath = `${__dirname}/routes` static listen_port = process.env.HTTP_LISTEN_PORT ?? 3003 @@ -19,12 +21,15 @@ export default class API extends Server { contexts = { db: new DbManager(), SSEManager: new SSEManager(), + redis: RedisClient(), } async onInitialize() { global.sse = this.contexts.SSEManager + global.redis = this.contexts.redis.client await this.contexts.db.initialize() + await this.contexts.redis.initialize() this.contexts.limits = await LimitsClass.get() } diff --git a/packages/server/services/music/package.json b/packages/server/services/music/package.json index e230c3e0..f114a35f 100755 --- a/packages/server/services/music/package.json +++ b/packages/server/services/music/package.json @@ -1,9 +1,9 @@ { - "name": "music", - "version": "0.60.2", - "dependencies": { - "ms": "^2.1.3", - "music-metadata": "^7.14.0", - "openai": "^4.47.2" - } + "name": "music", + "version": "0.60.2", + "dependencies": { + "ms": "^2.1.3", + "music-metadata": "^7.14.0", + "openai": "^4.47.2" + } } diff --git a/packages/server/services/music/routes/music/radio/[radio_id]/get.js b/packages/server/services/music/routes/music/radio/[radio_id]/get.js index 90969443..4fc39c16 100644 --- a/packages/server/services/music/routes/music/radio/[radio_id]/get.js +++ b/packages/server/services/music/routes/music/radio/[radio_id]/get.js @@ -1,9 +1,7 @@ export default async (req, res) => { const radioId = req.params.radio_id - let redisData = await global.websocket.redis - .hgetall(`radio-${radioId}`) - .catch(() => null) + let redisData = await redis.hgetall(`radio-${radioId}`).catch(() => null) return redisData } diff --git a/packages/server/services/music/routes/music/radio/list/get.js b/packages/server/services/music/routes/music/radio/list/get.js index c10a069c..186bf1ed 100644 --- a/packages/server/services/music/routes/music/radio/list/get.js +++ b/packages/server/services/music/routes/music/radio/list/get.js @@ -1,19 +1,13 @@ import { RadioProfile } from "@db_models" async function scanKeysWithPagination(pattern, count = 10, cursor = "0") { - const result = await global.websocket.redis.scan( - cursor, - "MATCH", - pattern, - "COUNT", - count, - ) + const result = await redis.scan(cursor, "MATCH", pattern, "COUNT", count) return result[1] } async function getHashData(hashKey) { - const hashData = await global.websocket.redis.hgetall(hashKey) + const hashData = await redis.hgetall(hashKey) return hashData } diff --git a/packages/server/services/music/routes/music/radio/sse/[channel_id]/get.js b/packages/server/services/music/routes/music/radio/sse/[channel_id]/get.js index 6fc963bb..5e1be783 100644 --- a/packages/server/services/music/routes/music/radio/sse/[channel_id]/get.js +++ b/packages/server/services/music/routes/music/radio/sse/[channel_id]/get.js @@ -3,9 +3,7 @@ export default async (req, res) => { const radioId = channel_id.split("radio:")[1] - let redisData = await global.websocket.redis - .hgetall(`radio-${radioId}`) - .catch(() => null) + let redisData = await redis.hgetall(`radio-${radioId}`).catch(() => null) global.sse.connectToChannelStream(channel_id, req, res, { initialData: { diff --git a/packages/server/services/music/routes/music/radio/webhook/post.js b/packages/server/services/music/routes/music/radio/webhook/post.js index 0a44b1d9..24827732 100644 --- a/packages/server/services/music/routes/music/radio/webhook/post.js +++ b/packages/server/services/music/routes/music/radio/webhook/post.js @@ -63,20 +63,17 @@ export default async (req) => { const redis_id = `radio-${data.radio_id}` - const existMember = await global.websocket.redis.hexists( - redis_id, - "radio_id", - ) + const existMember = await redis.hexists(redis_id, "radio_id") if (data.online) { - await global.websocket.redis.hset(redis_id, { + await redis.hset(redis_id, { ...data, now_playing: JSON.stringify(data.now_playing), }) } if (!data.online && existMember) { - await global.websocket.redis.hdel(redis_id) + await redis.hdel(redis_id) } console.log(`[${data.radio_id}] Updating radio data`) @@ -85,7 +82,6 @@ export default async (req) => { event: "update", data: data, }) - global.websocket.io.to(`radio:${data.radio_id}`).emit(`update`, data) return data } diff --git a/packages/server/services/music/routes/music/releases/[release_id]/data/get.js b/packages/server/services/music/routes/music/releases/[release_id]/data/get.js index 257daebc..4e0caca5 100644 --- a/packages/server/services/music/routes/music/releases/[release_id]/data/get.js +++ b/packages/server/services/music/routes/music/releases/[release_id]/data/get.js @@ -1,5 +1,4 @@ -import { MusicRelease, Track } from "@db_models" -import TrackClass from "@classes/track" +import ReleaseClass from "@classes/release" export default { middlewares: ["withOptionalAuthentication"], @@ -7,29 +6,10 @@ export default { const { release_id } = req.params const { limit = 50, offset = 0 } = req.query - let release = await MusicRelease.findOne({ - _id: release_id, - }) - - if (!release) { - throw new OperationError(404, "Release not found") - } - - release = release.toObject() - - const totalTracks = await Track.countDocuments({ - _id: release.list, - }) - - const tracks = await TrackClass.get(release.list, { + return await ReleaseClass.data(release_id, { user_id: req.auth?.session?.user_id, - onlyList: true, + limit, + offset, }) - - release.listLength = totalTracks - release.items = tracks - release.list = tracks - - return release }, } diff --git a/packages/server/services/music/routes/music/releases/[release_id]/delete.js b/packages/server/services/music/routes/music/releases/[release_id]/delete.js index 4b7adb5b..30a40b86 100644 --- a/packages/server/services/music/routes/music/releases/[release_id]/delete.js +++ b/packages/server/services/music/routes/music/releases/[release_id]/delete.js @@ -1,26 +1,10 @@ -import { MusicRelease, Track } from "@db_models" +import ReleaseClass from "@classes/release" export default { - middlewares: ["withAuthentication"], - fn: async (req) => { - const { release_id } = req.params - - let release = await MusicRelease.findOne({ - _id: release_id - }) - - if (!release) { - throw new OperationError(404, "Release not found") - } - - if (release.user_id !== req.auth.session.user_id) { - throw new OperationError(403, "Unauthorized") - } - - await MusicRelease.deleteOne({ - _id: release_id - }) - - return release - } -} \ No newline at end of file + middlewares: ["withAuthentication"], + fn: async (req) => { + return await ReleaseClass.delete(req.params.release_id, { + user_id: req.auth.session.user_id, + }) + }, +} diff --git a/packages/server/services/music/routes/music/releases/self/get.js b/packages/server/services/music/routes/music/releases/self/get.js index 55eacba2..4c696b69 100644 --- a/packages/server/services/music/routes/music/releases/self/get.js +++ b/packages/server/services/music/routes/music/releases/self/get.js @@ -29,7 +29,7 @@ export default { if (req.query.resolveItemsData === "true") { releases = await Promise.all( playlists.map(async (playlist) => { - playlist.items = await Track.find({ + playlist.list = await Track.find({ _id: [...playlist.list], }) @@ -39,7 +39,7 @@ export default { } return { - total_length: await MusicRelease.countDocuments(searchQuery), + total_items: await MusicRelease.countDocuments(searchQuery), items: releases, } }, diff --git a/packages/server/services/music/routes/music/tracks/put.js b/packages/server/services/music/routes/music/tracks/put.js index d7720e47..415bb16a 100644 --- a/packages/server/services/music/routes/music/tracks/put.js +++ b/packages/server/services/music/routes/music/tracks/put.js @@ -2,36 +2,36 @@ import requiredFields from "@shared-utils/requiredFields" import TrackClass from "@classes/track" export default { - middlewares: ["withAuthentication"], - fn: async (req) => { - if (Array.isArray(req.body.list)) { - let results = [] + middlewares: ["withAuthentication"], + fn: async (req) => { + if (Array.isArray(req.body.items)) { + let results = [] - for await (const item of req.body.list) { - if (!item.source || !item.title) { - continue - } + for await (const item of req.body.items) { + if (!item.source || !item.title) { + continue + } - const track = await TrackClass.create({ - ...item, - user_id: req.auth.session.user_id, - }) + const track = await TrackClass.create({ + ...item, + user_id: req.auth.session.user_id, + }) - results.push(track) - } + results.push(track) + } - return { - list: results - } - } + return { + items: results, + } + } - requiredFields(["title", "source"], req.body) + requiredFields(["title", "source"], req.body) - const track = await TrackClass.create({ - ...req.body, - user_id: req.auth.session.user_id, - }) + const track = await TrackClass.create({ + ...req.body, + user_id: req.auth.session.user_id, + }) - return track - } -} \ No newline at end of file + return track + }, +}