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 { 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"

View File

@ -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]),
) )

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

View File

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

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(",") 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 _id: { $in: track_ids },
} })
} else {
data = await Track.findOne({
_id: track_id,
}).lean()
return { if (!data) {
total_count: await Track.countDocuments({ throw new OperationError(404, "Track not found")
_id: { $in: track_ids },
}),
list: tracks,
} }
} }
let track = await Track.findOne({ data = await fullfillData(data, {
_id: track_id,
}).lean()
if (!track) {
throw new OperationError(404, "Track not found")
}
track = await fullfillData(track, {
user_id, 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 { TrackLyric } from "@db_models"
import axios from "axios" import axios from "axios"
function parseTimeToMs(timeStr) { function secondsToMs(number) {
const [minutes, seconds, milliseconds] = timeStr.split(":") return number * 1000
return (
Number(minutes) * 60 * 1000 +
Number(seconds) * 1000 +
Number(milliseconds)
)
} }
async function remoteLcrToSyncedLyrics(lrcUrl) { class LRCV1 {
const { data } = await axios.get(lrcUrl) 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) => { return (
const syncedLine = {} Number(minutes) * 60 + Number(seconds) + Number(milliseconds) / 1000
)
}
//syncedLine.time = line.match(/\[.*\]/)[0] static parseString(str) {
syncedLine.time = line.split(" ")[0] str = str.split("\n")
syncedLine.text = line.replace(syncedLine.time, "").trim()
if (syncedLine.text === "") { str = str.map((str) => {
delete syncedLine.text let line = {}
syncedLine.break = true
}
syncedLine.time = syncedLine.time.replace(/\[|\]/g, "") line.time = str.split(" ")[0]
syncedLine.time = syncedLine.time.replace(".", ":") 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) => { // parse time
const nextLine = syncedLyrics[index + 1] line.time = line.time.replace(/\[|\]/g, "")
line.time = line.time.replace(".", ":")
line.time = this.timeStrToSeconds(line.time)
syncedLine.startTimeMs = parseTimeToMs(syncedLine.time) return line
syncedLine.endTimeMs = nextLine })
? parseTimeToMs(nextLine.time)
: parseTimeToMs(syncedLyrics[syncedLyrics.length - 1].time)
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) => { 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.sync_audio_at) { if (result.video_starts_at || result.sync_audio_at) {
result.sync_audio_at_ms = parseTimeToMs(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 delete result.__v
return result return result

View File

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