mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-18 06:54:15 +00:00
Add playlist class with CRUD methods and update lyrics handling
- Implement Playlist class with create, modify, delete, appendItem, and removeItem - Refactor lyrics endpoints to use video_starts_at instead of sync_audio_at - Improve LRC parsing and timing logic for synced lyrics - Fix track and release data ordering and assignment - Remove unused imports and minor code cleanup
This commit is contained in:
parent
f2cb816b21
commit
582790ba88
@ -1,5 +1,4 @@
|
|||||||
import { Track, Playlist, MusicRelease } from "@db_models"
|
import { Track, Playlist, MusicRelease } from "@db_models"
|
||||||
import { MusicLibraryItem } from "@db_models"
|
|
||||||
|
|
||||||
import toggleFavorite from "./methods/toggleFavorite"
|
import toggleFavorite from "./methods/toggleFavorite"
|
||||||
import getUserLibrary from "./methods/getUserLibrary"
|
import getUserLibrary from "./methods/getUserLibrary"
|
||||||
|
@ -121,6 +121,7 @@ async function fetchAllKindsData(userId, limit, offsetStr) {
|
|||||||
const actualItems = await Model.find({
|
const actualItems = await Model.find({
|
||||||
_id: { $in: itemIds },
|
_id: { $in: itemIds },
|
||||||
}).lean()
|
}).lean()
|
||||||
|
|
||||||
const actualItemsMap = new Map(
|
const actualItemsMap = new Map(
|
||||||
actualItems.map((item) => [item._id.toString(), item]),
|
actualItems.map((item) => [item._id.toString(), item]),
|
||||||
)
|
)
|
||||||
|
17
packages/server/services/music/classes/playlist/index.js
Normal file
17
packages/server/services/music/classes/playlist/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import getData from "./methods/getData"
|
||||||
|
import create from "./methods/create"
|
||||||
|
import modify from "./methods/modify"
|
||||||
|
import deletePlaylist from "./methods/deletePlaylist"
|
||||||
|
|
||||||
|
import appendItem from "./methods/appendItem"
|
||||||
|
import removeItem from "./methods/removeItem"
|
||||||
|
|
||||||
|
export default class Playlist {
|
||||||
|
static get = getData
|
||||||
|
static create = create
|
||||||
|
static modify = modify
|
||||||
|
static delete = deletePlaylist
|
||||||
|
|
||||||
|
static appendItem = appendItem
|
||||||
|
static removeItem = removeItem
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (id, item) => {
|
||||||
|
let playlist = await Playlist.findById(id).lean()
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
throw new OperationError(404, "Playlist not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item === "string" && !Array.isArray(item)) {
|
||||||
|
item = [item]
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist.items = [...playlist.items, ...item]
|
||||||
|
|
||||||
|
return await Playlist.findByIdAndUpdate(id, playlist)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (payload) => {
|
||||||
|
let playlist = await Playlist.create(playlist)
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (id) => {
|
||||||
|
let playlist = await Playlist.findById(id)
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
throw new OperationError(404, "Playlist not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Playlist.findByIdAndDelete(id)
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (id) => {
|
||||||
|
let playlist = await Playlist.findById(id)
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
throw new OperationError(404, "Playlist not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (id, update) => {
|
||||||
|
let playlist = await Playlist.findById(id).lean()
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
throw new OperationError(404, "Playlist not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist = {
|
||||||
|
...playlist,
|
||||||
|
...update,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Playlist.findByIdAndUpdate(id, playlist)
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Playlist } from "@db_models"
|
||||||
|
|
||||||
|
export default async (id, item) => {
|
||||||
|
let playlist = await Playlist.findById(id).lean()
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
throw new OperationError(404, "Playlist not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item === "string" && !Array.isArray(item)) {
|
||||||
|
item = [item]
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist.items = playlist.items.filter((entry) => !item.includes(entry))
|
||||||
|
|
||||||
|
return await Playlist.findByIdAndUpdate(id, playlist)
|
||||||
|
}
|
@ -35,6 +35,8 @@ export default class Release {
|
|||||||
onlyList: true,
|
onlyList: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
release.items = tracks
|
||||||
|
release.total_items = totalTracks
|
||||||
release.total_duration = tracks.reduce((acc, track) => {
|
release.total_duration = tracks.reduce((acc, track) => {
|
||||||
if (track.metadata?.duration) {
|
if (track.metadata?.duration) {
|
||||||
return acc + parseFloat(track.metadata.duration)
|
return acc + parseFloat(track.metadata.duration)
|
||||||
@ -42,8 +44,6 @@ export default class Release {
|
|||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, 0)
|
}, 0)
|
||||||
release.total_items = totalTracks
|
|
||||||
release.items = tracks
|
|
||||||
|
|
||||||
return release
|
return release
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,10 @@ export default async (payload = {}) => {
|
|||||||
|
|
||||||
requiredFields(["title", "source", "user_id"], payload)
|
requiredFields(["title", "source", "user_id"], payload)
|
||||||
|
|
||||||
|
console.log(`create()::`, {
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof payload._id === "string") {
|
if (typeof payload._id === "string") {
|
||||||
return await ModifyTrack(payload._id, payload)
|
return await ModifyTrack(payload._id, payload)
|
||||||
}
|
}
|
||||||
@ -71,19 +75,19 @@ export default async (payload = {}) => {
|
|||||||
source: payload.source,
|
source: payload.source,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
public: payload.public ?? true,
|
public: payload.public ?? true,
|
||||||
|
publisher: {
|
||||||
|
user_id: payload.user_id,
|
||||||
|
},
|
||||||
|
created_at: new Date(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(payload.artists)) {
|
if (Array.isArray(payload.artists)) {
|
||||||
obj.artist = payload.artists.join(", ")
|
obj.artist = payload.artists.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
let track = new Track({
|
console.log({ obj: obj })
|
||||||
...obj,
|
|
||||||
publisher: {
|
let track = new Track(obj)
|
||||||
user_id: payload.user_id,
|
|
||||||
},
|
|
||||||
created_at: new Date(),
|
|
||||||
})
|
|
||||||
|
|
||||||
await track.save()
|
await track.save()
|
||||||
|
|
||||||
|
@ -51,42 +51,53 @@ export default async (track_id, { user_id = null, onlyList = false } = {}) => {
|
|||||||
|
|
||||||
const isMultiple = Array.isArray(track_id) || track_id.includes(",")
|
const isMultiple = Array.isArray(track_id) || track_id.includes(",")
|
||||||
|
|
||||||
|
let totalItems = 1
|
||||||
|
let data = null
|
||||||
|
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
const track_ids = Array.isArray(track_id)
|
const track_ids = Array.isArray(track_id)
|
||||||
? track_id
|
? track_id
|
||||||
: track_id.split(",")
|
: track_id.split(",")
|
||||||
|
|
||||||
let tracks = await Track.find({
|
data = await Track.find({
|
||||||
_id: { $in: track_ids },
|
_id: { $in: track_ids },
|
||||||
}).lean()
|
}).lean()
|
||||||
|
|
||||||
tracks = await fullfillData(tracks, {
|
// order tracks by ids
|
||||||
user_id,
|
data = data.sort((a, b) => {
|
||||||
|
return (
|
||||||
|
track_ids.indexOf(a._id.toString()) -
|
||||||
|
track_ids.indexOf(b._id.toString())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (onlyList) {
|
totalItems = await Track.countDocuments({
|
||||||
return tracks
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
total_count: await Track.countDocuments({
|
|
||||||
_id: { $in: track_ids },
|
_id: { $in: track_ids },
|
||||||
}),
|
})
|
||||||
list: tracks,
|
} else {
|
||||||
}
|
data = await Track.findOne({
|
||||||
}
|
|
||||||
|
|
||||||
let track = await Track.findOne({
|
|
||||||
_id: track_id,
|
_id: track_id,
|
||||||
}).lean()
|
}).lean()
|
||||||
|
|
||||||
if (!track) {
|
if (!data) {
|
||||||
throw new OperationError(404, "Track not found")
|
throw new OperationError(404, "Track not found")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
track = await fullfillData(track, {
|
data = await fullfillData(data, {
|
||||||
user_id,
|
user_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return track[0]
|
if (isMultiple) {
|
||||||
|
if (onlyList) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total_count: totalItems,
|
||||||
|
list: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[0]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { TrackLyric } from "@db_models"
|
import { TrackLyric } from "@db_models"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
|
||||||
function parseTimeToMs(timeStr) {
|
function secondsToMs(number) {
|
||||||
|
return number * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
class LRCV1 {
|
||||||
|
static timeStrToMs(timeStr) {
|
||||||
const [minutes, seconds, milliseconds] = timeStr.split(":")
|
const [minutes, seconds, milliseconds] = timeStr.split(":")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -9,45 +14,54 @@ function parseTimeToMs(timeStr) {
|
|||||||
Number(seconds) * 1000 +
|
Number(seconds) * 1000 +
|
||||||
Number(milliseconds)
|
Number(milliseconds)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
async function remoteLcrToSyncedLyrics(lrcUrl) {
|
|
||||||
const { data } = await axios.get(lrcUrl)
|
|
||||||
|
|
||||||
let syncedLyrics = data
|
|
||||||
|
|
||||||
syncedLyrics = syncedLyrics.split("\n")
|
|
||||||
|
|
||||||
syncedLyrics = syncedLyrics.map((line) => {
|
|
||||||
const syncedLine = {}
|
|
||||||
|
|
||||||
//syncedLine.time = line.match(/\[.*\]/)[0]
|
|
||||||
syncedLine.time = line.split(" ")[0]
|
|
||||||
syncedLine.text = line.replace(syncedLine.time, "").trim()
|
|
||||||
|
|
||||||
if (syncedLine.text === "") {
|
|
||||||
delete syncedLine.text
|
|
||||||
syncedLine.break = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
syncedLine.time = syncedLine.time.replace(/\[|\]/g, "")
|
static timeStrToSeconds(timeStr) {
|
||||||
syncedLine.time = syncedLine.time.replace(".", ":")
|
const [minutes, seconds, milliseconds] = timeStr.split(":")
|
||||||
|
|
||||||
return syncedLine
|
return (
|
||||||
|
Number(minutes) * 60 + Number(seconds) + Number(milliseconds) / 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseString(str) {
|
||||||
|
str = str.split("\n")
|
||||||
|
|
||||||
|
str = str.map((str) => {
|
||||||
|
let line = {}
|
||||||
|
|
||||||
|
line.time = str.split(" ")[0]
|
||||||
|
line.text = str.replace(line.time, "").trim()
|
||||||
|
|
||||||
|
// detect empty lines as breaks
|
||||||
|
if (line.text === "" || line.text === "<break>") {
|
||||||
|
delete line.text
|
||||||
|
line.break = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse time
|
||||||
|
line.time = line.time.replace(/\[|\]/g, "")
|
||||||
|
line.time = line.time.replace(".", ":")
|
||||||
|
line.time = this.timeStrToSeconds(line.time)
|
||||||
|
|
||||||
|
return line
|
||||||
})
|
})
|
||||||
|
|
||||||
syncedLyrics = syncedLyrics.map((syncedLine, index) => {
|
return str
|
||||||
const nextLine = syncedLyrics[index + 1]
|
}
|
||||||
|
|
||||||
syncedLine.startTimeMs = parseTimeToMs(syncedLine.time)
|
static setTimmings(lyricsArray) {
|
||||||
syncedLine.endTimeMs = nextLine
|
lyricsArray = lyricsArray.map((line, index) => {
|
||||||
? parseTimeToMs(nextLine.time)
|
const nextLine = lyricsArray[index + 1]
|
||||||
: parseTimeToMs(syncedLyrics[syncedLyrics.length - 1].time)
|
|
||||||
|
|
||||||
return syncedLine
|
line.start_ms = secondsToMs(line.time)
|
||||||
|
line.end_ms = secondsToMs(nextLine ? nextLine.time : line.time + 1)
|
||||||
|
|
||||||
|
return line
|
||||||
})
|
})
|
||||||
|
|
||||||
return syncedLyrics
|
return lyricsArray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (req) => {
|
export default async (req) => {
|
||||||
@ -56,47 +70,41 @@ export default async (req) => {
|
|||||||
|
|
||||||
let result = await TrackLyric.findOne({
|
let result = await TrackLyric.findOne({
|
||||||
track_id,
|
track_id,
|
||||||
})
|
}).lean()
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new OperationError(404, "Track lyric not found")
|
throw new OperationError(404, "Track lyric not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.toObject()
|
|
||||||
|
|
||||||
result.translated_lang = translate_lang
|
result.translated_lang = translate_lang
|
||||||
result.available_langs = []
|
result.available_langs = []
|
||||||
|
|
||||||
const lrc = result.lrc_v2 ?? result.lrc
|
if (typeof result.lrc === "object") {
|
||||||
|
result.available_langs = Object.keys(result.lrc)
|
||||||
|
|
||||||
result.isLyricsV2 = !!result.lrc_v2
|
if (!result.lrc[translate_lang]) {
|
||||||
|
|
||||||
if (typeof lrc === "object") {
|
|
||||||
result.available_langs = Object.keys(lrc)
|
|
||||||
|
|
||||||
if (!lrc[translate_lang]) {
|
|
||||||
translate_lang = "original"
|
translate_lang = "original"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lrc[translate_lang]) {
|
if (result.lrc[translate_lang]) {
|
||||||
if (result.isLyricsV2 === true) {
|
if (typeof result.lrc[translate_lang] === "string") {
|
||||||
result.synced_lyrics = await axios.get(lrc[translate_lang])
|
let { data } = await axios.get(result.lrc[translate_lang])
|
||||||
|
|
||||||
result.synced_lyrics = result.synced_lyrics.data
|
result.synced_lyrics = LRCV1.parseString(data)
|
||||||
|
result.synced_lyrics = LRCV1.setTimmings(result.synced_lyrics)
|
||||||
} else {
|
} else {
|
||||||
result.synced_lyrics = await remoteLcrToSyncedLyrics(
|
result.synced_lyrics = result.lrc[translate_lang]
|
||||||
result.lrc[translate_lang],
|
result.synced_lyrics = LRCV1.setTimmings(result.synced_lyrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.video_starts_at || result.sync_audio_at) {
|
||||||
|
result.video_starts_at_ms = LRCV1.timeStrToMs(
|
||||||
|
result.video_starts_at ?? result.sync_audio_at,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.sync_audio_at) {
|
|
||||||
result.sync_audio_at_ms = parseTimeToMs(result.sync_audio_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.lrc
|
|
||||||
delete result.lrc_v2
|
|
||||||
delete result.__v
|
delete result.__v
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -4,7 +4,7 @@ export default {
|
|||||||
useMiddlewares: ["withAuthentication"],
|
useMiddlewares: ["withAuthentication"],
|
||||||
fn: async (req) => {
|
fn: async (req) => {
|
||||||
const { track_id } = req.params
|
const { track_id } = req.params
|
||||||
const { video_source, lrc, sync_audio_at } = req.body
|
const { video_source, lrc, video_starts_at } = req.body
|
||||||
|
|
||||||
// check if track exists
|
// check if track exists
|
||||||
let track = await Track.findById(track_id).catch(() => null)
|
let track = await Track.findById(track_id).catch(() => null)
|
||||||
@ -17,12 +17,6 @@ export default {
|
|||||||
throw new OperationError(403, "Unauthorized")
|
throw new OperationError(403, "Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Setting lyrics for track ${track_id} >`, {
|
|
||||||
track_id: track_id,
|
|
||||||
video_source: video_source,
|
|
||||||
lrc: lrc,
|
|
||||||
})
|
|
||||||
|
|
||||||
// check if trackLyric exists
|
// check if trackLyric exists
|
||||||
let trackLyric = await TrackLyric.findOne({
|
let trackLyric = await TrackLyric.findOne({
|
||||||
track_id: track_id,
|
track_id: track_id,
|
||||||
@ -33,8 +27,8 @@ export default {
|
|||||||
trackLyric = new TrackLyric({
|
trackLyric = new TrackLyric({
|
||||||
track_id: track_id,
|
track_id: track_id,
|
||||||
video_source: video_source,
|
video_source: video_source,
|
||||||
|
video_starts_at: video_starts_at,
|
||||||
lrc: lrc,
|
lrc: lrc,
|
||||||
sync_audio_at: sync_audio_at,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await trackLyric.save()
|
await trackLyric.save()
|
||||||
@ -49,8 +43,8 @@ export default {
|
|||||||
update.lrc = lrc
|
update.lrc = lrc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof sync_audio_at !== "undefined") {
|
if (typeof video_starts_at !== "undefined") {
|
||||||
update.sync_audio_at = sync_audio_at
|
update.video_starts_at = video_starts_at
|
||||||
}
|
}
|
||||||
|
|
||||||
trackLyric = await TrackLyric.findOneAndUpdate(
|
trackLyric = await TrackLyric.findOneAndUpdate(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user