Refactor music module: migrate from list to items fields

This commit is contained in:
SrGooglo 2025-04-24 06:10:21 +00:00
parent 3268cb819a
commit 7b7e7b9bb7
14 changed files with 308 additions and 316 deletions

View File

@ -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
}
}
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
}
}

View File

@ -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()
}

View File

@ -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()
}
return await track.save()
}

View File

@ -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(),
}
}
return {
liked: trackLike ? true : false,
track_like_id: trackLike ? trackLike._id : null,
track_id: track._id.toString(),
}
}

View File

@ -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()
}

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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: {

View File

@ -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
}

View File

@ -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
},
}

View File

@ -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
}
}
middlewares: ["withAuthentication"],
fn: async (req) => {
return await ReleaseClass.delete(req.params.release_id, {
user_id: req.auth.session.user_id,
})
},
}

View File

@ -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,
}
},

View File

@ -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
}
}
return track
},
}