Migrated admin and media APIs to Nitro
This commit is contained in:
1 parent
ee5809a489
commit
bca41b5e6f
18 files changed
+786
-1
No files matched your search
@@ -0,0 +1,30 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import { Album } from "../../models/album"
|
||||
import { Track } from "../../models/track"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const query = getQuery(event)
|
||||
const parsedId = parseMongoId(query.id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
const force = query.force === "true" || query.force === true
|
||||
const album = await Album.findById(parsedId.value)
|
||||
if (!album) {
|
||||
setResponseStatus(event, 404)
|
||||
return { message: "Album not found" }
|
||||
}
|
||||
|
||||
const dependents = await Track.find({ album: album._id }).select("_id name").exec()
|
||||
if (force) await Promise.all(dependents.map((dependent) => Track.findByIdAndDelete(dependent._id)))
|
||||
else if (dependents.length) {
|
||||
setResponseStatus(event, 409)
|
||||
return { message: "Album has one or more dependents", dependentType: "Track", dependents }
|
||||
}
|
||||
|
||||
await Album.findByIdAndDelete(parsedId.value)
|
||||
void tryCleanUploadFileOrDirectory(event, album.file)
|
||||
return {}
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import mongoose from "mongoose"
|
||||
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form"
|
||||
import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { Album } from "../../models/album"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parsedId = parseMongoId(getQuery(event).id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const artistInput = adminFormTexts(parts, "artists")
|
||||
const artists = artistInput.length ? splitIds(artistInput.join(",")) : undefined
|
||||
const genre = adminFormText(parts, "genre")
|
||||
const file = adminFormFile(parts, "file")
|
||||
if (artists && !artists.every((id) => mongoose.isValidObjectId(id))) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "One or more ID(s) are invalid" }
|
||||
}
|
||||
if (genre && !mongoose.isValidObjectId(genre)) return validationResponse(event, "Invalid id")
|
||||
if (file && !file.type.includes("image/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only image files are accepted" }
|
||||
}
|
||||
|
||||
const album = await Album.findById(parsedId.value)
|
||||
if (!album) {
|
||||
setResponseStatus(event, 404)
|
||||
return {}
|
||||
}
|
||||
if (file) {
|
||||
void tryCleanUploadFileOrDirectory(event, album.file)
|
||||
const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}`
|
||||
await saveUploadFile(event, filePath, file.data)
|
||||
album.file = filePath
|
||||
}
|
||||
if (name) album.name = name
|
||||
if (artists) album.artists = artists.map((id) => new mongoose.Types.ObjectId(id))
|
||||
if (genre) album.genre = new mongoose.Types.ObjectId(genre)
|
||||
await album.save()
|
||||
return album
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form"
|
||||
import { saveUploadFile } from "../../utils/upload-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { Album } from "../../models/album"
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const artistInput = adminFormTexts(parts, "artists")
|
||||
const artists = artistInput.length ? splitIds(artistInput.join(",")) : undefined
|
||||
const genre = adminFormText(parts, "genre")
|
||||
const file = adminFormFile(parts, "file")
|
||||
if (name === undefined || !artists || !genre || !file) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Required" }
|
||||
}
|
||||
if (!mongoose.isValidObjectId(genre)) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Invalid id" }
|
||||
}
|
||||
if (!artists.every((id) => mongoose.isValidObjectId(id))) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "One or more ID(s) are invalid" }
|
||||
}
|
||||
if (!file.type.includes("image/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only image files are accepted" }
|
||||
}
|
||||
|
||||
const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}`
|
||||
await saveUploadFile(event, filePath, file.data)
|
||||
const album = await Album.create({ name, artists, file: filePath, genre })
|
||||
setResponseStatus(event, 201)
|
||||
return album
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import { Album } from "../../models/album"
|
||||
import { Artist } from "../../models/artist"
|
||||
import { Track } from "../../models/track"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
|
||||
async function findDependents(model: typeof Track | typeof Album, field: string, value: unknown) {
|
||||
return model.find({ [field]: value }).select("_id name").exec()
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const query = getQuery(event)
|
||||
const parsedId = parseMongoId(query.id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
const force = query.force === "true" || query.force === true
|
||||
const artist = await Artist.findById(parsedId.value)
|
||||
if (!artist) {
|
||||
setResponseStatus(event, 404)
|
||||
return { message: "Artist not found" }
|
||||
}
|
||||
|
||||
let dependents = await findDependents(Track, "artists", artist._id)
|
||||
if (force) await Promise.all(dependents.map((dependent) => Track.findByIdAndDelete(dependent._id)))
|
||||
else if (dependents.length) {
|
||||
setResponseStatus(event, 409)
|
||||
return { message: "Artist has one or more dependents", dependentType: "Track", dependents }
|
||||
}
|
||||
|
||||
dependents = await findDependents(Album, "artists", artist._id)
|
||||
if (force) await Promise.all(dependents.map((dependent) => Album.findByIdAndDelete(dependent._id)))
|
||||
else if (dependents.length) {
|
||||
setResponseStatus(event, 409)
|
||||
return { message: "Artist has one or more dependents", dependentType: "Artist", dependents }
|
||||
}
|
||||
|
||||
await Artist.findByIdAndDelete(parsedId.value)
|
||||
void tryCleanUploadFileOrDirectory(event, artist.file)
|
||||
return {}
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import mongoose from "mongoose"
|
||||
import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form"
|
||||
import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { Artist } from "../../models/artist"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parsedId = parseMongoId(getQuery(event).id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const file = adminFormFile(parts, "file")
|
||||
if (file && !file.type.includes("image/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only image files are accepted" }
|
||||
}
|
||||
|
||||
const artist = await Artist.findById(parsedId.value)
|
||||
if (!artist) {
|
||||
setResponseStatus(event, 404)
|
||||
return {}
|
||||
}
|
||||
if (file) {
|
||||
void tryCleanUploadFileOrDirectory(event, artist.file)
|
||||
const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}`
|
||||
await saveUploadFile(event, filePath, file.data)
|
||||
artist.file = filePath
|
||||
}
|
||||
if (name) artist.name = name
|
||||
await artist.save()
|
||||
return artist
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form"
|
||||
import { saveUploadFile } from "../../utils/upload-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { Artist } from "../../models/artist"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const file = adminFormFile(parts, "file")
|
||||
if (name === undefined || !file) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Required" }
|
||||
}
|
||||
if (!file.type.includes("image/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only image files are accepted" }
|
||||
}
|
||||
|
||||
const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}`
|
||||
await saveUploadFile(event, filePath, file.data)
|
||||
const artist = await Artist.create({ name, file: filePath })
|
||||
setResponseStatus(event, 201)
|
||||
return artist
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import { Album } from "../../models/album"
|
||||
import { Genre } from "../../models/genre"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId } from "../../utils/api-validation"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
|
||||
const query = getQuery(event)
|
||||
const parsedId = parseMongoId(query.id)
|
||||
if ("error" in parsedId) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: parsedId.error }
|
||||
}
|
||||
if (query.force !== "true" && query.force !== "false") {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: query.force === undefined ? "Required" : "Expected boolean, received string" }
|
||||
}
|
||||
const force = query.force === "true"
|
||||
|
||||
const genre = await Genre.findById(parsedId.value)
|
||||
if (!genre) {
|
||||
setResponseStatus(event, 404)
|
||||
return { message: "Genre not found" }
|
||||
}
|
||||
|
||||
const dependents = await Album.find({ genre: genre._id }).select("_id name")
|
||||
if (force) {
|
||||
await Promise.all(dependents.map((dependent) => Album.findByIdAndDelete(dependent._id)))
|
||||
} else if (dependents.length) {
|
||||
setResponseStatus(event, 409)
|
||||
return {
|
||||
message: "Genre has one or more dependents",
|
||||
dependentType: "Album",
|
||||
dependents,
|
||||
}
|
||||
}
|
||||
|
||||
await Genre.findByIdAndDelete(parsedId.value)
|
||||
return {}
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getQuery, readBody, setResponseStatus } from "h3"
|
||||
import { Genre } from "../../models/genre"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId } from "../../utils/api-validation"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
|
||||
const parsedId = parseMongoId(getQuery(event).id)
|
||||
if ("error" in parsedId) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: parsedId.error }
|
||||
}
|
||||
|
||||
const body = await readBody<unknown>(event)
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
setResponseStatus(event, 400)
|
||||
const received = body === null ? "null" : Array.isArray(body) ? "array" : typeof body
|
||||
return { message: `Expected object, received ${received}` }
|
||||
}
|
||||
|
||||
const record = body as Record<string, unknown>
|
||||
if (record.name !== undefined && typeof record.name !== "string") {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: `Expected string, received ${record.name === null ? "null" : typeof record.name}` }
|
||||
}
|
||||
|
||||
const genre = await Genre.findByIdAndUpdate(parsedId.value, record.name === undefined ? {} : { name: record.name }, { new: true })
|
||||
if (!genre) {
|
||||
setResponseStatus(event, 404)
|
||||
return { message: "Genre not found" }
|
||||
}
|
||||
return genre
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { readBody, setResponseStatus } from "h3"
|
||||
import { Genre } from "../../models/genre"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
|
||||
const body = await readBody<unknown>(event)
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
setResponseStatus(event, 400)
|
||||
const received = body === null ? "null" : Array.isArray(body) ? "array" : typeof body
|
||||
return { message: `Expected object, received ${received}` }
|
||||
}
|
||||
|
||||
const name = (body as { name?: unknown }).name
|
||||
if (typeof name !== "string") {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: name === undefined ? "Required" : `Expected string, received ${name === null ? "null" : typeof name}` }
|
||||
}
|
||||
try {
|
||||
const genre = await Genre.create({ name })
|
||||
setResponseStatus(event, 201)
|
||||
return genre
|
||||
} catch (error) {
|
||||
setResponseStatus(event, 400)
|
||||
if (typeof error === "object" && error !== null && "code" in error && error.code === 11000) {
|
||||
return { message: "Genre already exists" }
|
||||
}
|
||||
return { message: "Unknown error" }
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getQuery, setResponseStatus } from "h3"
|
||||
import { Track } from "../../models/track"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parsedId = parseMongoId(getQuery(event).id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
|
||||
const track = await Track.findByIdAndDelete(parsedId.value)
|
||||
if (!track) {
|
||||
setResponseStatus(event, 404)
|
||||
return {}
|
||||
}
|
||||
void tryCleanUploadFileOrDirectory(event, track.fileDir)
|
||||
return {}
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import path from "node:path"
|
||||
import mongoose from "mongoose"
|
||||
import { getQuery } from "h3"
|
||||
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, parseOptionalJson, splitIds } from "../../utils/admin-form"
|
||||
import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
import { getAudioDuration, processAudioFile } from "../../utils/audio-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
||||
import { Track } from "../../models/track"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parsedId = parseMongoId(getQuery(event).id)
|
||||
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const album = adminFormText(parts, "album")
|
||||
const artistInput = adminFormTexts(parts, "artists")
|
||||
const artists = artistInput.length ? splitIds(artistInput.join(",")) : undefined
|
||||
const file = adminFormFile(parts, "file")
|
||||
const lyrics = adminFormText(parts, "lyrics")
|
||||
if (file && !file.type.includes("audio/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only audio files are accepted" }
|
||||
}
|
||||
if (album && !mongoose.isValidObjectId(album)) return validationResponse(event, "Invalid id")
|
||||
if (artists && !artists.every((id) => mongoose.isValidObjectId(id))) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "One or more ID(s) are invalid" }
|
||||
}
|
||||
|
||||
const track = await Track.findById(parsedId.value)
|
||||
if (!track) {
|
||||
setResponseStatus(event, 404)
|
||||
return {}
|
||||
}
|
||||
if (file) {
|
||||
void tryCleanUploadFileOrDirectory(event, track.fileDir)
|
||||
const relativeFilePath = `tracks/${crypto.randomUUID()}/original.${safeFileExtension(file.name)}`
|
||||
await saveUploadFile(event, relativeFilePath, file.data)
|
||||
const duration = await getAudioDuration(event, relativeFilePath)
|
||||
void processAudioFile(event, relativeFilePath).catch((error) => console.error(error))
|
||||
track.fileDir = path.dirname(relativeFilePath)
|
||||
track.durationInSeconds = Math.round(duration)
|
||||
}
|
||||
if (name) track.name = name
|
||||
if (album) track.album = new mongoose.Types.ObjectId(album)
|
||||
if (artists) track.artists = artists.map((id) => new mongoose.Types.ObjectId(id))
|
||||
if (lyrics) {
|
||||
const parsedLyrics = parseOptionalJson(lyrics)
|
||||
if (!parsedLyrics.ok) throw new Error("Invalid lyrics JSON")
|
||||
track.lyrics = parsedLyrics.value as any
|
||||
}
|
||||
await track.save()
|
||||
return track
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import path from "node:path"
|
||||
import mongoose from "mongoose"
|
||||
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, parseOptionalJson, splitIds } from "../../utils/admin-form"
|
||||
import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files"
|
||||
import { getAudioDuration, processAudioFile } from "../../utils/audio-files"
|
||||
import { requireAuthenticatedUser } from "../../utils/auth"
|
||||
import { Track } from "../../models/track"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAuthenticatedUser(event, true)
|
||||
const parts = await readAdminForm(event)
|
||||
const name = adminFormText(parts, "name")
|
||||
const album = adminFormText(parts, "album")
|
||||
const artistInput = adminFormTexts(parts, "artists")
|
||||
const artists = artistInput.length ? splitIds(artistInput.join(",")) : undefined
|
||||
const file = adminFormFile(parts, "file")
|
||||
const lyricsInput = adminFormText(parts, "lyrics")
|
||||
if (name === undefined || album === undefined || !artists || !file) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Required" }
|
||||
}
|
||||
if (!mongoose.isValidObjectId(album)) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Invalid id" }
|
||||
}
|
||||
if (!artists.every((id) => mongoose.isValidObjectId(id))) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "One or more ID(s) are invalid" }
|
||||
}
|
||||
const parsedLyrics = parseOptionalJson(lyricsInput)
|
||||
if (!parsedLyrics.ok) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Lyrics must be a JSON object" }
|
||||
}
|
||||
if (!file.type.includes("audio/")) {
|
||||
setResponseStatus(event, 400)
|
||||
return { message: "Only audio files are accepted" }
|
||||
}
|
||||
|
||||
const relativeFilePath = `tracks/${crypto.randomUUID()}/original`
|
||||
const fileDirectory = path.dirname(relativeFilePath)
|
||||
try {
|
||||
console.log("Processing audio")
|
||||
await saveUploadFile(event, relativeFilePath, file.data)
|
||||
const duration = await getAudioDuration(event, relativeFilePath)
|
||||
void processAudioFile(event, relativeFilePath).catch((error) => console.error(error))
|
||||
const track = new Track({
|
||||
name,
|
||||
album: new mongoose.Types.ObjectId(album),
|
||||
artists: artists.map((id) => new mongoose.Types.ObjectId(id)),
|
||||
fileDir: fileDirectory,
|
||||
durationInSeconds: Math.round(duration),
|
||||
lyrics: parsedLyrics.value || undefined,
|
||||
})
|
||||
await track.save()
|
||||
setResponseStatus(event, 201)
|
||||
return track
|
||||
} catch (error) {
|
||||
void tryCleanUploadFileOrDirectory(event, fileDirectory)
|
||||
console.log(error)
|
||||
setResponseStatus(event, 500)
|
||||
return null
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user