mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
update music request footer
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import mitt, { Emitter } from 'mitt'
|
||||
import { Music } from './store/useMusicRequest';
|
||||
|
||||
declare type MittType<T = any> = {
|
||||
onOpenTemplateSettings: {
|
||||
template: string,
|
||||
|
||||
},
|
||||
onMusicRequestPlayerEnded: {
|
||||
music: Music
|
||||
}
|
||||
onMusicRequestPlayNextWaitingMusic: {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,7 +226,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'manage-musicRequest',
|
||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||
meta: {
|
||||
title: '点歌 (放歌',
|
||||
title: '点歌 (点播',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
},
|
||||
|
||||
160
src/store/useMusicRequest.ts
Normal file
160
src/store/useMusicRequest.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { DanmakuUserInfo, SongFrom, SongsInfo } from '@/api/api-models'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export type WaitMusicInfo = {
|
||||
from: DanmakuUserInfo
|
||||
music: SongsInfo
|
||||
}
|
||||
export type Music = {
|
||||
id: number
|
||||
title: string
|
||||
artist: string
|
||||
src: string
|
||||
pic: string
|
||||
lrc: string
|
||||
}
|
||||
export type MusicRequestSettings = {
|
||||
playMusicWhenFree: boolean
|
||||
|
||||
repeat: 'repeat-one' | 'repeat-all' | 'no-repeat'
|
||||
listMaxHeight: string
|
||||
shuffle: boolean
|
||||
volume: number
|
||||
|
||||
orderPrefix: string
|
||||
orderCooldown?: number
|
||||
orderMusicFirst: boolean
|
||||
platform: 'netease' | 'kugou'
|
||||
deviceId?: string
|
||||
|
||||
blacklist: string[]
|
||||
}
|
||||
|
||||
export const useMusicRequestProvider = defineStore('MusicRequest', () => {
|
||||
const waitingMusics = useStorage<WaitMusicInfo[]>('Setting.MusicRequest.Waiting', [])
|
||||
const originMusics = ref<SongsInfo[]>([])
|
||||
const aplayerMusics = computed(() => originMusics.value.map((m) => songToMusic(m)))
|
||||
const currentMusic = ref<Music>({
|
||||
id: -1,
|
||||
title: '',
|
||||
artist: '',
|
||||
src: '',
|
||||
pic: '',
|
||||
lrc: '',
|
||||
} as Music)
|
||||
const currentOriginMusic = ref<WaitMusicInfo>()
|
||||
const isPlayingOrderMusic = ref(false)
|
||||
const aplayerRef = ref()
|
||||
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
|
||||
playMusicWhenFree: true,
|
||||
repeat: 'repeat-all',
|
||||
listMaxHeight: '300',
|
||||
shuffle: true,
|
||||
volume: 0.5,
|
||||
|
||||
orderPrefix: '点歌',
|
||||
orderCooldown: 600,
|
||||
orderMusicFirst: true,
|
||||
platform: 'netease',
|
||||
|
||||
blacklist: [],
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
function addWaitingMusic(info: WaitMusicInfo) {
|
||||
if ((settings.value.orderMusicFirst && !isPlayingOrderMusic.value) || originMusics.value.length == 0 || aplayerRef.value?.audio.paused) {
|
||||
playMusic(info.music)
|
||||
console.log(`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`)
|
||||
} else {
|
||||
waitingMusics.value.push(info)
|
||||
message.success(`[${info.from.name}] 点了一首 ${info.music.name} - ${info.music.author?.join('/')}`)
|
||||
}
|
||||
}
|
||||
function onMusicEnd() {
|
||||
if (!playWaitingMusic()) {
|
||||
isPlayingOrderMusic.value = false
|
||||
if (currentOriginMusic) {
|
||||
currentOriginMusic.value = undefined
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!settings.value.playMusicWhenFree) {
|
||||
message.info('根据配置,已暂停播放音乐')
|
||||
currentMusic.value = aplayerMusics.value[0]
|
||||
pauseMusic()
|
||||
}
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
function onMusicPlay() {}
|
||||
function playWaitingMusic() {
|
||||
const info = waitingMusics.value.shift()
|
||||
if (info) {
|
||||
message.success(`正在播放 [${info.from.name}] 点的 ${info.music.name}`)
|
||||
console.log(`正在播放 [${info.from.name}] 点的 ${info.music.name}`)
|
||||
setTimeout(() => {
|
||||
isPlayingOrderMusic.value = true
|
||||
const index = waitingMusics.value.indexOf(info)
|
||||
if (index > -1) {
|
||||
waitingMusics.value.splice(index, 1)
|
||||
}
|
||||
currentOriginMusic.value = info
|
||||
aplayerRef.value?.pause()
|
||||
playMusic(info.music)
|
||||
}, 10)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
function playMusic(music: SongsInfo) {
|
||||
//pauseMusic()
|
||||
currentMusic.value = songToMusic(music)
|
||||
aplayerRef.value?.thenPlay()
|
||||
}
|
||||
function pauseMusic() {
|
||||
if (!aplayerRef.value?.audio.paused) {
|
||||
aplayerRef.value?.pause()
|
||||
}
|
||||
}
|
||||
function songToMusic(s: SongsInfo) {
|
||||
return {
|
||||
id: s.id,
|
||||
title: s.name,
|
||||
artist: s.author?.join('/'),
|
||||
src: s.from == SongFrom.Netease ? `https://music.163.com/song/media/outer/url?id=${s.id}.mp3` : s.url,
|
||||
pic: s.cover ?? '',
|
||||
lrc: '',
|
||||
} as Music
|
||||
}
|
||||
function setSinkId() {
|
||||
try {
|
||||
aplayerRef.value?.audio.setSinkId(settings.value.deviceId ?? 'default')
|
||||
console.log('设置音频输出设备为 ' + (settings.value.deviceId ?? '默认'))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error('设置音频输出设备失败: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
waitingMusics,
|
||||
originMusics,
|
||||
aplayerMusics,
|
||||
currentMusic,
|
||||
currentOriginMusic,
|
||||
isPlayingOrderMusic,
|
||||
settings,
|
||||
setSinkId,
|
||||
playWaitingMusic,
|
||||
playMusic,
|
||||
addWaitingMusic,
|
||||
onMusicEnd,
|
||||
onMusicPlay,
|
||||
pauseMusic,
|
||||
aplayerRef,
|
||||
}
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { ThemeType } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||
import { ACCOUNT_API_URL } from '@/data/constants'
|
||||
import { useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||
import {
|
||||
CalendarClock24Filled,
|
||||
Chat24Filled,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
NIcon,
|
||||
NLayout,
|
||||
NLayoutContent,
|
||||
NLayoutFooter,
|
||||
NLayoutHeader,
|
||||
NLayoutSider,
|
||||
NMenu,
|
||||
@@ -37,12 +39,14 @@ import {
|
||||
NSpace,
|
||||
NSpin,
|
||||
NSwitch,
|
||||
NTag,
|
||||
NText,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import APlayer from 'vue3-aplayer'
|
||||
import DanmakuLayout from './manage/DanmakuLayout.vue'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
@@ -60,9 +64,18 @@ const type = computed(() => {
|
||||
return ''
|
||||
})
|
||||
const cookie = useStorage('JWT_Token', '')
|
||||
const musicRquestStore = useMusicRequestProvider()
|
||||
|
||||
const canResendEmail = ref(false)
|
||||
|
||||
const aplayerHeight = computed(() => {
|
||||
return musicRquestStore.originMusics.length == 0 ? '0' : '80'
|
||||
})
|
||||
const aplayer = ref()
|
||||
watch(aplayer, () => {
|
||||
musicRquestStore.aplayerRef = aplayer.value
|
||||
})
|
||||
|
||||
function renderIcon(icon: unknown) {
|
||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||
}
|
||||
@@ -276,7 +289,7 @@ const menuOptions = [
|
||||
},
|
||||
),
|
||||
]),
|
||||
default: () => accountInfo.value?.isBiliVerified ? '需要使用直播弹幕的功能' : '你尚未进行 Bilibili 认证, 请前往面板进行绑定',
|
||||
default: () => (accountInfo.value?.isBiliVerified ? '需要使用直播弹幕的功能' : '你尚未进行 Bilibili 认证, 请前往面板进行绑定'),
|
||||
},
|
||||
),
|
||||
key: 'manage-danmaku',
|
||||
@@ -428,82 +441,103 @@ onMounted(() => {
|
||||
</template>
|
||||
</NPageHeader>
|
||||
</NLayoutHeader>
|
||||
<NScrollbar x-scrollable>
|
||||
<NLayout has-sider>
|
||||
<NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false">
|
||||
<NSpace justify="center" style="margin-top: 16px">
|
||||
<NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%">
|
||||
<template #icon>
|
||||
<NIcon :component="BrowsersOutline" />
|
||||
</template>
|
||||
<template v-if="width >= 180"> 面板 </template>
|
||||
</NButton>
|
||||
<NTooltip v-if="width >= 180">
|
||||
<template #trigger>
|
||||
<NButton @click="$router.push({ name: 'manage-feedback' })">
|
||||
<template #icon>
|
||||
<NIcon :component="PersonFeedback24Filled" />
|
||||
<NLayout has-sider>
|
||||
<NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false">
|
||||
<NSpace justify="center" style="margin-top: 16px">
|
||||
<NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%">
|
||||
<template #icon>
|
||||
<NIcon :component="BrowsersOutline" />
|
||||
</template>
|
||||
<template v-if="width >= 180"> 面板 </template>
|
||||
</NButton>
|
||||
<NTooltip v-if="width >= 180">
|
||||
<template #trigger>
|
||||
<NButton @click="$router.push({ name: 'manage-feedback' })">
|
||||
<template #icon>
|
||||
<NIcon :component="PersonFeedback24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
反馈
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
<NMenu
|
||||
style="margin-top: 12px"
|
||||
:disabled="accountInfo?.isEmailVerified != true"
|
||||
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
/>
|
||||
<NSpace v-if="width > 150" justify="center" align="center" vertical>
|
||||
<NText depth="3">
|
||||
有更多功能建议请
|
||||
<NButton text type="info" @click="$router.push({ name: 'manage-feedback' })"> 反馈 </NButton>
|
||||
</NText>
|
||||
<NText depth="3">
|
||||
<NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton>
|
||||
</NText>
|
||||
</NSpace>
|
||||
</NLayoutSider>
|
||||
<NLayout>
|
||||
<NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`">
|
||||
<NLayoutContent style="box-sizing: border-box; padding: 20px; min-width: 300px">
|
||||
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
|
||||
<KeepAlive>
|
||||
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
|
||||
<Suspense v-else>
|
||||
<component :is="Component" />
|
||||
<template #fallback>
|
||||
<NSpin show />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
反馈
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
<NMenu
|
||||
style="margin-top: 12px"
|
||||
:disabled="accountInfo?.isEmailVerified != true"
|
||||
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
/>
|
||||
<NSpace v-if="width > 150" justify="center" align="center" vertical>
|
||||
<NText depth="3">
|
||||
有更多功能建议请
|
||||
<NButton text type="info" @click="$router.push({ name: 'manage-feedback' })"> 反馈 </NButton>
|
||||
</NText>
|
||||
<NText depth="3">
|
||||
<NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton>
|
||||
</NText>
|
||||
</NSpace>
|
||||
</NLayoutSider>
|
||||
<NScrollbar style="height: calc(100vh - 50px)">
|
||||
<NLayout>
|
||||
<div style="box-sizing: border-box; padding: 20px; min-width: 300px">
|
||||
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
|
||||
<KeepAlive>
|
||||
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
|
||||
<Suspense v-else>
|
||||
<component :is="Component" />
|
||||
<template #fallback>
|
||||
<NSpin show />
|
||||
</template>
|
||||
</Suspense>
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
<template v-else>
|
||||
<NAlert type="info">
|
||||
请进行邮箱验证
|
||||
<br /><br />
|
||||
<NSpace>
|
||||
<NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail"> 重新发送验证邮件 </NButton>
|
||||
<NCountdown v-if="!canResendEmail" :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()" @finish="canResendEmail = true" />
|
||||
</Suspense>
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
<template v-else>
|
||||
<NAlert type="info">
|
||||
请进行邮箱验证
|
||||
<br /><br />
|
||||
<NSpace>
|
||||
<NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail"> 重新发送验证邮件 </NButton>
|
||||
<NCountdown v-if="!canResendEmail" :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()" @finish="canResendEmail = true" />
|
||||
|
||||
<NPopconfirm @positive-click="logout" size="small">
|
||||
<template #trigger>
|
||||
<NButton type="error"> 登出 </NButton>
|
||||
</template>
|
||||
确定登出?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NAlert>
|
||||
</template>
|
||||
<NBackTop />
|
||||
</div>
|
||||
</NLayout>
|
||||
<NPopconfirm @positive-click="logout" size="small">
|
||||
<template #trigger>
|
||||
<NButton type="error"> 登出 </NButton>
|
||||
</template>
|
||||
确定登出?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NAlert>
|
||||
</template>
|
||||
<NBackTop />
|
||||
</NLayoutContent>
|
||||
</NScrollbar>
|
||||
<NLayoutFooter :style="`height: ${aplayerHeight}px;overflow: auto`">
|
||||
<div style="display: flex; align-items: center; margin: 0 10px 0 10px">
|
||||
<APlayer
|
||||
v-if="musicRquestStore.aplayerMusics.length > 0"
|
||||
ref="aplayer"
|
||||
:list="musicRquestStore.aplayerMusics"
|
||||
v-model:music="musicRquestStore.currentMusic"
|
||||
v-model:volume="musicRquestStore.settings.volume"
|
||||
v-model:shuffle="musicRquestStore.settings.shuffle"
|
||||
v-model:repeat="musicRquestStore.settings.repeat"
|
||||
:listMaxHeight="'200'"
|
||||
mutex
|
||||
listFolded
|
||||
@ended="musicRquestStore.onMusicEnd"
|
||||
@play="musicRquestStore.onMusicPlay"
|
||||
style="flex: 1;min-width: 400px;"
|
||||
/>
|
||||
<NSpace vertical>
|
||||
<NTag :bordered="false" type="info" size="small"> 队列: {{ musicRquestStore.waitingMusics.length }} </NTag>
|
||||
<NButton size="small" type="info" @click="musicRquestStore.waitingMusics.length > 0 ? musicRquestStore.onMusicEnd() : musicRquestStore.aplayerRef?.onAudioEnded()"> 下一首 </NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NScrollbar>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
<template v-else>
|
||||
<NLayoutContent style="display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 50px; height: 100%; box-sizing: border-box">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DanmakuUserInfo, EventModel, FunctionTypes, SongFrom, SongsInfo } from
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
|
||||
import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { List } from 'linqts'
|
||||
import {
|
||||
@@ -37,27 +38,10 @@ import {
|
||||
SelectOption,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import APlayer from 'vue3-aplayer'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import MusicRequestOBS from '../obs/MusicRequestOBS.vue'
|
||||
|
||||
type MusicRequestSettings = {
|
||||
playMusicWhenFree: boolean
|
||||
|
||||
repeat: 'repeat-one' | 'repeat-all' | 'no-repeat'
|
||||
listMaxHeight: string
|
||||
shuffle: boolean
|
||||
volume: number
|
||||
|
||||
orderPrefix: string
|
||||
orderCooldown?: number
|
||||
orderMusicFirst: boolean
|
||||
platform: 'netease' | 'kugou'
|
||||
deviceId?: string
|
||||
|
||||
blacklist: string[]
|
||||
}
|
||||
type Music = {
|
||||
id: number
|
||||
title: string
|
||||
@@ -71,21 +55,11 @@ type WaitMusicInfo = {
|
||||
music: SongsInfo
|
||||
}
|
||||
|
||||
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
|
||||
playMusicWhenFree: true,
|
||||
repeat: 'repeat-all',
|
||||
listMaxHeight: '300',
|
||||
shuffle: true,
|
||||
volume: 0.5,
|
||||
|
||||
orderPrefix: '点歌',
|
||||
orderCooldown: 600,
|
||||
orderMusicFirst: true,
|
||||
platform: 'netease',
|
||||
|
||||
blacklist: [],
|
||||
const settings = computed(() => {
|
||||
return musicRquestStore.settings
|
||||
})
|
||||
const cooldown = useStorage<{ [id: number]: number }>('Setting.MusicRequest.Cooldown', {})
|
||||
const musicRquestStore = useMusicRequestProvider()
|
||||
|
||||
const props = defineProps<{
|
||||
client: DanmakuClient
|
||||
@@ -99,28 +73,9 @@ const deviceList = ref<SelectOption[]>([])
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
|
||||
const aplayer = ref()
|
||||
|
||||
const listening = ref(false)
|
||||
|
||||
const originMusics = ref<SongsInfo[]>(await get())
|
||||
const waitingMusics = useStorage<WaitMusicInfo[]>('Setting.MusicRequest.Waiting', [])
|
||||
const musics = computed(() => {
|
||||
return originMusics.value.map((s) => songToMusic(s))
|
||||
})
|
||||
const currentMusic = ref<Music>({
|
||||
id: -1,
|
||||
title: '',
|
||||
artist: '',
|
||||
src: '',
|
||||
pic: '',
|
||||
lrc: '',
|
||||
} as Music)
|
||||
const currentOriginMusic = ref<WaitMusicInfo>()
|
||||
watch(currentOriginMusic, (music) => {
|
||||
if (music) {
|
||||
currentMusic.value = songToMusic(music.music)
|
||||
}
|
||||
const originMusics = computed(() => {
|
||||
return musicRquestStore.originMusics
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
@@ -242,7 +197,7 @@ function delMusic(song: SongsInfo) {
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已删除')
|
||||
originMusics.value = originMusics.value.filter((s) => s.key != song.key)
|
||||
musicRquestStore.originMusics = originMusics.value.filter((s) => s.key != song.key)
|
||||
} else {
|
||||
message.error('删除失败: ' + data.message)
|
||||
}
|
||||
@@ -256,7 +211,7 @@ function clearMusic() {
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已清空')
|
||||
originMusics.value = []
|
||||
musicRquestStore.originMusics = []
|
||||
} else {
|
||||
message.error('清空失败: ' + data.message)
|
||||
}
|
||||
@@ -284,7 +239,7 @@ async function downloadConfig() {
|
||||
if (data.msg) {
|
||||
message.error('获取失败: ' + data.msg)
|
||||
} else {
|
||||
settings.value = data.data
|
||||
musicRquestStore.settings = data.data ?? ({} as MusicRequestSettings)
|
||||
message.success('已获取配置文件')
|
||||
}
|
||||
})
|
||||
@@ -304,10 +259,9 @@ function stopListen() {
|
||||
listening.value = false
|
||||
message.success('已停止监听')
|
||||
}
|
||||
const isPlayingOrderMusic = ref(false)
|
||||
async function onGetEvent(data: EventModel) {
|
||||
if (!listening.value || !checkMessage(data.msg)) return
|
||||
if (settings.value.orderCooldown && cooldown.value[data.uid]) {
|
||||
if (settings.value.orderCooldown && cooldown.value[data.uid] && data.uid != (accountInfo.value?.biliId ?? -1)) {
|
||||
const lastRequest = cooldown.value[data.uid]
|
||||
if (Date.now() - lastRequest < settings.value.orderCooldown * 1000) {
|
||||
message.info(`[${data.name}] 冷却中,距离下次点歌还有 ${((settings.value.orderCooldown * 1000 - (Date.now() - lastRequest)) / 1000).toFixed(1)} 秒`)
|
||||
@@ -333,63 +287,13 @@ async function onGetEvent(data: EventModel) {
|
||||
},
|
||||
music: result,
|
||||
} as WaitMusicInfo
|
||||
if ((settings.value.orderMusicFirst && !isPlayingOrderMusic.value) || originMusics.value.length == 0 || aplayer.value?.audio.paused) {
|
||||
playWaitingMusic(music)
|
||||
console.log(`正在播放 [${data.name}] 点的 ${result.name} - ${result.author?.join('/')}`)
|
||||
} else {
|
||||
waitingMusics.value.push(music)
|
||||
message.success(`[${data.name}] 点了一首 ${result.name} - ${result.author?.join('/')}`)
|
||||
}
|
||||
musicRquestStore.addWaitingMusic(music)
|
||||
}
|
||||
}
|
||||
function checkMessage(msg: string) {
|
||||
return msg.trim().toLowerCase().startsWith(settings.value.orderPrefix.trimStart())
|
||||
}
|
||||
function onMusicEnd() {
|
||||
const data = waitingMusics.value.shift()
|
||||
if (data) {
|
||||
setTimeout(() => {
|
||||
playWaitingMusic(data)
|
||||
message.success(`正在播放 [${data.from.name}] 点的 ${data.music.name}`)
|
||||
console.log(`正在播放 [${data.from.name}] 点的 ${data.music.name}`)
|
||||
}, 10) //逆天
|
||||
} else {
|
||||
isPlayingOrderMusic.value = false
|
||||
currentOriginMusic.value = undefined
|
||||
setTimeout(() => {
|
||||
if (!settings.value.playMusicWhenFree) {
|
||||
message.info('根据配置,已暂停播放音乐')
|
||||
pauseMusic()
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
function playWaitingMusic(music: WaitMusicInfo) {
|
||||
isPlayingOrderMusic.value = true
|
||||
const index = waitingMusics.value.indexOf(music)
|
||||
if (index > -1) {
|
||||
waitingMusics.value.splice(index, 1)
|
||||
}
|
||||
pauseMusic()
|
||||
currentOriginMusic.value = music
|
||||
nextTick(() => {
|
||||
aplayer.value?.play()
|
||||
})
|
||||
}
|
||||
function pauseMusic() {
|
||||
if (!aplayer.value?.audio.paused) {
|
||||
aplayer.value?.pause()
|
||||
}
|
||||
}
|
||||
function setSinkId() {
|
||||
try {
|
||||
aplayer.value?.audio.setSinkId(settings.value.deviceId ?? 'default')
|
||||
console.log('设置音频输出设备为 ' + (settings.value.deviceId ?? '默认'))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error('设置音频输出设备失败: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
function songToMusic(s: SongsInfo) {
|
||||
return {
|
||||
id: s.id,
|
||||
@@ -417,32 +321,34 @@ async function getOutputDevice() {
|
||||
}
|
||||
function blockMusic(song: SongsInfo) {
|
||||
settings.value.blacklist.push(song.name)
|
||||
waitingMusics.value.splice(
|
||||
waitingMusics.value.findIndex((m) => m.music == song),
|
||||
musicRquestStore.waitingMusics.splice(
|
||||
musicRquestStore.waitingMusics.findIndex((m) => m.music == song),
|
||||
1,
|
||||
)
|
||||
message.success(`[${song.name}] 已添加到黑名单`)
|
||||
}
|
||||
function updateWaiting() {
|
||||
QueryPostAPI(MUSIC_REQUEST_API_URL + 'update-waiting', {
|
||||
playing: currentOriginMusic.value,
|
||||
waiting: waitingMusics.value,
|
||||
playing: musicRquestStore.currentOriginMusic,
|
||||
waiting: musicRquestStore.waitingMusics,
|
||||
})
|
||||
}
|
||||
|
||||
let timer: number
|
||||
onMounted(async () => {
|
||||
props.client.onEvent('danmaku', onGetEvent)
|
||||
|
||||
if (musicRquestStore.originMusics.length == 0) {
|
||||
musicRquestStore.originMusics = await get()
|
||||
}
|
||||
if (originMusics.value.length > 0) {
|
||||
currentMusic.value = songToMusic(originMusics.value[0])
|
||||
musicRquestStore.currentMusic = songToMusic(originMusics.value[0])
|
||||
}
|
||||
await getOutputDevice()
|
||||
if (deviceList.value.length > 0) {
|
||||
if (!deviceList.value.find((d) => d.value == settings.value.deviceId)) {
|
||||
settings.value.deviceId = undefined
|
||||
} else {
|
||||
setSinkId()
|
||||
musicRquestStore.setSinkId()
|
||||
}
|
||||
}
|
||||
timer = setInterval(updateWaiting, 2000)
|
||||
@@ -483,26 +389,14 @@ onUnmounted(() => {
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<APlayer
|
||||
v-if="musics.length > 0 || currentMusic.src"
|
||||
:list="musics"
|
||||
v-model:music="currentMusic"
|
||||
ref="aplayer"
|
||||
v-model:volume="settings.volume"
|
||||
v-model:shuffle="settings.shuffle"
|
||||
v-model:repeat="settings.repeat"
|
||||
:listMaxHeight="'200'"
|
||||
mutex
|
||||
@ended="onMusicEnd"
|
||||
/>
|
||||
<NCollapse :default-expanded-names="['1']">
|
||||
<NCollapseItem title="队列" name="1">
|
||||
<NEmpty v-if="waitingMusics.length == 0"> 暂无 </NEmpty>
|
||||
<NEmpty v-if="musicRquestStore.waitingMusics.length == 0"> 暂无 </NEmpty>
|
||||
<NList v-else size="small" bordered>
|
||||
<NListItem v-for="item in waitingMusics">
|
||||
<NListItem v-for="item in musicRquestStore.waitingMusics">
|
||||
<NSpace align="center">
|
||||
<NButton @click="playWaitingMusic(item)" type="primary" secondary size="small"> 播放 </NButton>
|
||||
<NButton @click="waitingMusics.splice(waitingMusics.indexOf(item), 1)" type="error" secondary size="small"> 取消 </NButton>
|
||||
<NButton @click="musicRquestStore.playMusic(item.music)" type="primary" secondary size="small"> 播放 </NButton>
|
||||
<NButton @click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)" type="error" secondary size="small"> 取消 </NButton>
|
||||
<NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
|
||||
<span>
|
||||
<NTag v-if="item.music.from == SongFrom.Netease" type="success" size="small"> 网易</NTag>
|
||||
@@ -558,7 +452,13 @@ onUnmounted(() => {
|
||||
</NSpace>
|
||||
<NSpace>
|
||||
<NButton @click="getOutputDevice"> 获取输出设备 </NButton>
|
||||
<NSelect v-model:value="settings.deviceId" :options="deviceList" :fallback-option="() => ({ label: '未选择', value: '' })" style="min-width: 200px" @update:value="setSinkId" />
|
||||
<NSelect
|
||||
v-model:value="settings.deviceId"
|
||||
:options="deviceList"
|
||||
:fallback-option="() => ({ label: '未选择', value: '' })"
|
||||
style="min-width: 200px"
|
||||
@update:value="musicRquestStore.setSinkId"
|
||||
/>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NTabPane>
|
||||
@@ -573,7 +473,7 @@ onUnmounted(() => {
|
||||
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入 </NButton>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 15px 0 10px 0" />
|
||||
<NEmpty v-if="musics.length == 0"> 暂无 </NEmpty>
|
||||
<NEmpty v-if="musicRquestStore.originMusics.length == 0"> 暂无 </NEmpty>
|
||||
<NVirtualList v-else :style="`max-height: 1000px`" :item-size="30" :items="originMusics" item-resizable>
|
||||
<template #default="{ item, index }">
|
||||
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`">
|
||||
@@ -584,6 +484,8 @@ onUnmounted(() => {
|
||||
</template>
|
||||
确定删除?
|
||||
</NPopconfirm>
|
||||
|
||||
<NButton type="info" secondary size="small" @click="musicRquestStore.playMusic(item)"> 播放 </NButton>
|
||||
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
|
||||
</NSpace>
|
||||
</p>
|
||||
|
||||
@@ -117,6 +117,7 @@ const voiceOptions = computed(() => {
|
||||
.ToArray()
|
||||
})
|
||||
const isSpeaking = ref(false)
|
||||
const speakingText = ref('')
|
||||
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
|
||||
const isVtsuruVoiceAPI = computed(() => {
|
||||
return settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
|
||||
@@ -191,6 +192,7 @@ async function speak() {
|
||||
if (text) {
|
||||
isSpeaking.value = true
|
||||
readedDanmaku.value++
|
||||
speakingText.value = text
|
||||
console.log(`[TTS] 正在朗读: ${text}`)
|
||||
if (checkTimer) {
|
||||
clearInterval(checkTimer)
|
||||
@@ -258,7 +260,7 @@ function speakFromAPI(text: string) {
|
||||
.replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}`
|
||||
const tempURL = new URL(url)
|
||||
if (isVtsuruVoiceAPI.value && splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 50) {
|
||||
message.error('本站提供的测试接口字数不允许超过 50 字. 内容: [' + tempURL.searchParams.get('text') + ']')
|
||||
message.error('本站提供的测试接口字数不允许超过 100 字. 内容: [' + tempURL.searchParams.get('text') + ']')
|
||||
cancelSpeech()
|
||||
return
|
||||
}
|
||||
@@ -266,6 +268,10 @@ function speakFromAPI(text: string) {
|
||||
nextTick(() => {
|
||||
//apiAudio.value?.load()
|
||||
apiAudio.value?.play().catch((err) => {
|
||||
if (err.toString().startsWith('AbortError')) {
|
||||
return
|
||||
}
|
||||
console.log(err)
|
||||
console.log(err)
|
||||
message.error('无法播放语音:' + err)
|
||||
cancelSpeech()
|
||||
@@ -283,8 +289,14 @@ function cancelSpeech() {
|
||||
checkTimer = undefined
|
||||
}
|
||||
isApiAudioLoading.value = false
|
||||
apiAudio.value?.pause()
|
||||
pauseAPI()
|
||||
EasySpeech.cancel()
|
||||
speakingText.value = ''
|
||||
}
|
||||
function pauseAPI() {
|
||||
if (!apiAudio.value?.paused) {
|
||||
apiAudio.value?.pause()
|
||||
}
|
||||
}
|
||||
function onGetEvent(data: EventModel) {
|
||||
if (!canSpeech.value) {
|
||||
@@ -506,7 +518,7 @@ onUnmounted(() => {
|
||||
<NAlert v-if="!speechSynthesisInfo || !speechSynthesisInfo.speechSynthesis" type="error"> 你的浏览器不支持语音功能 </NAlert>
|
||||
<template v-else>
|
||||
<NSpace vertical>
|
||||
<NAlert v-if="settings.voiceType == 'local'" type="info" closeable >
|
||||
<NAlert v-if="settings.voiceType == 'local'" type="info" closeable>
|
||||
建议在 Edge 浏览器使用
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
@@ -671,7 +683,7 @@ onUnmounted(() => {
|
||||
<NAlert v-if="isVtsuruVoiceAPI" type="success" closable>
|
||||
看起来你正在使用本站提供的测试API (voice.vtsuru.live), 这个接口将会返回
|
||||
<NButton text type="info" tag="a" href="https://space.bilibili.com/5859321" target="_blank"> Xz乔希 </NButton>
|
||||
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试 侵删
|
||||
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试, 不保证可用性. 侵删
|
||||
</NAlert>
|
||||
</NSpace>
|
||||
<br />
|
||||
@@ -689,7 +701,7 @@ onUnmounted(() => {
|
||||
placeholder="API 地址, 例如 xxx.com/voice/bert-vits2?text={{text}}&id=0 (前面不要带https://)"
|
||||
:status="/^(?:https?:\/\/)/.test(settings.voiceAPI?.toLowerCase() ?? '') ? 'error' : 'success'"
|
||||
/>
|
||||
<NButton @click="testAPI" type="info"> 测试 </NButton>
|
||||
<NButton @click="testAPI" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<br /><br />
|
||||
<NSpace vertical>
|
||||
@@ -720,22 +732,22 @@ onUnmounted(() => {
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 弹幕模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.danmakuTemplate" placeholder="弹幕消息" />
|
||||
<NButton @click="test(EventDataTypes.Message)" type="info"> 测试 </NButton>
|
||||
<NButton @click="test(EventDataTypes.Message)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 礼物模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.giftTemplate" placeholder="礼物消息" />
|
||||
<NButton @click="test(EventDataTypes.Gift)" type="info"> 测试 </NButton>
|
||||
<NButton @click="test(EventDataTypes.Gift)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> SC模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.scTemplate" placeholder="SC消息" />
|
||||
<NButton @click="test(EventDataTypes.SC)" type="info"> 测试 </NButton>
|
||||
<NButton @click="test(EventDataTypes.SC)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 上舰模板 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.guardTemplate" placeholder="上舰消息" />
|
||||
<NButton @click="test(EventDataTypes.Guard)" type="info"> 测试 </NButton>
|
||||
<NButton @click="test(EventDataTypes.Guard)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
<NDivider> 设置 </NDivider>
|
||||
|
||||
Reference in New Issue
Block a user