Files
vtsuru.live/src/views/manage/SongListManageView.vue
2023-12-19 10:41:04 +08:00

569 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { objectsToCSV } from '@/Utils'
import { useAccount } from '@/api/account'
import { SongFrom, SongLanguage, SongRequestOption, SongsInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import SongList from '@/components/SongList.vue'
import { FETCH_API, SONG_API_URL } from '@/data/constants'
import { Info24Filled } from '@vicons/fluent'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
import { List } from 'linqts'
import {
FormInst,
FormRules,
NButton,
NCheckbox,
NDivider,
NForm,
NFormItem,
NIcon,
NInput,
NInputGroup,
NInputGroupLabel,
NInputNumber,
NModal,
NPagination,
NSelect,
NSpace,
NSpin,
NTabPane,
NTable,
NTabs,
NTag,
NTooltip,
NTransfer,
useMessage,
} from 'naive-ui'
import { Option } from 'naive-ui/es/transfer/src/interface'
import { computed, onMounted, ref } from 'vue'
const message = useMessage()
const accountInfo = useAccount()
const showModal = ref(false)
const neteaseIdInput = ref()
const fivesingSearchInput = ref()
const isModalLoading = ref(false)
const neteaseSongListId = computed(() => {
try {
const url = new URL(neteaseIdInput.value)
console.log(url)
if (url.host == 'music.163.com') {
const regex = /id=(\d+)/
// 使用exec方法在链接中查找匹配项
const match = regex.exec(neteaseIdInput.value)
// 如果找到了匹配项那么match[1]就是分组1的值也就是id的值
if (match) {
return Number(match[1])
}
}
} catch (err) {}
try {
return Number(neteaseIdInput.value)
} catch {}
return null
})
const songs = ref<SongsInfo[]>([])
const neteaseSongs = ref<SongsInfo[]>([])
const neteaseSongsOptions = ref<Option[]>([])
const selectedNeteaseSongs = ref<string[]>([])
const fivesingResults = ref<SongsInfo[]>([])
const fivesingTotalPageCount = ref(1)
const fivesingCurrentPage = ref(1)
const isGettingFivesingSongPlayUrl = ref(0)
const authors = computed(() => {
return new List(songs.value)
.SelectMany((s) => new List(s?.author))
.Distinct()
.ToArray()
.map((t) => ({
label: t,
value: t,
}))
})
const tags = computed(() => {
return new List(songs.value)
.SelectMany((s) => new List(s?.tags))
.Distinct()
.ToArray()
.map((t) => ({
label: t,
value: t,
}))
})
const formRef = ref<FormInst | null>(null)
const addSongModel = ref<SongsInfo>({} as SongsInfo)
const addSongRules: FormRules = {
name: [
{
required: true,
message: '请输入歌曲名称',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
}
const songSelectOption = [
{
label: '中文',
value: SongLanguage.Chinese,
},
{
label: '日语',
value: SongLanguage.Japanese,
},
{
label: '英语',
value: SongLanguage.English,
},
{
label: '法语',
value: SongLanguage.French,
},
{
label: '西语',
value: SongLanguage.Spanish,
},
{
label: '其他',
value: SongLanguage.Other,
},
]
async function addCustomSong() {
isModalLoading.value = true
formRef.value
?.validate()
.then(async () => {
await addSongs([addSongModel.value], SongFrom.Custom)
.then((data) => {
if (data.code == 200) {
if (data.data.length == 1) {
message.success('成功添加歌曲: ' + addSongModel.value.name)
songs.value.push(data.data[0])
addSongModel.value = {} as SongsInfo
} else {
message.error('未能添加歌曲, 已存在相同名称的曲目')
}
} else {
message.error('添加失败: ' + data.message)
}
})
.catch((err) => {
message.error('添加失败')
})
})
.finally(() => {
isModalLoading.value = false
})
}
async function addNeteaseSongs() {
isModalLoading.value = true
const selected = neteaseSongs.value.filter((s) => selectedNeteaseSongs.value.find((select) => s.key == select))
await addSongs(selected, SongFrom.Netease)
.then((data) => {
if (data.code == 200) {
message.success(`已添加 ${data.data.length} 首歌曲`)
songs.value.push(...data.data)
neteaseSongsOptions.value = neteaseSongs.value.map((s) => ({
label: `${s.name} - ${s.author.join('/')}`,
value: s.key,
disabled: songs.value.findIndex((exist) => exist.id == s.id) > -1 || data.data.findIndex((add) => add.id == s.id) > -1,
}))
} else {
message.error('添加失败: ' + data.message)
}
})
.catch((err) => {
message.error('添加失败')
})
.finally(() => {
isModalLoading.value = false
})
}
async function addFingsingSongs(song: SongsInfo) {
isModalLoading.value = true
if (!song.url) {
try {
const url = await getFivesingSongUrl(song)
song.url = url
} catch (err) {
isModalLoading.value = false
message.error('添加失败')
return
}
}
await addSongs([song], SongFrom.FiveSing)
.then((data) => {
if (data.code == 200) {
message.success(`已添加歌曲`)
addSongModel.value = {} as SongsInfo
songs.value.push(...data.data)
} else {
message.error('添加失败: ' + data.message)
}
})
.catch((err) => {
message.error('添加失败')
})
.finally(() => {
isModalLoading.value = false
})
}
async function addSongs(songsShoudAdd: SongsInfo[], from: SongFrom) {
return QueryPostAPI<SongsInfo[]>(
SONG_API_URL + 'add',
songsShoudAdd.map((s) => ({
Name: s.name,
Id: from == SongFrom.Custom ? -1 : s.id,
From: from,
Author: s.author,
Url: s.url,
Description: s.description,
})),
)
}
async function getNeteaseSongList() {
isModalLoading.value = true
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get-netease-list', {
id: neteaseSongListId.value,
})
.then((data) => {
if (data.code == 200) {
neteaseSongs.value = data.data
neteaseSongsOptions.value = data.data.map((s) => ({
label: `${s.name} - ${!s.author ? '未知' : s.author.join('/')}`,
value: s.key,
disabled: songs.value.findIndex((exist) => exist.id == s.id) > -1,
}))
message.success(`成功获取歌曲信息, 共 ${data.data.length} 条, 歌单中已存在 ${neteaseSongsOptions.value.filter((s) => s.disabled).length}`)
} else {
message.error('获取歌单失败: ' + data.message)
}
})
.catch((err) => {
message.error('获取歌单失败: ' + err)
})
.finally(() => {
isModalLoading.value = false
})
}
async function getFivesingSearchList(isRestart = false) {
isModalLoading.value = true
await fetch(FETCH_API + `http://search.5sing.kugou.com/home/json?keyword=${fivesingSearchInput.value}&sort=1&page=${fivesingCurrentPage.value}&filter=3`)
.then(async (data) => {
const json = await data.json()
if (json.list.length == 0) {
message.error('搜索结果为空')
}
if (isRestart) {
fivesingCurrentPage.value = 1
}
fivesingResults.value = []
//fivesingResultsOptions.value = []
json.list.forEach((song: any) => {
const songInfo = {
id: song.songId,
name: extractTextFromHtml(song.songName),
author: [song.originSinger, song.singer],
//url: `http://service.5sing.kugou.com/song/getsongurl?songid=${song.songId}&songtype=bz&from=web&version=6.6.72`,
} as SongsInfo
fivesingResults.value.push(songInfo)
})
fivesingTotalPageCount.value = json.pageInfo.totalPages
message.success(`成功获取搜索信息, 共 ${json.pageInfo.totalCount} 条, 当前第 ${fivesingCurrentPage.value}`)
})
.catch((err) => {
message.error('获取歌单失败: ' + err)
})
.finally(() => {
isModalLoading.value = false
})
}
function extractTextFromHtml(html: string): string {
const regex = /<em class="keyword">(.*?)<\/em>/
const match = regex.exec(html)
if (match) {
return match[1]
} else {
return html
}
}
async function playFivesingSong(song: SongsInfo) {
isGettingFivesingSongPlayUrl.value = song.id
await getFivesingSongUrl(song)
.then((data) => {
song.url = data
})
.catch((err) => {
message.error('获取歌曲链接失败: ' + err)
})
.finally(() => {
isGettingFivesingSongPlayUrl.value = 0
})
}
async function getFivesingSongUrl(song: SongsInfo): Promise<string> {
const data = await fetch(FETCH_API + `http://service.5sing.kugou.com/song/getsongurl?songid=${song.id}&songtype=bz&from=web&version=6.6.72`)
const result = await data.text()
//忽略掉result的第一个字符和最后一个字符, 并反序列化
const json = JSON.parse(result.substring(1, result.length - 1))
if (json.code == 0) {
return json.data.lqurl
}
return ''
}
const isLoading = ref(true)
async function getSongs() {
isLoading.value = true
await QueryGetAPI<any>(SONG_API_URL + 'get', {
id: accountInfo.value?.id,
})
.then((data) => {
if (data.code == 200) {
songs.value = data.data
}
})
.catch((err) => {
message.error('获取歌曲失败: ' + err)
})
.finally(() => {
isLoading.value = false
})
}
function exportData() {
const from = (f: SongFrom) => {
switch (f) {
case SongFrom.Custom:
return '手动添加'
case SongFrom.Netease:
return '网易云'
case SongFrom.FiveSing:
return '5sing'
}
}
const text = objectsToCSV(
songs.value.map((s) => ({
id: s.id,
名称: s.name,
翻译名称: s.translateName,
作者: s.author?.join('/') ?? '未知',
创建于: format(s.createTime, 'yyyy-MM-dd HH:mm:ss'),
更新于: format(s.updateTime, 'yyyy-MM-dd HH:mm:ss'),
描述: s.description,
来自: from(s.from),
语言: s.language.map((l) => songSelectOption.find((o) => o.value == l)?.label).join(','),
标签: s.tags?.join(',') ?? '',
链接: s.url,
})),
)
const BOM = new Uint8Array([0xef, 0xbb, 0xbf])
const utf8encoder = new TextEncoder()
const utf8array = utf8encoder.encode(text)
saveAs(new Blob([BOM, utf8array], { type: 'text/csv;charset=utf-8;' }), `歌单_${format(Date.now(), 'yyyy-MM-dd HH:mm:ss')}_${accountInfo.value?.name}_.csv`)
}
onMounted(async () => {
await getSongs()
})
</script>
<template>
<NSpace>
<NButton @click="showModal = true" type="primary"> 添加歌曲 </NButton>
<NButton @click="exportData" type="primary" secondary> 导出为 CSV </NButton>
<NButton
:loading="isLoading"
@click="
() => {
getSongs()
message.success('完成')
}
"
>
刷新
</NButton>
<NButton @click="$router.push({ name: 'manage-index', query: { tab: 'template', template: 'songlist' } })"> 修改模板 </NButton>
</NSpace>
<NDivider style="margin: 16px 0 16px 0" />
<NModal v-model:show="showModal" style="max-width: 1000px" preset="card">
<template #header> 添加歌曲 </template>
<NSpin :show="isModalLoading">
<NTabs default-value="custom" animated>
<NTabPane name="custom" tab="手动录入">
<NForm ref="formRef" :rules="addSongRules" :model="addSongModel">
<NFormItem path="name" label="名称">
<NInput v-model:value="addSongModel.name" autosize style="min-width: 200px" placeholder="就是歌曲名称" />
</NFormItem>
<NFormItem path="author" label="作者">
<NSelect v-model:value="addSongModel.author" :options="authors" filterable multiple tag placeholder="输入后按回车新增" />
</NFormItem>
<NFormItem path="description" label="备注">
<NInput v-model:value="addSongModel.description" placeholder="可选" :maxlength="250" show-count autosize style="min-width: 300px" clearable />
</NFormItem>
<NFormItem path="language" label="语言">
<NSelect v-model:value="addSongModel.language" multiple :options="songSelectOption" placeholder="可选" />
</NFormItem>
<NFormItem path="tags" label="标签">
<NSelect v-model:value="addSongModel.tags" filterable multiple clearable tag placeholder="可选,输入后按回车新增" :options="tags" />
</NFormItem>
<NFormItem path="url" label="链接">
<NInput v-model:value="addSongModel.url" placeholder="可选, 后缀为mp3、wav、ogg时将会尝试播放, 否则会在新页面打开" />
</NFormItem>
<NFormItem path="options">
<template #label>
点歌设置
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
这个不是控制是否允许点歌的! 启用后将会覆盖点歌功能中的设置, 用于单独设置歌曲要求
</NTooltip>
</template>
<NSpace vertical>
<NCheckbox
:checked="addSongModel.options != undefined"
@update:checked="
(checked: boolean) => {
addSongModel.options = checked
? ({
needJianzhang: false,
needTidu: false,
needZongdu: false,
} as SongRequestOption)
: undefined
}
"
>
是否启用
</NCheckbox>
<template v-if="addSongModel.options != undefined">
<NSpace>
<NCheckbox v-model:checked="addSongModel.options.needJianzhang"> 需要舰长 </NCheckbox>
<NCheckbox v-model:checked="addSongModel.options.needTidu"> 需要提督 </NCheckbox>
<NCheckbox v-model:checked="addSongModel.options.needZongdu"> 需要总督 </NCheckbox>
</NSpace>
<NSpace align="center">
<NCheckbox
:checked="addSongModel.options.scMinPrice != undefined"
@update:checked="
(checked: boolean) => {
if (addSongModel.options) addSongModel.options.scMinPrice = checked ? 30 : undefined
}
"
>
需要SC
</NCheckbox>
<NInputGroup v-if="addSongModel.options?.scMinPrice" style="width: 200px">
<NInputGroupLabel> SC最低价格 </NInputGroupLabel>
<NInputNumber v-model:value="addSongModel.options.scMinPrice" min="30" />
</NInputGroup>
</NSpace>
<NSpace align="center">
<NCheckbox
:checked="addSongModel.options.fanMedalMinLevel != undefined"
@update:checked="
(checked: boolean) => {
if (addSongModel.options) addSongModel.options.fanMedalMinLevel = checked ? 5 : undefined
}
"
>
需要粉丝牌
<NTooltip>
<template #trigger>
<NIcon :component="Info24Filled" />
</template>
这个即使不开也会遵循全局点歌设置的粉丝牌等级
</NTooltip>
</NCheckbox>
<NInputGroup v-if="addSongModel.options?.fanMedalMinLevel" style="width: 200px">
<NInputGroupLabel> 最低等级 </NInputGroupLabel>
<NInputNumber v-model:value="addSongModel.options.fanMedalMinLevel" min="0" />
</NInputGroup>
</NSpace>
</template>
</NSpace>
</NFormItem>
</NForm>
<NButton type="primary" @click="addCustomSong"> 添加 </NButton>
</NTabPane>
<NTabPane name="netease" tab="从网易云歌单导入">
<NInput clearable style="width: 100%" autosize :status="neteaseSongListId ? 'success' : 'error'" v-model:value="neteaseIdInput" placeholder="直接输入歌单Id或者网页链接">
<template #suffix>
<NTag v-if="neteaseSongListId" type="success" size="small"> 歌单Id: {{ neteaseSongListId }} </NTag>
</template>
</NInput>
<NDivider style="margin: 10px" />
<NButton type="primary" @click="getNeteaseSongList" :disabled="!neteaseSongListId"> 获取 </NButton>
<template v-if="neteaseSongsOptions.length > 0">
<NDivider style="margin: 10px" />
<NTransfer style="height: 500px" ref="transfer" v-model:value="selectedNeteaseSongs" :options="neteaseSongsOptions" source-filterable />
<NDivider style="margin: 10px" />
<NButton type="primary" @click="addNeteaseSongs"> 添加到歌单 | {{ selectedNeteaseSongs.length }} </NButton>
</template>
</NTabPane>
<NTabPane name="5sing" tab="从5sing搜索">
<NInput clearable style="width: 100%" autosize v-model:value="fivesingSearchInput" placeholder="输入要搜索的歌名" maxlength="15" />
<NDivider style="margin: 10px" />
<NButton type="primary" @click="getFivesingSearchList(true)" :disabled="!fivesingSearchInput"> 搜索 </NButton>
<template v-if="fivesingResults.length > 0">
<NDivider style="margin: 10px" />
<div style="overflow-x: auto">
<NTable size="small" style="overflow-x: auto">
<thead>
<tr>
<th>名称</th>
<th>作者</th>
<th>试听</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="song in fivesingResults" v-bind:key="song.id">
<td>{{ song.name }}</td>
<td>
<NSpace>
<NTag size="small" v-for="author in song.author" :key="author">
{{ author }}
</NTag>
</NSpace>
</td>
<td style="display: flex; justify-content: flex-end">
<!-- 在这里播放song.url链接中的音频 -->
<NButton size="small" v-if="!song.url" @click="playFivesingSong(song)" :loading="isGettingFivesingSongPlayUrl == song.id"> 试听 </NButton>
<audio v-else controls style="max-height: 30px">
<source :src="song.url" />
</audio>
</td>
<td>
<NButton size="small" color="green" @click="addFingsingSongs(song)" :disabled="songs.findIndex((s) => s.from == SongFrom.FiveSing && s.id == song.id) > -1"> 添加 </NButton>
</td>
</tr>
</tbody>
</NTable>
</div>
<br />
<NPagination v-model:page="fivesingCurrentPage" :page-count="fivesingTotalPageCount" simple @update-page="getFivesingSearchList(false)" />
</template>
</NTabPane>
</NTabs>
</NSpin>
</NModal>
<NSpin v-if="isLoading" show />
<SongList v-else :songs="songs" is-self />
<NDivider />
</template>