diff --git a/packages/server/services/music/classes/library/index.js b/packages/server/services/music/classes/library/index.js index 187322f1..d8a1896f 100644 --- a/packages/server/services/music/classes/library/index.js +++ b/packages/server/services/music/classes/library/index.js @@ -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" diff --git a/packages/server/services/music/classes/library/methods/getUserLibrary.js b/packages/server/services/music/classes/library/methods/getUserLibrary.js index d2121755..c1c33b42 100644 --- a/packages/server/services/music/classes/library/methods/getUserLibrary.js +++ b/packages/server/services/music/classes/library/methods/getUserLibrary.js @@ -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]), ) diff --git a/packages/server/services/music/classes/playlist/index.js b/packages/server/services/music/classes/playlist/index.js new file mode 100644 index 00000000..d4978664 --- /dev/null +++ b/packages/server/services/music/classes/playlist/index.js @@ -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 +} diff --git a/packages/server/services/music/classes/playlist/methods/appendItem.js b/packages/server/services/music/classes/playlist/methods/appendItem.js new file mode 100644 index 00000000..92d67730 --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/appendItem.js @@ -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) +} diff --git a/packages/server/services/music/classes/playlist/methods/create.js b/packages/server/services/music/classes/playlist/methods/create.js new file mode 100644 index 00000000..1a3dcb33 --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/create.js @@ -0,0 +1,7 @@ +import { Playlist } from "@db_models" + +export default async (payload) => { + let playlist = await Playlist.create(playlist) + + return playlist +} diff --git a/packages/server/services/music/classes/playlist/methods/deletePlaylist.js b/packages/server/services/music/classes/playlist/methods/deletePlaylist.js new file mode 100644 index 00000000..8c87580f --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/deletePlaylist.js @@ -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) +} diff --git a/packages/server/services/music/classes/playlist/methods/getData.js b/packages/server/services/music/classes/playlist/methods/getData.js new file mode 100644 index 00000000..407ce410 --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/getData.js @@ -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 +} diff --git a/packages/server/services/music/classes/playlist/methods/modify.js b/packages/server/services/music/classes/playlist/methods/modify.js new file mode 100644 index 00000000..7e7f430a --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/modify.js @@ -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) +} diff --git a/packages/server/services/music/classes/playlist/methods/removeItem.js b/packages/server/services/music/classes/playlist/methods/removeItem.js new file mode 100644 index 00000000..66e9d9f3 --- /dev/null +++ b/packages/server/services/music/classes/playlist/methods/removeItem.js @@ -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) +} diff --git a/packages/server/services/music/classes/release/index.js b/packages/server/services/music/classes/release/index.js index 027337a9..5e5f8a56 100644 --- a/packages/server/services/music/classes/release/index.js +++ b/packages/server/services/music/classes/release/index.js @@ -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 } diff --git a/packages/server/services/music/classes/track/methods/create.js b/packages/server/services/music/classes/track/methods/create.js index 1df4f788..97adbf14 100644 --- a/packages/server/services/music/classes/track/methods/create.js +++ b/packages/server/services/music/classes/track/methods/create.js @@ -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() diff --git a/packages/server/services/music/classes/track/methods/get.js b/packages/server/services/music/classes/track/methods/get.js index 26111c50..7d86f558 100644 --- a/packages/server/services/music/classes/track/methods/get.js +++ b/packages/server/services/music/classes/track/methods/get.js @@ -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] } diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/get.js b/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/get.js index 39f35f1a..13e1481e 100644 --- a/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/get.js +++ b/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/get.js @@ -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 === "") { + 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 diff --git a/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/put.js b/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/put.js index 2e917b84..18c913e7 100644 --- a/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/put.js +++ b/packages/server/services/music/routes/music/tracks/[track_id]/lyrics/put.js @@ -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(