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,9 +1,16 @@
|
|||||||
import mitt, { Emitter } from 'mitt'
|
import mitt, { Emitter } from 'mitt'
|
||||||
|
import { Music } from './store/useMusicRequest';
|
||||||
|
|
||||||
declare type MittType<T = any> = {
|
declare type MittType<T = any> = {
|
||||||
onOpenTemplateSettings: {
|
onOpenTemplateSettings: {
|
||||||
template: string,
|
template: string,
|
||||||
|
|
||||||
|
},
|
||||||
|
onMusicRequestPlayerEnded: {
|
||||||
|
music: Music
|
||||||
|
}
|
||||||
|
onMusicRequestPlayNextWaitingMusic: {
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 类型
|
// 类型
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'manage-musicRequest',
|
name: 'manage-musicRequest',
|
||||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '点歌 (放歌',
|
title: '点歌 (点播',
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
danmaku: 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 { QueryGetAPI } from '@/api/query'
|
||||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||||
import { ACCOUNT_API_URL } from '@/data/constants'
|
import { ACCOUNT_API_URL } from '@/data/constants'
|
||||||
|
import { useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||||
import {
|
import {
|
||||||
CalendarClock24Filled,
|
CalendarClock24Filled,
|
||||||
Chat24Filled,
|
Chat24Filled,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
NIcon,
|
NIcon,
|
||||||
NLayout,
|
NLayout,
|
||||||
NLayoutContent,
|
NLayoutContent,
|
||||||
|
NLayoutFooter,
|
||||||
NLayoutHeader,
|
NLayoutHeader,
|
||||||
NLayoutSider,
|
NLayoutSider,
|
||||||
NMenu,
|
NMenu,
|
||||||
@@ -37,12 +39,14 @@ import {
|
|||||||
NSpace,
|
NSpace,
|
||||||
NSpin,
|
NSpin,
|
||||||
NSwitch,
|
NSwitch,
|
||||||
|
NTag,
|
||||||
NText,
|
NText,
|
||||||
NTooltip,
|
NTooltip,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} 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 { RouterLink, useRoute } from 'vue-router'
|
||||||
|
import APlayer from 'vue3-aplayer'
|
||||||
import DanmakuLayout from './manage/DanmakuLayout.vue'
|
import DanmakuLayout from './manage/DanmakuLayout.vue'
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
@@ -60,9 +64,18 @@ const type = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
const cookie = useStorage('JWT_Token', '')
|
const cookie = useStorage('JWT_Token', '')
|
||||||
|
const musicRquestStore = useMusicRequestProvider()
|
||||||
|
|
||||||
const canResendEmail = ref(false)
|
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) {
|
function renderIcon(icon: unknown) {
|
||||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
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',
|
key: 'manage-danmaku',
|
||||||
@@ -428,7 +441,6 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</NPageHeader>
|
</NPageHeader>
|
||||||
</NLayoutHeader>
|
</NLayoutHeader>
|
||||||
<NScrollbar x-scrollable>
|
|
||||||
<NLayout has-sider>
|
<NLayout has-sider>
|
||||||
<NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false">
|
<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">
|
<NSpace justify="center" style="margin-top: 16px">
|
||||||
@@ -467,9 +479,9 @@ onMounted(() => {
|
|||||||
</NText>
|
</NText>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NLayoutSider>
|
</NLayoutSider>
|
||||||
<NScrollbar style="height: calc(100vh - 50px)">
|
|
||||||
<NLayout>
|
<NLayout>
|
||||||
<div style="box-sizing: border-box; padding: 20px; min-width: 300px">
|
<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 }">
|
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
|
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
|
||||||
@@ -499,11 +511,33 @@ onMounted(() => {
|
|||||||
</NAlert>
|
</NAlert>
|
||||||
</template>
|
</template>
|
||||||
<NBackTop />
|
<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>
|
</div>
|
||||||
|
</NLayoutFooter>
|
||||||
</NLayout>
|
</NLayout>
|
||||||
</NScrollbar>
|
|
||||||
</NLayout>
|
</NLayout>
|
||||||
</NScrollbar>
|
|
||||||
</NLayout>
|
</NLayout>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NLayoutContent style="display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 50px; height: 100%; box-sizing: border-box">
|
<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 { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
|
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||||
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
|
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
|
||||||
|
import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { List } from 'linqts'
|
import { List } from 'linqts'
|
||||||
import {
|
import {
|
||||||
@@ -37,27 +38,10 @@ import {
|
|||||||
SelectOption,
|
SelectOption,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import APlayer from 'vue3-aplayer'
|
|
||||||
import { clearInterval, setInterval } from 'worker-timers'
|
import { clearInterval, setInterval } from 'worker-timers'
|
||||||
import MusicRequestOBS from '../obs/MusicRequestOBS.vue'
|
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 = {
|
type Music = {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
@@ -71,21 +55,11 @@ type WaitMusicInfo = {
|
|||||||
music: SongsInfo
|
music: SongsInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
|
const settings = computed(() => {
|
||||||
playMusicWhenFree: true,
|
return musicRquestStore.settings
|
||||||
repeat: 'repeat-all',
|
|
||||||
listMaxHeight: '300',
|
|
||||||
shuffle: true,
|
|
||||||
volume: 0.5,
|
|
||||||
|
|
||||||
orderPrefix: '点歌',
|
|
||||||
orderCooldown: 600,
|
|
||||||
orderMusicFirst: true,
|
|
||||||
platform: 'netease',
|
|
||||||
|
|
||||||
blacklist: [],
|
|
||||||
})
|
})
|
||||||
const cooldown = useStorage<{ [id: number]: number }>('Setting.MusicRequest.Cooldown', {})
|
const cooldown = useStorage<{ [id: number]: number }>('Setting.MusicRequest.Cooldown', {})
|
||||||
|
const musicRquestStore = useMusicRequestProvider()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
client: DanmakuClient
|
client: DanmakuClient
|
||||||
@@ -99,28 +73,9 @@ const deviceList = ref<SelectOption[]>([])
|
|||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const aplayer = ref()
|
|
||||||
|
|
||||||
const listening = ref(false)
|
const listening = ref(false)
|
||||||
|
const originMusics = computed(() => {
|
||||||
const originMusics = ref<SongsInfo[]>(await get())
|
return musicRquestStore.originMusics
|
||||||
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 isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -242,7 +197,7 @@ function delMusic(song: SongsInfo) {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
message.success('已删除')
|
message.success('已删除')
|
||||||
originMusics.value = originMusics.value.filter((s) => s.key != song.key)
|
musicRquestStore.originMusics = originMusics.value.filter((s) => s.key != song.key)
|
||||||
} else {
|
} else {
|
||||||
message.error('删除失败: ' + data.message)
|
message.error('删除失败: ' + data.message)
|
||||||
}
|
}
|
||||||
@@ -256,7 +211,7 @@ function clearMusic() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
message.success('已清空')
|
message.success('已清空')
|
||||||
originMusics.value = []
|
musicRquestStore.originMusics = []
|
||||||
} else {
|
} else {
|
||||||
message.error('清空失败: ' + data.message)
|
message.error('清空失败: ' + data.message)
|
||||||
}
|
}
|
||||||
@@ -284,7 +239,7 @@ async function downloadConfig() {
|
|||||||
if (data.msg) {
|
if (data.msg) {
|
||||||
message.error('获取失败: ' + data.msg)
|
message.error('获取失败: ' + data.msg)
|
||||||
} else {
|
} else {
|
||||||
settings.value = data.data
|
musicRquestStore.settings = data.data ?? ({} as MusicRequestSettings)
|
||||||
message.success('已获取配置文件')
|
message.success('已获取配置文件')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -304,10 +259,9 @@ function stopListen() {
|
|||||||
listening.value = false
|
listening.value = false
|
||||||
message.success('已停止监听')
|
message.success('已停止监听')
|
||||||
}
|
}
|
||||||
const isPlayingOrderMusic = ref(false)
|
|
||||||
async function onGetEvent(data: EventModel) {
|
async function onGetEvent(data: EventModel) {
|
||||||
if (!listening.value || !checkMessage(data.msg)) return
|
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]
|
const lastRequest = cooldown.value[data.uid]
|
||||||
if (Date.now() - lastRequest < settings.value.orderCooldown * 1000) {
|
if (Date.now() - lastRequest < settings.value.orderCooldown * 1000) {
|
||||||
message.info(`[${data.name}] 冷却中,距离下次点歌还有 ${((settings.value.orderCooldown * 1000 - (Date.now() - lastRequest)) / 1000).toFixed(1)} 秒`)
|
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,
|
music: result,
|
||||||
} as WaitMusicInfo
|
} as WaitMusicInfo
|
||||||
if ((settings.value.orderMusicFirst && !isPlayingOrderMusic.value) || originMusics.value.length == 0 || aplayer.value?.audio.paused) {
|
musicRquestStore.addWaitingMusic(music)
|
||||||
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('/')}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkMessage(msg: string) {
|
function checkMessage(msg: string) {
|
||||||
return msg.trim().toLowerCase().startsWith(settings.value.orderPrefix.trimStart())
|
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) {
|
function songToMusic(s: SongsInfo) {
|
||||||
return {
|
return {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
@@ -417,32 +321,34 @@ async function getOutputDevice() {
|
|||||||
}
|
}
|
||||||
function blockMusic(song: SongsInfo) {
|
function blockMusic(song: SongsInfo) {
|
||||||
settings.value.blacklist.push(song.name)
|
settings.value.blacklist.push(song.name)
|
||||||
waitingMusics.value.splice(
|
musicRquestStore.waitingMusics.splice(
|
||||||
waitingMusics.value.findIndex((m) => m.music == song),
|
musicRquestStore.waitingMusics.findIndex((m) => m.music == song),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
message.success(`[${song.name}] 已添加到黑名单`)
|
message.success(`[${song.name}] 已添加到黑名单`)
|
||||||
}
|
}
|
||||||
function updateWaiting() {
|
function updateWaiting() {
|
||||||
QueryPostAPI(MUSIC_REQUEST_API_URL + 'update-waiting', {
|
QueryPostAPI(MUSIC_REQUEST_API_URL + 'update-waiting', {
|
||||||
playing: currentOriginMusic.value,
|
playing: musicRquestStore.currentOriginMusic,
|
||||||
waiting: waitingMusics.value,
|
waiting: musicRquestStore.waitingMusics,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let timer: number
|
let timer: number
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
props.client.onEvent('danmaku', onGetEvent)
|
props.client.onEvent('danmaku', onGetEvent)
|
||||||
|
if (musicRquestStore.originMusics.length == 0) {
|
||||||
|
musicRquestStore.originMusics = await get()
|
||||||
|
}
|
||||||
if (originMusics.value.length > 0) {
|
if (originMusics.value.length > 0) {
|
||||||
currentMusic.value = songToMusic(originMusics.value[0])
|
musicRquestStore.currentMusic = songToMusic(originMusics.value[0])
|
||||||
}
|
}
|
||||||
await getOutputDevice()
|
await getOutputDevice()
|
||||||
if (deviceList.value.length > 0) {
|
if (deviceList.value.length > 0) {
|
||||||
if (!deviceList.value.find((d) => d.value == settings.value.deviceId)) {
|
if (!deviceList.value.find((d) => d.value == settings.value.deviceId)) {
|
||||||
settings.value.deviceId = undefined
|
settings.value.deviceId = undefined
|
||||||
} else {
|
} else {
|
||||||
setSinkId()
|
musicRquestStore.setSinkId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timer = setInterval(updateWaiting, 2000)
|
timer = setInterval(updateWaiting, 2000)
|
||||||
@@ -483,26 +389,14 @@ onUnmounted(() => {
|
|||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<NDivider />
|
<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']">
|
<NCollapse :default-expanded-names="['1']">
|
||||||
<NCollapseItem title="队列" name="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>
|
<NList v-else size="small" bordered>
|
||||||
<NListItem v-for="item in waitingMusics">
|
<NListItem v-for="item in musicRquestStore.waitingMusics">
|
||||||
<NSpace align="center">
|
<NSpace align="center">
|
||||||
<NButton @click="playWaitingMusic(item)" type="primary" secondary size="small"> 播放 </NButton>
|
<NButton @click="musicRquestStore.playMusic(item.music)" type="primary" secondary size="small"> 播放 </NButton>
|
||||||
<NButton @click="waitingMusics.splice(waitingMusics.indexOf(item), 1)" type="error" 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>
|
<NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
|
||||||
<span>
|
<span>
|
||||||
<NTag v-if="item.music.from == SongFrom.Netease" type="success" size="small"> 网易</NTag>
|
<NTag v-if="item.music.from == SongFrom.Netease" type="success" size="small"> 网易</NTag>
|
||||||
@@ -558,7 +452,13 @@ onUnmounted(() => {
|
|||||||
</NSpace>
|
</NSpace>
|
||||||
<NSpace>
|
<NSpace>
|
||||||
<NButton @click="getOutputDevice"> 获取输出设备 </NButton>
|
<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>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
@@ -573,7 +473,7 @@ onUnmounted(() => {
|
|||||||
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入 </NButton>
|
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入 </NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<NDivider style="margin: 15px 0 10px 0" />
|
<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>
|
<NVirtualList v-else :style="`max-height: 1000px`" :item-size="30" :items="originMusics" item-resizable>
|
||||||
<template #default="{ item, index }">
|
<template #default="{ item, index }">
|
||||||
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`">
|
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`">
|
||||||
@@ -584,6 +484,8 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
确定删除?
|
确定删除?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
|
|
||||||
|
<NButton type="info" secondary size="small" @click="musicRquestStore.playMusic(item)"> 播放 </NButton>
|
||||||
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
|
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const voiceOptions = computed(() => {
|
|||||||
.ToArray()
|
.ToArray()
|
||||||
})
|
})
|
||||||
const isSpeaking = ref(false)
|
const isSpeaking = ref(false)
|
||||||
|
const speakingText = ref('')
|
||||||
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
|
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
|
||||||
const isVtsuruVoiceAPI = computed(() => {
|
const isVtsuruVoiceAPI = computed(() => {
|
||||||
return settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
|
return settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
|
||||||
@@ -191,6 +192,7 @@ async function speak() {
|
|||||||
if (text) {
|
if (text) {
|
||||||
isSpeaking.value = true
|
isSpeaking.value = true
|
||||||
readedDanmaku.value++
|
readedDanmaku.value++
|
||||||
|
speakingText.value = text
|
||||||
console.log(`[TTS] 正在朗读: ${text}`)
|
console.log(`[TTS] 正在朗读: ${text}`)
|
||||||
if (checkTimer) {
|
if (checkTimer) {
|
||||||
clearInterval(checkTimer)
|
clearInterval(checkTimer)
|
||||||
@@ -258,7 +260,7 @@ function speakFromAPI(text: string) {
|
|||||||
.replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}`
|
.replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}`
|
||||||
const tempURL = new URL(url)
|
const tempURL = new URL(url)
|
||||||
if (isVtsuruVoiceAPI.value && splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 50) {
|
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()
|
cancelSpeech()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,10 @@ function speakFromAPI(text: string) {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
//apiAudio.value?.load()
|
//apiAudio.value?.load()
|
||||||
apiAudio.value?.play().catch((err) => {
|
apiAudio.value?.play().catch((err) => {
|
||||||
|
if (err.toString().startsWith('AbortError')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(err)
|
||||||
console.log(err)
|
console.log(err)
|
||||||
message.error('无法播放语音:' + err)
|
message.error('无法播放语音:' + err)
|
||||||
cancelSpeech()
|
cancelSpeech()
|
||||||
@@ -283,8 +289,14 @@ function cancelSpeech() {
|
|||||||
checkTimer = undefined
|
checkTimer = undefined
|
||||||
}
|
}
|
||||||
isApiAudioLoading.value = false
|
isApiAudioLoading.value = false
|
||||||
apiAudio.value?.pause()
|
pauseAPI()
|
||||||
EasySpeech.cancel()
|
EasySpeech.cancel()
|
||||||
|
speakingText.value = ''
|
||||||
|
}
|
||||||
|
function pauseAPI() {
|
||||||
|
if (!apiAudio.value?.paused) {
|
||||||
|
apiAudio.value?.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function onGetEvent(data: EventModel) {
|
function onGetEvent(data: EventModel) {
|
||||||
if (!canSpeech.value) {
|
if (!canSpeech.value) {
|
||||||
@@ -671,7 +683,7 @@ onUnmounted(() => {
|
|||||||
<NAlert v-if="isVtsuruVoiceAPI" type="success" closable>
|
<NAlert v-if="isVtsuruVoiceAPI" type="success" closable>
|
||||||
看起来你正在使用本站提供的测试API (voice.vtsuru.live), 这个接口将会返回
|
看起来你正在使用本站提供的测试API (voice.vtsuru.live), 这个接口将会返回
|
||||||
<NButton text type="info" tag="a" href="https://space.bilibili.com/5859321" target="_blank"> Xz乔希 </NButton>
|
<NButton text type="info" tag="a" href="https://space.bilibili.com/5859321" target="_blank"> Xz乔希 </NButton>
|
||||||
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试 侵删
|
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试, 不保证可用性. 侵删
|
||||||
</NAlert>
|
</NAlert>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<br />
|
<br />
|
||||||
@@ -689,7 +701,7 @@ onUnmounted(() => {
|
|||||||
placeholder="API 地址, 例如 xxx.com/voice/bert-vits2?text={{text}}&id=0 (前面不要带https://)"
|
placeholder="API 地址, 例如 xxx.com/voice/bert-vits2?text={{text}}&id=0 (前面不要带https://)"
|
||||||
:status="/^(?:https?:\/\/)/.test(settings.voiceAPI?.toLowerCase() ?? '') ? 'error' : 'success'"
|
:status="/^(?:https?:\/\/)/.test(settings.voiceAPI?.toLowerCase() ?? '') ? 'error' : 'success'"
|
||||||
/>
|
/>
|
||||||
<NButton @click="testAPI" type="info"> 测试 </NButton>
|
<NButton @click="testAPI" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
|
||||||
</NInputGroup>
|
</NInputGroup>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<NSpace vertical>
|
<NSpace vertical>
|
||||||
@@ -720,22 +732,22 @@ onUnmounted(() => {
|
|||||||
<NInputGroup>
|
<NInputGroup>
|
||||||
<NInputGroupLabel> 弹幕模板 </NInputGroupLabel>
|
<NInputGroupLabel> 弹幕模板 </NInputGroupLabel>
|
||||||
<NInput v-model:value="settings.danmakuTemplate" placeholder="弹幕消息" />
|
<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>
|
||||||
<NInputGroup>
|
<NInputGroup>
|
||||||
<NInputGroupLabel> 礼物模板 </NInputGroupLabel>
|
<NInputGroupLabel> 礼物模板 </NInputGroupLabel>
|
||||||
<NInput v-model:value="settings.giftTemplate" placeholder="礼物消息" />
|
<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>
|
||||||
<NInputGroup>
|
<NInputGroup>
|
||||||
<NInputGroupLabel> SC模板 </NInputGroupLabel>
|
<NInputGroupLabel> SC模板 </NInputGroupLabel>
|
||||||
<NInput v-model:value="settings.scTemplate" placeholder="SC消息" />
|
<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>
|
||||||
<NInputGroup>
|
<NInputGroup>
|
||||||
<NInputGroupLabel> 上舰模板 </NInputGroupLabel>
|
<NInputGroupLabel> 上舰模板 </NInputGroupLabel>
|
||||||
<NInput v-model:value="settings.guardTemplate" placeholder="上舰消息" />
|
<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>
|
</NInputGroup>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<NDivider> 设置 </NDivider>
|
<NDivider> 设置 </NDivider>
|
||||||
|
|||||||
Reference in New Issue
Block a user