feat(admin+server): 酷狗对接 - 扫码绑定管理员账号 / 按排期自动创建酷狗日歌单 + 同步歌曲
数据模型:SystemSettings 新增 kugouApiBaseUrl / kugouAdminCookie / kugouAdminToken / kugouAdminUser / kugouAdminBoundAt 5 字段;新建 KuGouPlaylistSync 同步历史表(12 字段 + playDate 倒序索引)。迁移脚本:app/drizzle/migrations/20260919143000_add_kugou_integration.sql 后端 Server:新增 server/services/kugouService.ts 封装 KuGouMusicApi sidecar 转发(自动拼 Cookie / Token header);新增 6 个 admin/kugou 路由:status / qr-create / qr-check / unbind / sync-schedule-playlists / sync-history。同步核心:逐日出非草稿排期 → kgHash 直取优先 → 失败用 cloudsearch/search 按标题+歌手三级召回 → /playlist/create + /playlist/tracks op=add 每 100 首分批;写同步历史(status、added/missing、歌单 URL)。 后台 UI:新建 Admin/KuGouImporter.client.vue 三栏布局——左栏 sidecar 健康卡+部署说明+扫码绑定/解绑弹窗;中栏多选排期日期+逐日出排期预览卡+按日同步;下栏最近 100 条同步历史(7 种状态 badge + 歌单链接跳转)。dashboard.vue L105 playlist-card 挂载点由 PlaylistCardPage 替换为 KuGouImporter;权限 permissions.js 5 个 ADMIN 族角色开放 playlist-card 页面访问(接口内部绑定/同步再强校验 ADMIN/SUPER_ADMIN)。 侧car要求:部署 KuGouMusicApi(参考文档 https://kugoumusicapi-docs.4everland.app),默认本机 http://127.0.0.1:3000,可通过 SystemSettings.kugouApiBaseUrl 或环境变量 KUGOU_API_BASE 覆盖。
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { ofetch } from 'ofetch'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { systemSettings } from '~/drizzle/schema'
|
||||
import { CacheService } from './cacheService'
|
||||
|
||||
export interface KuGouQrLoginStep1 {
|
||||
status?: number
|
||||
data?: {
|
||||
qr_id?: string
|
||||
login_id?: string
|
||||
qrimg?: string
|
||||
img?: string
|
||||
extra?: any
|
||||
}
|
||||
error?: string
|
||||
errcode?: number
|
||||
}
|
||||
|
||||
export interface KuGouQrLoginStep2 {
|
||||
status?: number
|
||||
data?: {
|
||||
status?: number
|
||||
token?: string
|
||||
cookie?: string
|
||||
userinfo?: any
|
||||
auth_params?: any
|
||||
}
|
||||
error?: string
|
||||
errcode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
let cacheService: CacheService | null = null
|
||||
function getCache() {
|
||||
if (!cacheService) cacheService = CacheService.getInstance()
|
||||
return cacheService
|
||||
}
|
||||
|
||||
async function readAdminSettings() {
|
||||
try {
|
||||
const c = getCache()
|
||||
const cached = await c.getSystemSettings?.()
|
||||
if (cached && (cached.kugouApiBaseUrl || cached.kugouAdminToken || cached.kugouAdminCookie)) {
|
||||
return cached
|
||||
}
|
||||
} catch (_) {}
|
||||
const rows = await db.select().from(systemSettings).limit(1)
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
export interface KuGouCallOptions {
|
||||
forceCookie?: string
|
||||
forceToken?: string
|
||||
}
|
||||
|
||||
export async function kuGouFetch<T = any>(
|
||||
path: string,
|
||||
query?: Record<string, any>,
|
||||
options?: KuGouCallOptions,
|
||||
): Promise<T> {
|
||||
const settings = (await readAdminSettings()) as any
|
||||
const base = (options?.forceToken ? null : settings?.kugouApiBaseUrl) || process.env.KUGOU_API_BASE || 'http://127.0.0.1:3000'
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128 Safari/537.36',
|
||||
}
|
||||
const cookie = options?.forceCookie || settings?.kugouAdminCookie
|
||||
if (cookie) headers.Cookie = cookie
|
||||
const url = base + path
|
||||
return await ofetch<T>(url, {
|
||||
query,
|
||||
headers,
|
||||
retry: 1,
|
||||
timeout: 20000,
|
||||
onRequest({ options }) {
|
||||
const token = options?.forceToken || settings?.kugouAdminToken
|
||||
if (token && options.query) {
|
||||
;(options.query as any).token = token
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function kuGouQrLoginCreate(platform: 'web' | 'android' | 'lite' = 'web') {
|
||||
return await kuGouFetch<KuGouQrLoginStep1>(
|
||||
platform === 'lite' ? '/login/qr/lite/create' : '/login/qr/create',
|
||||
{ timestamp: Date.now() },
|
||||
)
|
||||
}
|
||||
|
||||
export async function kuGouQrLoginCheck(params: { qr_id?: string; login_id?: string; key?: string; cookie?: string }) {
|
||||
return await kuGouFetch<KuGouQrLoginStep2>(
|
||||
'/login/qr/check',
|
||||
{
|
||||
qr_id: params.qr_id,
|
||||
login_id: params.login_id,
|
||||
key: params.key,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{ forceCookie: params.cookie },
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user