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:
SrGooglo 2025-06-16 20:52:15 +00:00
parent f2cb816b21
commit 582790ba88
14 changed files with 209 additions and 96 deletions

View File

@ -1,5 +1,4 @@
import { Track, Playlist, MusicRelease } from "@db_models"
import { MusicLibraryItem } from "@db_models"
import toggleFavorite from "./methods/toggleFavorite"
import getUserLibrary from "./methods/getUserLibrary"

View File

@ -121,6 +121,7 @@ async function fetchAllKindsData(userId, limit, offsetStr) {
const actualItems = await Model.find({
_id: { $in: itemIds },
}).lean()
const actualItemsMap = new Map(
actualItems.map((item) => [item._id.toString(), item]),
)

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

View File

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

View File

@ -0,0 +1,7 @@
import { Playlist } from "@db_models"
export default async (payload) => {
let playlist = await Playlist.create(playlist)
return playlist
}

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,8 @@ export default class Release {
onlyList: true,
})
release.items = tracks
release.total_items = totalTracks
release.total_duration = tracks.reduce((acc, track) => {
if (track.metadata?.duration) {
return acc + parseFloat(track.metadata.duration)
@ -42,8 +44,6 @@ export default class Release {
return acc
}, 0)
release.total_items = totalTracks
release.items = tracks
return release
}

View File

@ -31,6 +31,10 @@ export default async (payload = {}) => {
requiredFields(["title", "source", "user_id"], payload)
console.log(`create()::`, {
payload,
})
if (typeof payload._id === "string") {
return await ModifyTrack(payload._id, payload)
}
@ -71,19 +75,19 @@ export default async (payload = {}) => {
source: payload.source,
metadata: metadata,
public: payload.public ?? true,
publisher: {
user_id: payload.user_id,
},
created_at: new Date(),
}
if (Array.isArray(payload.artists)) {
obj.artist = payload.artists.join(", ")
}
let track = new Track({
...obj,
publisher: {
user_id: payload.user_id,
},
created_at: new Date(),
})
console.log({ obj: obj })
let track = new Track(obj)
await track.save()

View File

@ -51,42 +51,53 @@ export default async (track_id, { user_id = null, onlyList = false } = {}) => {
const isMultiple = Array.isArray(track_id) || track_id.includes(",")
let totalItems = 1
let data = null
if (isMultiple) {
const track_ids = Array.isArray(track_id)
? track_id
: track_id.split(",")
let tracks = await Track.find({
data = await Track.find({
_id: { $in: track_ids },
}).lean()
tracks = await fullfillData(tracks, {
user_id,
// order tracks by ids
data = data.sort((a, b) => {
return (
track_ids.indexOf(a._id.toString()) -
track_ids.indexOf(b._id.toString())
)
})
if (onlyList) {
return tracks
}
totalItems = await Track.countDocuments({
_id: { $in: track_ids },
})
} else {
data = await Track.findOne({
_id: track_id,
}).lean()
return {
total_count: await Track.countDocuments({
_id: { $in: track_ids },
}),
list: tracks,
if (!data) {
throw new OperationError(404, "Track not found")
}
}
let track = await Track.findOne({
_id: track_id,
}).lean()
if (!track) {
throw new OperationError(404, "Track not found")
}
track = await fullfillData(track, {
data = await fullfillData(data, {
user_id,
})
return track[0]
if (isMultiple) {
if (onlyList) {
return data
}
return {
total_count: totalItems,
list: data,
}
}
return data[0]
}

View File

@ -1,53 +1,67 @@
import { TrackLyric } from "@db_models"
import axios from "axios"
function parseTimeToMs(timeStr) {
const [minutes, seconds, milliseconds] = timeStr.split(":")
return (
Number(minutes) * 60 * 1000 +
Number(seconds) * 1000 +
Number(milliseconds)
)
function secondsToMs(number) {
return number * 1000
}
async function remoteLcrToSyncedLyrics(lrcUrl) {
const { data } = await axios.get(lrcUrl)
class LRCV1 {
static timeStrToMs(timeStr) {
const [minutes, seconds, milliseconds] = timeStr.split(":")
let syncedLyrics = data
return (
Number(minutes) * 60 * 1000 +
Number(seconds) * 1000 +
Number(milliseconds)
)
}
syncedLyrics = syncedLyrics.split("\n")
static timeStrToSeconds(timeStr) {
const [minutes, seconds, milliseconds] = timeStr.split(":")
syncedLyrics = syncedLyrics.map((line) => {
const syncedLine = {}
return (
Number(minutes) * 60 + Number(seconds) + Number(milliseconds) / 1000
)
}
//syncedLine.time = line.match(/\[.*\]/)[0]
syncedLine.time = line.split(" ")[0]
syncedLine.text = line.replace(syncedLine.time, "").trim()
static parseString(str) {
str = str.split("\n")
if (syncedLine.text === "") {
delete syncedLine.text
syncedLine.break = true
}
str = str.map((str) => {
let line = {}
syncedLine.time = syncedLine.time.replace(/\[|\]/g, "")
syncedLine.time = syncedLine.time.replace(".", ":")
line.time = str.split(" ")[0]
line.text = str.replace(line.time, "").trim()
return syncedLine
})
// detect empty lines as breaks
if (line.text === "" || line.text === "<break>") {
delete line.text
line.break = true
}
syncedLyrics = syncedLyrics.map((syncedLine, index) => {
const nextLine = syncedLyrics[index + 1]
// parse time
line.time = line.time.replace(/\[|\]/g, "")
line.time = line.time.replace(".", ":")
line.time = this.timeStrToSeconds(line.time)
syncedLine.startTimeMs = parseTimeToMs(syncedLine.time)
syncedLine.endTimeMs = nextLine
? parseTimeToMs(nextLine.time)
: parseTimeToMs(syncedLyrics[syncedLyrics.length - 1].time)
return line
})
return syncedLine
})
return str
}
return syncedLyrics
static setTimmings(lyricsArray) {
lyricsArray = lyricsArray.map((line, index) => {
const nextLine = lyricsArray[index + 1]
line.start_ms = secondsToMs(line.time)
line.end_ms = secondsToMs(nextLine ? nextLine.time : line.time + 1)
return line
})
return lyricsArray
}
}
export default async (req) => {
@ -56,47 +70,41 @@ export default async (req) => {
let result = await TrackLyric.findOne({
track_id,
})
}).lean()
if (!result) {
throw new OperationError(404, "Track lyric not found")
}
result = result.toObject()
result.translated_lang = translate_lang
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 (typeof lrc === "object") {
result.available_langs = Object.keys(lrc)
if (!lrc[translate_lang]) {
if (!result.lrc[translate_lang]) {
translate_lang = "original"
}
if (lrc[translate_lang]) {
if (result.isLyricsV2 === true) {
result.synced_lyrics = await axios.get(lrc[translate_lang])
if (result.lrc[translate_lang]) {
if (typeof result.lrc[translate_lang] === "string") {
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 {
result.synced_lyrics = await remoteLcrToSyncedLyrics(
result.lrc[translate_lang],
)
result.synced_lyrics = result.lrc[translate_lang]
result.synced_lyrics = LRCV1.setTimmings(result.synced_lyrics)
}
}
}
if (result.sync_audio_at) {
result.sync_audio_at_ms = parseTimeToMs(result.sync_audio_at)
if (result.video_starts_at || result.sync_audio_at) {
result.video_starts_at_ms = LRCV1.timeStrToMs(
result.video_starts_at ?? result.sync_audio_at,
)
}
result.lrc
delete result.lrc_v2
delete result.__v
return result

View File

@ -4,7 +4,7 @@ export default {
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
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
let track = await Track.findById(track_id).catch(() => null)
@ -17,12 +17,6 @@ export default {
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
let trackLyric = await TrackLyric.findOne({
track_id: track_id,
@ -33,8 +27,8 @@ export default {
trackLyric = new TrackLyric({
track_id: track_id,
video_source: video_source,
video_starts_at: video_starts_at,
lrc: lrc,
sync_audio_at: sync_audio_at,
})
await trackLyric.save()
@ -49,8 +43,8 @@ export default {
update.lrc = lrc
}
if (typeof sync_audio_at !== "undefined") {
update.sync_audio_at = sync_audio_at
if (typeof video_starts_at !== "undefined") {
update.video_starts_at = video_starts_at
}
trackLyric = await TrackLyric.findOneAndUpdate(