mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 05:38:04 +00:00
feat: updates
This commit is contained in:
parent
c6c59b2cd9
commit
7ce516877e
63 changed files with 6591 additions and 1107 deletions
5
packages/server/.env.example
Normal file
5
packages/server/.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
APPLE_MUSIC_TOKEN = Bearer xxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# If you want to use a different database provider like SQLite/PostgreSQL,
|
||||
# you will need to change the 'provider' in /prisma/schema.prisma
|
||||
DATABASE_URL='mysql://USER:PASSWORD@HOST:PORT/DATABASE'
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
test-env: [
|
||||
TS_NODE_FILES=true,
|
||||
TS_NODE_PROJECT=./test/tsconfig.json
|
||||
]
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# fly.toml file generated for ypm on 2022-09-06T00:57:21+08:00
|
||||
|
||||
app = "ypm"
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = ["npm run start"]
|
||||
|
||||
[build]
|
||||
builder = "heroku/buildpacks:20"
|
||||
|
||||
[env]
|
||||
PORT = "8080"
|
||||
|
||||
[experimental]
|
||||
allowed_public_ports = []
|
||||
auto_rollback = true
|
||||
|
||||
[[services]]
|
||||
http_checks = []
|
||||
internal_port = 35530
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
type = "connections"
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["http"]
|
||||
port = 80
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["tls", "http"]
|
||||
port = 443
|
||||
|
||||
[[services.tcp_checks]]
|
||||
grace_period = "1s"
|
||||
interval = "15s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
5685
packages/server/package-lock.json
generated
Normal file
5685
packages/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,15 +3,11 @@
|
|||
"version": "1.0.0",
|
||||
"description": "This project was bootstrapped with Fastify-CLI.",
|
||||
"main": "app.ts",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run build:ts && tsc -p test/tsconfig.json && tap --ts \"test/**/*.test.ts\"",
|
||||
"start": "fastify start --port 35530 --address 0.0.0.0 -l info dist/app.js",
|
||||
"build:ts": "tsc",
|
||||
"watch:ts": "tsc -w",
|
||||
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"dev": "npm run build && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch\" \"npm:dev:start\"",
|
||||
"dev:start": "fastify start --ignore-watch=.ts$ -w --port 35530 --address 0.0.0.0 -l info -P dist/app.js"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
@ -20,18 +16,20 @@
|
|||
"dependencies": {
|
||||
"@fastify/autoload": "^5.0.0",
|
||||
"@fastify/sensible": "^4.1.0",
|
||||
"@fastify/static": "^6.6.1",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"NeteaseCloudMusicApi": "^4.8.7",
|
||||
"axios": "^0.27.2",
|
||||
"fastify": "^4.0.0",
|
||||
"fastify": "^4.5.3",
|
||||
"fastify-cli": "^4.4.0",
|
||||
"fastify-plugin": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/tap": "^15.0.5",
|
||||
"concurrently": "^7.0.0",
|
||||
"fastify-tsconfig": "^1.0.1",
|
||||
"tap": "^16.1.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.4"
|
||||
"prisma": "^4.8.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
packages/server/prisma/schema.prisma
Normal file
45
packages/server/prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
relationMode = "prisma"
|
||||
}
|
||||
|
||||
model Album {
|
||||
id Int @id @unique
|
||||
neteaseId Int @unique
|
||||
name String @db.Text
|
||||
neteaseName String @db.Text
|
||||
artistName String @db.Text
|
||||
neteaseArtistName String @db.Text
|
||||
copyright String? @db.Text
|
||||
editorialVideo String? @db.Text
|
||||
artwork String? @db.Text
|
||||
editorialNote AlbumEditorialNote?
|
||||
}
|
||||
|
||||
model AlbumEditorialNote {
|
||||
id Int @id @unique
|
||||
album Album @relation(fields: [id], references: [id])
|
||||
en_US String? @map("en-US") @db.Text
|
||||
zh_CN String? @map("zh-CN") @db.Text
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id Int @id @unique
|
||||
neteaseId Int @unique
|
||||
name String @db.Text
|
||||
artwork String? @db.Text
|
||||
editorialVideo String? @db.Text
|
||||
artistBio ArtistBio?
|
||||
}
|
||||
|
||||
model ArtistBio {
|
||||
id Int @id @unique
|
||||
artist Artist @relation(fields: [id], references: [id])
|
||||
en_US String? @map("en-US") @db.Text
|
||||
zh_CN String? @map("zh-CN") @db.Text
|
||||
}
|
||||
|
|
@ -2,28 +2,12 @@ import { join } from 'path'
|
|||
import AutoLoad, { AutoloadPluginOptions } from '@fastify/autoload'
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
export type AppOptions = {
|
||||
// Place your custom options for app below here.
|
||||
} & Partial<AutoloadPluginOptions>
|
||||
|
||||
const app: FastifyPluginAsync<AppOptions> = async (
|
||||
fastify,
|
||||
opts
|
||||
): Promise<void> => {
|
||||
// Place here your custom code!
|
||||
|
||||
// Do not touch the following lines
|
||||
|
||||
// This loads all plugins defined in plugins
|
||||
// those should be support plugins that are reused
|
||||
// through your application
|
||||
const app: FastifyPluginAsync<AutoloadPluginOptions> = async (fastify, opts) => {
|
||||
void fastify.register(AutoLoad, {
|
||||
dir: join(__dirname, 'plugins'),
|
||||
options: opts,
|
||||
})
|
||||
|
||||
// This loads all plugins defined in routes
|
||||
// define your routes in one of these
|
||||
void fastify.register(AutoLoad, {
|
||||
dir: join(__dirname, 'routes'),
|
||||
options: opts,
|
||||
|
|
|
|||
25
packages/server/src/plugins/prismaPlugin.ts
Normal file
25
packages/server/src/plugins/prismaPlugin.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import fp from 'fastify-plugin'
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
// Use TypeScript module augmentation to declare the type of server.prisma to be PrismaClient
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
prisma: PrismaClient
|
||||
}
|
||||
}
|
||||
|
||||
const prismaPlugin: FastifyPluginAsync = fp(async (server, options) => {
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
await prisma.$connect()
|
||||
|
||||
// Make Prisma Client available through the fastify server instance: server.prisma
|
||||
server.decorate('prisma', prisma)
|
||||
|
||||
server.addHook('onClose', async server => {
|
||||
await server.prisma.$disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
export default prismaPlugin
|
||||
|
|
@ -1,24 +1,65 @@
|
|||
import { FastifyPluginAsync } from 'fastify'
|
||||
import appleMusicRequest from '../../utils/appleMusicRequest'
|
||||
import { album as getAlbum } from 'NeteaseCloudMusicApi'
|
||||
|
||||
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
type ResponseSchema = {
|
||||
id: number
|
||||
neteaseId: number
|
||||
name: string
|
||||
artistName: string
|
||||
editorialVideo: string
|
||||
artwork: string
|
||||
copyright: string
|
||||
neteaseName: string
|
||||
neteaseArtistName: string
|
||||
editorialNote: {
|
||||
en_US: string
|
||||
zh_CN: string
|
||||
}
|
||||
}
|
||||
|
||||
const album: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
fastify.get<{
|
||||
Querystring: {
|
||||
name: string
|
||||
artist: string
|
||||
lang: 'zh-CN' | 'en-US'
|
||||
neteaseId: string
|
||||
lang?: 'zh-CN' | 'en-US'
|
||||
}
|
||||
}>('/album', async function (request, reply) {
|
||||
const { name, lang, artist } = request.query
|
||||
}>('/album', opts, async function (request, reply): Promise<ResponseSchema | undefined> {
|
||||
const { neteaseId: neteaseIdString, lang = 'en-US' } = request.query
|
||||
|
||||
// validate neteaseAlbumID
|
||||
const neteaseId = Number(neteaseIdString)
|
||||
if (isNaN(neteaseId)) {
|
||||
reply.code(400).send('params "neteaseId" is required')
|
||||
return
|
||||
}
|
||||
|
||||
// get from database
|
||||
const fromDB = await fastify.prisma.album.findFirst({
|
||||
where: { neteaseId: neteaseId },
|
||||
include: { editorialNote: { select: { en_US: true, zh_CN: true } } },
|
||||
})
|
||||
if (fromDB) {
|
||||
return fromDB as ResponseSchema
|
||||
}
|
||||
|
||||
// get from netease
|
||||
const { body: neteaseAlbum } = (await getAlbum({ id: neteaseId })) as any
|
||||
const artist = neteaseAlbum?.album?.artist?.name
|
||||
const albumName = neteaseAlbum?.album?.name
|
||||
if (!artist || !albumName) {
|
||||
return
|
||||
}
|
||||
|
||||
// get from apple
|
||||
const fromApple = await appleMusicRequest({
|
||||
method: 'GET',
|
||||
url: '/search',
|
||||
params: {
|
||||
term: name,
|
||||
term: `${artist} ${albumName}`,
|
||||
types: 'albums',
|
||||
'fields[albums]': 'artistName,name,editorialVideo,editorialNotes',
|
||||
limit: '1',
|
||||
'fields[albums]': 'artistName,artwork,name,copyright,editorialVideo,editorialNotes',
|
||||
limit: '10',
|
||||
l: lang.toLowerCase(),
|
||||
},
|
||||
})
|
||||
|
|
@ -27,12 +68,59 @@ const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
|||
const album =
|
||||
albums?.find(
|
||||
(a: any) =>
|
||||
a.attributes.name.toLowerCase() === name.toLowerCase() &&
|
||||
a.attributes.name.toLowerCase() === albumName.toLowerCase() &&
|
||||
a.attributes.artistName.toLowerCase() === artist.toLowerCase()
|
||||
) || albums?.[0]
|
||||
if (!album) return
|
||||
|
||||
return album
|
||||
// get editorialNote
|
||||
const editorialNote = {
|
||||
en_US: lang === 'en-US' ? album.attributes.editorialNotes?.standard : '',
|
||||
zh_CN: lang === 'zh-CN' ? album.attributes.editorialNotes?.standard : '',
|
||||
}
|
||||
const otherLangEditorialNoteResult = await appleMusicRequest({
|
||||
method: 'GET',
|
||||
url: `/albums/${album.id}`,
|
||||
params: {
|
||||
'fields[albums]': 'editorialNotes',
|
||||
'omit[resource:albums]': 'relationships',
|
||||
l: lang === 'zh-CN' ? 'en-US' : 'zh-CN',
|
||||
},
|
||||
})
|
||||
const otherLangEditorialNote =
|
||||
otherLangEditorialNoteResult?.data?.[0]?.attributes?.editorialNotes?.standard
|
||||
if (lang === 'zh-CN') {
|
||||
editorialNote.en_US = otherLangEditorialNote
|
||||
} else if (lang === 'en-US') {
|
||||
editorialNote.zh_CN = otherLangEditorialNote
|
||||
}
|
||||
|
||||
const data: ResponseSchema = {
|
||||
id: Number(album.id),
|
||||
neteaseId: Number(neteaseId),
|
||||
name: album.attributes.name,
|
||||
artistName: album.attributes.artistName,
|
||||
editorialVideo: album.attributes.editorialVideo?.motionDetailSquare?.video,
|
||||
artwork: album.attributes.artwork?.url,
|
||||
editorialNote,
|
||||
copyright: album.attributes.copyright,
|
||||
neteaseName: albumName,
|
||||
neteaseArtistName: artist,
|
||||
}
|
||||
reply.send(data)
|
||||
|
||||
// save to database
|
||||
await fastify.prisma.album
|
||||
.create({
|
||||
data: {
|
||||
...data,
|
||||
editorialNote: { create: editorialNote },
|
||||
},
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
export default example
|
||||
export default album
|
||||
|
|
|
|||
|
|
@ -1,46 +1,112 @@
|
|||
import { FastifyPluginAsync } from 'fastify'
|
||||
import appleMusicRequest from '../../utils/appleMusicRequest'
|
||||
import { artist_detail as getArtistDetail } from 'NeteaseCloudMusicApi'
|
||||
|
||||
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
type ResponseSchema = {
|
||||
id: number
|
||||
neteaseId: number
|
||||
editorialVideo: string
|
||||
artwork: string
|
||||
name: string
|
||||
artistBio: {
|
||||
en_US: string
|
||||
zh_CN: string
|
||||
}
|
||||
}
|
||||
|
||||
const artist: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
fastify.get<{
|
||||
Querystring: {
|
||||
name: string
|
||||
lang: 'zh-CN' | 'en-US'
|
||||
neteaseId: string
|
||||
lang?: 'zh-CN' | 'en-US'
|
||||
}
|
||||
}>('/artist', async function (request, reply) {
|
||||
const { name, lang } = request.query
|
||||
}>('/artist', async function (request, reply): Promise<ResponseSchema | undefined> {
|
||||
const { neteaseId: neteaseIdString, lang = 'en-US' } = request.query
|
||||
|
||||
if (!name) {
|
||||
return {
|
||||
code: 400,
|
||||
message: 'params "name" is required',
|
||||
}
|
||||
// validate neteaseId
|
||||
const neteaseId = Number(neteaseIdString)
|
||||
if (isNaN(neteaseId)) {
|
||||
reply.code(400).send('params "neteaseId" is required')
|
||||
return
|
||||
}
|
||||
|
||||
// get from database
|
||||
const fromDB = await fastify.prisma.artist.findFirst({
|
||||
where: { neteaseId: neteaseId },
|
||||
include: { artistBio: { select: { en_US: true, zh_CN: true } } },
|
||||
})
|
||||
if (fromDB) {
|
||||
return fromDB as ResponseSchema
|
||||
}
|
||||
|
||||
// get from netease
|
||||
const { body: neteaseArtist } = (await getArtistDetail({ id: neteaseId })) as any
|
||||
const artistName = neteaseArtist?.data?.artist?.name
|
||||
if (!artistName) {
|
||||
return
|
||||
}
|
||||
|
||||
const fromApple = await appleMusicRequest({
|
||||
method: 'GET',
|
||||
url: '/search',
|
||||
params: {
|
||||
term: name,
|
||||
term: artistName,
|
||||
types: 'artists',
|
||||
'fields[artists]': 'url,name,artwork,editorialVideo,artistBio',
|
||||
'omit[resource:artists]': 'relationships',
|
||||
platform: 'web',
|
||||
limit: '1',
|
||||
l: lang?.toLowerCase() || 'en-us',
|
||||
with: 'serverBubbles',
|
||||
limit: '5',
|
||||
l: lang?.toLowerCase(),
|
||||
},
|
||||
})
|
||||
|
||||
const artist = fromApple?.results?.artist?.data?.[0]
|
||||
|
||||
if (
|
||||
artist &&
|
||||
artist?.attributes?.name?.toLowerCase() === name.toLowerCase()
|
||||
) {
|
||||
return artist
|
||||
if (artist?.attributes?.name?.toLowerCase() !== artistName.toLowerCase()) {
|
||||
return
|
||||
}
|
||||
|
||||
// get ArtistBio
|
||||
const artistBio = {
|
||||
en_US: lang === 'en-US' ? artist.attributes.artistBio : '',
|
||||
zh_CN: lang === 'zh-CN' ? artist.attributes.artistBio : '',
|
||||
}
|
||||
const otherLangArtistBioResult = await appleMusicRequest({
|
||||
method: 'GET',
|
||||
url: `/artists/${artist.id}`,
|
||||
params: {
|
||||
'fields[artists]': 'artistBio',
|
||||
'omit[resource:artists]': 'relationships',
|
||||
l: lang === 'zh-CN' ? 'en-US' : 'zh-CN',
|
||||
},
|
||||
})
|
||||
const otherLangArtistBio = otherLangArtistBioResult?.data?.[0]?.attributes?.artistBio
|
||||
if (lang === 'zh-CN') {
|
||||
artistBio.en_US = otherLangArtistBio
|
||||
} else if (lang === 'en-US') {
|
||||
artistBio.zh_CN = otherLangArtistBio
|
||||
}
|
||||
|
||||
const data: ResponseSchema = {
|
||||
id: Number(artist.id),
|
||||
neteaseId: neteaseId,
|
||||
name: artist.attributes.name,
|
||||
artistBio,
|
||||
editorialVideo: artist?.attributes.editorialVideo?.motionArtistSquare1x1?.video,
|
||||
artwork: artist?.attributes?.artwork?.url,
|
||||
}
|
||||
|
||||
reply.send(data)
|
||||
|
||||
// save to database
|
||||
await fastify.prisma.artist
|
||||
.create({
|
||||
data: {
|
||||
...data,
|
||||
artistBio: { create: artistBio },
|
||||
},
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
}
|
||||
|
||||
export default example
|
||||
export default artist
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
fastify.get('/', async function (request, reply) {
|
||||
return { root: true }
|
||||
fastify.get('/', async (request, reply) => {
|
||||
return 'R3PLAY server is running!'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
// This file contains code that we reuse between our tests.
|
||||
import Fastify from 'fastify'
|
||||
import fp from 'fastify-plugin'
|
||||
import App from '../src/app'
|
||||
import * as tap from 'tap'
|
||||
|
||||
export type Test = typeof tap['Test']['prototype']
|
||||
|
||||
// Fill in this config with all the configurations
|
||||
// needed for testing the application
|
||||
async function config() {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Automatically build and tear down our instance
|
||||
async function build(t: Test) {
|
||||
const app = Fastify()
|
||||
|
||||
// fastify-plugin ensures that all decorators
|
||||
// are exposed for testing purposes, this is
|
||||
// different from the production setup
|
||||
void app.register(fp(App), await config())
|
||||
|
||||
await app.ready()
|
||||
|
||||
// Tear down our app after we are done
|
||||
t.teardown(() => void app.close())
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export { config, build }
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { test } from 'tap'
|
||||
import Fastify from 'fastify'
|
||||
import Support from '../../src/plugins/support'
|
||||
|
||||
test('support works standalone', async t => {
|
||||
const fastify = Fastify()
|
||||
void fastify.register(Support)
|
||||
await fastify.ready()
|
||||
|
||||
t.equal(fastify.someSupport(), 'hugs')
|
||||
})
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { test } from 'tap'
|
||||
import { build } from '../helper'
|
||||
|
||||
test('example is loaded', async t => {
|
||||
const app = await build(t)
|
||||
|
||||
const res = await app.inject({
|
||||
url: '/example',
|
||||
})
|
||||
|
||||
t.equal(res.payload, 'this is an example')
|
||||
})
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { test } from 'tap'
|
||||
import { build } from '../helper'
|
||||
|
||||
test('default root route', async t => {
|
||||
const app = await build(t)
|
||||
|
||||
const res = await app.inject({
|
||||
url: '/',
|
||||
})
|
||||
t.same(JSON.parse(res.payload), { root: true })
|
||||
})
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["../src/**/*.ts", "**/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue