mirror of
https://github.com/jorgev259/soc_site-astro.git
synced 2025-06-29 07:57:41 +00:00
Create Album endpoint
This commit is contained in:
parent
c3fca425cb
commit
967a6d1587
15 changed files with 388 additions and 85 deletions
10
src/auth.ts
10
src/auth.ts
|
|
@ -1,16 +1,16 @@
|
|||
import { betterAuth } from 'better-auth'
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||
import { username } from 'better-auth/plugins'
|
||||
import { username, bearer } from 'better-auth/plugins'
|
||||
|
||||
import prismaClient from './utils/prisma-client'
|
||||
import { sendEmail } from 'utils/email'
|
||||
import forgorTemplate from 'utils/forgorTemplate'
|
||||
import verifyTemplate from 'utils/verifyTemplate'
|
||||
import { sendEmail } from './utils/email'
|
||||
import forgorTemplate from './utils/forgorTemplate'
|
||||
import verifyTemplate from './utils/verifyTemplate'
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prismaClient, { provider: 'mysql' }),
|
||||
user: { modelName: 'users' },
|
||||
plugins: [username()],
|
||||
plugins: [username(), bearer()],
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
|
|
|
|||
58
src/integrations/requestCat.ts
Normal file
58
src/integrations/requestCat.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Prisma } from '@prisma/client'
|
||||
import axios from 'axios'
|
||||
import prismaClient from 'utils/prisma-client'
|
||||
import { getImage } from 'astro:assets'
|
||||
|
||||
import { WEBHOOK_URL } from 'astro:env/server'
|
||||
|
||||
const albumArtistNames = Prisma.validator<Prisma.albumsDefaultArgs>()({
|
||||
include: { artists: { include: { artist: { select: { name: true } } } } }
|
||||
})
|
||||
type AlbumArtistNames = Prisma.albumsGetPayload<typeof albumArtistNames>
|
||||
|
||||
async function postWebhook(album: AlbumArtistNames, userText = '') {
|
||||
const url = `https://www.sittingonclouds.net/album/${album.id}`
|
||||
const content = `${url}${userText}`
|
||||
const artistNames = album.artists.map((a) => a.artist.name)
|
||||
const coverImage = await getImage({
|
||||
src: `https://cdn.sittingonclouds.net/album/${album.id}.png`,
|
||||
height: 150,
|
||||
width: 150
|
||||
})
|
||||
const embeds = [
|
||||
{
|
||||
title: album.title,
|
||||
type: 'rich',
|
||||
description: album.subTitle || artistNames.join(' - '),
|
||||
url,
|
||||
color: parseInt(album.headerColor.substring(1), 16),
|
||||
thumbnail: { url: `https://www.sittingonclouds.net${coverImage.src}` }
|
||||
}
|
||||
]
|
||||
|
||||
const payload = { content, embeds }
|
||||
await axios.post(WEBHOOK_URL, payload)
|
||||
}
|
||||
|
||||
export const requestPOST = (operation: string, body: any) => axios.post(`http://localhost:7001/${operation}`, body)
|
||||
|
||||
export async function handleComplete(album: AlbumArtistNames, requestId?: number) {
|
||||
if (requestId) {
|
||||
const request = await prismaClient.requests.findUnique({
|
||||
where: { id: requestId },
|
||||
select: { state: true, id: true, userID: true, user: true }
|
||||
})
|
||||
if (!request || request.state === 'complete') return
|
||||
|
||||
await requestPOST('complete', { requestId: request.id })
|
||||
|
||||
const userText =
|
||||
request.userID || request.user
|
||||
? ` ${request.userID ? `<@${request.userID}>` : `@${request.user}`} :arrow_down:`
|
||||
: ''
|
||||
|
||||
await postWebhook(album, userText)
|
||||
} else {
|
||||
await postWebhook(album)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import prismaClient from 'utils/prisma-client'
|
||||
import * as m from 'paraglide/messages'
|
||||
import { Image } from 'astro:assets'
|
||||
import { AlbumStatus } from '@prisma/client'
|
||||
|
||||
import BaseLayout from 'layouts/base.astro'
|
||||
import TrackList from 'components/albumPage/TrackList'
|
||||
|
|
@ -16,10 +17,13 @@ import ouoIcon from 'img/assets/ouo-icon.png'
|
|||
|
||||
const { id } = Astro.params
|
||||
const { permissions } = Astro.locals
|
||||
|
||||
const hasDirect = permissions.includes('SKIP_ADS')
|
||||
const hasUpdate = permissions.includes('UPDATE')
|
||||
const hiddenCondition = !hasUpdate ? { status: AlbumStatus.SHOW } : {}
|
||||
|
||||
const album = await prismaClient.albums.findUnique({
|
||||
where: { id: Number(id) },
|
||||
where: { id: Number(id), ...hiddenCondition },
|
||||
include: {
|
||||
artists: { select: { artist: true } },
|
||||
categories: { select: { categoryName: true } },
|
||||
|
|
|
|||
111
src/pages/api/album/create.ts
Normal file
111
src/pages/api/album/create.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import type { APIRoute } from 'astro'
|
||||
import * as s from 'superstruct'
|
||||
import { AlbumStatus } from '@prisma/client'
|
||||
|
||||
import prismaClient from 'utils/prisma-client'
|
||||
import { Status, formToObject, slug } from 'utils/form'
|
||||
import { writeImg, getImgColor } from 'utils/img'
|
||||
import { handleComplete } from 'integrations/requestCat'
|
||||
import { DownloadInput } from 'schemas/album'
|
||||
|
||||
const CreateAlbum = s.object({
|
||||
cover: s.instance(File),
|
||||
title: s.optional(s.string()),
|
||||
subTitle: s.optional(s.string()),
|
||||
releaseDate: s.optional(s.date()),
|
||||
label: s.optional(s.string()),
|
||||
vgmdb: s.optional(s.string()),
|
||||
description: s.optional(s.string()),
|
||||
status: s.defaulted(s.enums(Object.values(AlbumStatus)), AlbumStatus.HIDDEN),
|
||||
animations: s.defaulted(s.array(s.integer()), []),
|
||||
artists: s.defaulted(s.array(s.string()), []),
|
||||
categories: s.defaulted(s.array(s.string()), []),
|
||||
classifications: s.defaulted(s.array(s.string()), []),
|
||||
games: s.defaulted(s.array(s.string()), []),
|
||||
platforms: s.defaulted(s.array(s.integer()), []),
|
||||
discs: s.defaulted(s.array(s.object({ number: s.integer(), body: s.string() })), []),
|
||||
downloads: s.defaulted(s.array(DownloadInput), []),
|
||||
related: s.defaulted(s.array(s.number()), []),
|
||||
stores: s.defaulted(s.array(s.object({ provider: s.string(), url: s.string() })), []),
|
||||
request: s.optional(s.integer())
|
||||
})
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const { session, permissions, user } = locals
|
||||
|
||||
if (!session || !user) return Status(401)
|
||||
if (!permissions.includes('CREATE')) return Status(403)
|
||||
|
||||
let body
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
body = s.create(formToObject(formData), CreateAlbum)
|
||||
} catch (err) {
|
||||
return Status(422, (err as Error).message)
|
||||
}
|
||||
|
||||
try {
|
||||
const albumRow = await prismaClient.$transaction(async (tx) => {
|
||||
const artistRows = body.artists.map((name: string) => ({ slug: slug(name), name }))
|
||||
|
||||
const albumRow = await tx.albums.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
subTitle: body.subTitle,
|
||||
releaseDate: body.releaseDate,
|
||||
label: body.label,
|
||||
vgmdb: body.vgmdb,
|
||||
description: body.description,
|
||||
createdBy: user.name,
|
||||
status: body.status,
|
||||
animations: { create: body.animations.map((id) => ({ animation: { connect: { id } } })) },
|
||||
artists: {
|
||||
create: artistRows.map((a) => ({
|
||||
artist: {
|
||||
connectOrCreate: {
|
||||
create: a,
|
||||
where: { slug: a.slug }
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
categories: { create: body.categories.map((c) => ({ category: { connect: { name: c } } })) },
|
||||
classifications: { create: body.classifications.map((name) => ({ classification: { connect: { name } } })) },
|
||||
games: { create: body.games.map((slug) => ({ game: { connect: { slug } } })) },
|
||||
platforms: { create: body.platforms.map((id) => ({ platform: { connect: { id } } })) },
|
||||
// albumHistories
|
||||
discs: { createMany: { data: body.discs } },
|
||||
relatedAlbums: { create: body.related.map((id) => ({ relatedAlbum: { connect: { id } } })) }
|
||||
},
|
||||
include: { artists: { include: { artist: { select: { name: true } } } } }
|
||||
})
|
||||
|
||||
const handleCover = async () => {
|
||||
const coverPath = await writeImg(body.cover, 'album', albumRow.id)
|
||||
const headerColor = await getImgColor(coverPath)
|
||||
await tx.albums.update({ where: { id: albumRow.id }, data: { headerColor } })
|
||||
albumRow.headerColor = headerColor
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
handleCover(),
|
||||
tx.downloads.createMany({
|
||||
data: body.downloads.map((d) => ({
|
||||
title: d.title,
|
||||
small: d.small,
|
||||
albumId: albumRow.id,
|
||||
links: { create: d.links }
|
||||
}))
|
||||
})
|
||||
])
|
||||
|
||||
return albumRow
|
||||
})
|
||||
|
||||
if (albumRow.status === AlbumStatus.SHOW) await handleComplete(albumRow, body.request)
|
||||
|
||||
return Status(200, albumRow.id.toString())
|
||||
} catch (err) {
|
||||
return Status(500, (err as Error).message)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import rss, { type RSSFeedItem } from '@astrojs/rss'
|
||||
import { AlbumStatus } from '@prisma/client'
|
||||
import type { APIContext } from 'astro'
|
||||
import prismaClient from 'utils/prisma-client'
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const albums = await prismaClient.albums.findMany({
|
||||
where: { status: 'show' },
|
||||
where: { status: AlbumStatus.SHOW },
|
||||
include: { artists: { include: { artist: { select: { name: true } } } } },
|
||||
take: 15,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
|
|
|
|||
15
src/schemas/album.ts
Normal file
15
src/schemas/album.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import * as s from 'superstruct'
|
||||
|
||||
const LinkInput = s.object({
|
||||
provider: s.string(),
|
||||
custom: s.optional(s.string()),
|
||||
url: s.optional(s.string()),
|
||||
url2: s.optional(s.string()),
|
||||
directUrl: s.optional(s.string())
|
||||
})
|
||||
|
||||
export const DownloadInput = s.object({
|
||||
title: s.string(),
|
||||
small: s.defaulted(s.boolean(), false),
|
||||
links: s.defaulted(s.array(LinkInput), [])
|
||||
})
|
||||
21
src/utils/form.ts
Normal file
21
src/utils/form.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import slugify from 'slugify'
|
||||
|
||||
export const Status = (status: number, statusText?: string) => new Response(null, { status, statusText })
|
||||
export const slug = (text: string) => slugify(text, { lower: true, strict: true })
|
||||
|
||||
export function formToObject(formData: FormData) {
|
||||
const object: Record<string, any> = {}
|
||||
formData.forEach((value, key) => {
|
||||
// Reflect.has in favor of: object.hasOwnProperty(key)
|
||||
if (!Reflect.has(object, key)) {
|
||||
object[key] = value
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(object[key])) {
|
||||
object[key] = [object[key]]
|
||||
}
|
||||
object[key].push(value)
|
||||
})
|
||||
|
||||
return object
|
||||
}
|
||||
29
src/utils/img.ts
Normal file
29
src/utils/img.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import path from 'node:path'
|
||||
import fs from 'node:fs/promises'
|
||||
import sharp from 'sharp'
|
||||
|
||||
function colorToHex(color: number) {
|
||||
const hexadecimal = color.toString(16)
|
||||
return hexadecimal.length === 1 ? '0' + hexadecimal : hexadecimal
|
||||
}
|
||||
|
||||
function convertRGBtoHex(red: number, green: number, blue: number) {
|
||||
return '#' + colorToHex(red) + colorToHex(green) + colorToHex(blue)
|
||||
}
|
||||
|
||||
export async function writeImg(file: File, folder: string, id: number | string) {
|
||||
const pathString = path.join('/var/www/soc_img/img', folder)
|
||||
const fullPath = path.join(pathString, `${id}.png`)
|
||||
|
||||
const fileArray = Buffer.from(await file.arrayBuffer())
|
||||
await fs.writeFile(fullPath, fileArray)
|
||||
|
||||
return fullPath
|
||||
}
|
||||
|
||||
export async function getImgColor(filePath: string) {
|
||||
const { dominant } = await sharp(filePath).stats()
|
||||
const { r, g, b } = dominant
|
||||
|
||||
return convertRGBtoHex(r, g, b)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue