Added saved library API
This commit is contained in:
1 parent
36d05ab04f
commit
0526e83a9b
7 files changed
+107
-11
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
import { setResponseStatus } from "h3"
|
||||||
|
import { User } from "#server/models/user"
|
||||||
|
import { Album } from "#server/models/album"
|
||||||
|
import { Track } from "#server/models/track"
|
||||||
|
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "#server/utils/auth"
|
||||||
|
|
||||||
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
|
const authenticatedUser = await requireAuthenticatedUser(event)
|
||||||
|
const user = await User.findById(authenticatedUser._id).select("savedTracks")
|
||||||
|
if (!user) {
|
||||||
|
setResponseStatus(event, 404)
|
||||||
|
return { message: "User not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.savedTracks.length) return []
|
||||||
|
|
||||||
|
const tracks = await Track.find({ _id: { $in: user.savedTracks } }).select("_id album").exec()
|
||||||
|
const albumByTrack = new Map(tracks.map((track: any) => [track._id.toString(), track.album?.toString()]))
|
||||||
|
const albumIds: string[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const savedTrack of user.savedTracks) {
|
||||||
|
const albumId = albumByTrack.get(savedTrack.toString())
|
||||||
|
if (albumId && !seen.has(albumId)) {
|
||||||
|
seen.add(albumId)
|
||||||
|
albumIds.push(albumId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!albumIds.length) return []
|
||||||
|
|
||||||
|
const albums = await Album.find({ _id: { $in: albumIds } }).populate("artists").populate("genre").exec()
|
||||||
|
const albumsById = new Map(albums.map((album: any) => [album._id.toString(), album]))
|
||||||
|
return albumIds.map((id) => albumsById.get(id)).filter((album) => album != null)
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
|
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "#server/utils/auth"
|
||||||
|
|
||||||
export default defineAuthenticatedEventHandler(async (event) => {
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
return await requireAuthenticatedUser(event)
|
return await requireAuthenticatedUser(event)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { getQuery, setResponseStatus } from "h3"
|
||||||
|
import { User } from "#server/models/user"
|
||||||
|
import {
|
||||||
|
defineAuthenticatedEventHandler,
|
||||||
|
requireAuthenticatedUser,
|
||||||
|
} from "#server/utils/auth"
|
||||||
|
import { parseMongoIds, validationResponse } from "#server/utils/api-validation"
|
||||||
|
|
||||||
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
|
const authenticatedUser = await requireAuthenticatedUser(event)
|
||||||
|
const parsedIds = parseMongoIds(getQuery(event).ids ?? getQuery(event).id)
|
||||||
|
if ("error" in parsedIds) return validationResponse(event, parsedIds.error)
|
||||||
|
|
||||||
|
const user = await User.findById(authenticatedUser._id)
|
||||||
|
if (!user) {
|
||||||
|
setResponseStatus(event, 404)
|
||||||
|
return { message: "User not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
user.savedTracks = user.savedTracks.filter(
|
||||||
|
(savedTrack: { toString(): string }) =>
|
||||||
|
!parsedIds.value.includes(savedTrack.toString()),
|
||||||
|
)
|
||||||
|
await user.save()
|
||||||
|
return {}
|
||||||
|
})
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
import { User } from "../../models/user"
|
import { User } from "#server/models/user"
|
||||||
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
|
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "#server/utils/auth"
|
||||||
import { setResponseStatus } from "h3"
|
import { setResponseStatus } from "h3"
|
||||||
|
|
||||||
export default defineAuthenticatedEventHandler(async (event) => {
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
const authenticatedUser = await requireAuthenticatedUser(event)
|
const authenticatedUser = await requireAuthenticatedUser(event)
|
||||||
const user = await User.findById(authenticatedUser._id).populate("savedTracks")
|
const user = await User.findById(authenticatedUser._id).populate({
|
||||||
|
path: "savedTracks",
|
||||||
|
populate: [
|
||||||
|
{ path: "artists" },
|
||||||
|
{ path: "album", populate: { path: "artists" } },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setResponseStatus(event, 404)
|
setResponseStatus(event, 404)
|
||||||
return { message: "User not found" }
|
return { message: "User not found" }
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.savedTracks
|
return user.savedTracks.filter((track: unknown) => track != null)
|
||||||
})
|
})
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getQuery, setResponseStatus } from "h3"
|
import { getQuery, setResponseStatus } from "h3"
|
||||||
import mongoose, { type ObjectId } from "mongoose"
|
import mongoose, { type ObjectId } from "mongoose"
|
||||||
import { User } from "../../models/user"
|
import { User } from "#server/models/user"
|
||||||
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
|
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "#server/utils/auth"
|
||||||
import { parseMongoId, validationResponse } from "../../utils/api-validation"
|
import { parseMongoId, validationResponse } from "#server/utils/api-validation"
|
||||||
|
|
||||||
export default defineAuthenticatedEventHandler(async (event) => {
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
const authenticatedUser = await requireAuthenticatedUser(event)
|
const authenticatedUser = await requireAuthenticatedUser(event)
|
||||||
@@ -16,7 +16,7 @@ export default defineAuthenticatedEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preserve the legacy comparison behavior and empty-object response.
|
// Preserve the legacy comparison behavior and empty-object response.
|
||||||
if ((user.savedTracks as ObjectId[]).includes(parsedId.value as unknown as ObjectId)) {
|
if ((user.savedTracks as ObjectId[]).some((savedTrack) => savedTrack.toString() === parsedId.value)) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { getQuery, setResponseStatus } from "h3"
|
||||||
|
import { User } from "#server/models/user"
|
||||||
|
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "#server/utils/auth"
|
||||||
|
import { parseMongoId, validationResponse } from "#server/utils/api-validation"
|
||||||
|
|
||||||
|
export default defineAuthenticatedEventHandler(async (event) => {
|
||||||
|
const authenticatedUser = await requireAuthenticatedUser(event)
|
||||||
|
const parsedId = parseMongoId(getQuery(event).id)
|
||||||
|
if ("error" in parsedId) return validationResponse(event, parsedId.error)
|
||||||
|
|
||||||
|
const user = await User.findById(authenticatedUser._id).select("savedTracks")
|
||||||
|
if (!user) {
|
||||||
|
setResponseStatus(event, 404)
|
||||||
|
return { message: "User not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contains: user.savedTracks.some(
|
||||||
|
(savedTrack: { toString(): string }) => savedTrack.toString() === parsedId.value,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
+11
-2
@@ -41,6 +41,9 @@ const operations: ApiOperation[] = [
|
|||||||
{ method: "get", path: "/user", tag: "User", summary: "Get the current user", auth: true, response: "User" },
|
{ method: "get", path: "/user", tag: "User", summary: "Get the current user", auth: true, response: "User" },
|
||||||
{ method: "get", path: "/user/tracks", tag: "User", summary: "Get saved tracks", auth: true, response: "TrackList" },
|
{ method: "get", path: "/user/tracks", tag: "User", summary: "Get saved tracks", auth: true, response: "TrackList" },
|
||||||
{ method: "patch", path: "/user/tracks", tag: "User", summary: "Save a track", auth: true, query: ["id"], response: "EmptyObject" },
|
{ method: "patch", path: "/user/tracks", tag: "User", summary: "Save a track", auth: true, query: ["id"], response: "EmptyObject" },
|
||||||
|
{ method: "delete", path: "/user/tracks", tag: "User", summary: "Remove saved tracks", auth: true, query: ["id", "ids"], response: "EmptyObject" },
|
||||||
|
{ method: "get", path: "/user/tracks/contains", tag: "User", summary: "Check whether a track is saved", auth: true, query: ["id"], response: "TrackMembership" },
|
||||||
|
{ method: "get", path: "/user/albums", tag: "User", summary: "Get albums represented by saved tracks", auth: true, response: "AlbumList" },
|
||||||
{ method: "post", path: "/admin/artist", tag: "Admin", summary: "Create an artist", admin: true, body: "multipart", response: "Artist", status: 201 },
|
{ method: "post", path: "/admin/artist", tag: "Admin", summary: "Create an artist", admin: true, body: "multipart", response: "Artist", status: 201 },
|
||||||
{ method: "patch", path: "/admin/artist", tag: "Admin", summary: "Update an artist", admin: true, query: ["id"], body: "multipart", response: "Artist" },
|
{ method: "patch", path: "/admin/artist", tag: "Admin", summary: "Update an artist", admin: true, query: ["id"], body: "multipart", response: "Artist" },
|
||||||
{ method: "delete", path: "/admin/artist", tag: "Admin", summary: "Delete an artist", admin: true, query: ["id", "force"], response: "EmptyObject" },
|
{ method: "delete", path: "/admin/artist", tag: "Admin", summary: "Delete an artist", admin: true, query: ["id", "force"], response: "EmptyObject" },
|
||||||
@@ -86,9 +89,14 @@ function operationSpec(operation: ApiOperation) {
|
|||||||
...(operation.query ?? []).map((name) => ({
|
...(operation.query ?? []).map((name) => ({
|
||||||
name,
|
name,
|
||||||
in: "query",
|
in: "query",
|
||||||
required: name === "q" || name === "ids" || name === "id" || (name === "force" && operation.path === "/admin/genre"),
|
required: operation.method === "delete" && operation.path === "/user/tracks"
|
||||||
|
? false
|
||||||
|
: name === "q" || name === "ids" || name === "id" || (name === "force" && operation.path === "/admin/genre"),
|
||||||
schema: fieldTypes[name] ?? { type: "string" },
|
schema: fieldTypes[name] ?? { type: "string" },
|
||||||
...(name === "ids" ? { style: "form", explode: true } : {}),
|
...(name === "ids" ? { style: "form", explode: true } : {}),
|
||||||
|
...(operation.method === "delete" && operation.path === "/user/tracks"
|
||||||
|
? { description: "Provide either id for one track or repeated ids for multiple tracks." }
|
||||||
|
: {}),
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
const bodySchema = operation.body === "json"
|
const bodySchema = operation.body === "json"
|
||||||
@@ -127,7 +135,7 @@ function operationSpec(operation: ApiOperation) {
|
|||||||
...((operation.method === "post" && operation.path.startsWith("/auth/")) || ((operation.auth || operation.admin) && operation.method !== "get" && !operation.path.startsWith("/admin/"))
|
...((operation.method === "post" && operation.path.startsWith("/auth/")) || ((operation.auth || operation.admin) && operation.method !== "get" && !operation.path.startsWith("/admin/"))
|
||||||
? { 403: jsonResponse("Invalid request origin", ref("Error")) }
|
? { 403: jsonResponse("Invalid request origin", ref("Error")) }
|
||||||
: {}),
|
: {}),
|
||||||
...(operation.path === "/user/tracks" || (operation.path.startsWith("/admin/") && (operation.method === "patch" || operation.method === "delete"))
|
...(operation.path.startsWith("/user/") || (operation.path.startsWith("/admin/") && (operation.method === "patch" || operation.method === "delete"))
|
||||||
? { 404: jsonResponse("Resource not found", ref("NotFound")) }
|
? { 404: jsonResponse("Resource not found", ref("NotFound")) }
|
||||||
: {}),
|
: {}),
|
||||||
...(operation.path === "/admin/track" && operation.method === "post"
|
...(operation.path === "/admin/track" && operation.method === "post"
|
||||||
@@ -202,6 +210,7 @@ export const openApiDocument = {
|
|||||||
AlbumOrNull: { anyOf: [ref("Album"), { type: "null" }] },
|
AlbumOrNull: { anyOf: [ref("Album"), { type: "null" }] },
|
||||||
TrackOrNull: { anyOf: [ref("Track"), { type: "null" }] },
|
TrackOrNull: { anyOf: [ref("Track"), { type: "null" }] },
|
||||||
EmptyObject: { type: "object", properties: {}, additionalProperties: false },
|
EmptyObject: { type: "object", properties: {}, additionalProperties: false },
|
||||||
|
TrackMembership: { type: "object", required: ["contains"], properties: { contains: { type: "boolean" } } },
|
||||||
NotFound: { oneOf: [ref("Error"), ref("EmptyObject")] },
|
NotFound: { oneOf: [ref("Error"), ref("EmptyObject")] },
|
||||||
DependentError: {
|
DependentError: {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
|||||||
Reference in new issue
Block a user