diff --git a/src/mitt.ts b/src/mitt.ts
index a17d805..9e1fa6f 100644
--- a/src/mitt.ts
+++ b/src/mitt.ts
@@ -1,8 +1,15 @@
import mitt, { Emitter } from 'mitt'
+import { Music } from './store/useMusicRequest';
declare type MittType = {
onOpenTemplateSettings: {
template: string,
+
+ },
+ onMusicRequestPlayerEnded: {
+ music: Music
+ }
+ onMusicRequestPlayNextWaitingMusic: {
}
};
diff --git a/src/router/index.ts b/src/router/index.ts
index 6b7ee05..b8ccbaf 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -226,7 +226,7 @@ const routes: Array = [
name: 'manage-musicRequest',
component: () => import('@/views/open_live/MusicRequest.vue'),
meta: {
- title: '点歌 (放歌',
+ title: '点歌 (点播',
keepAlive: true,
danmaku: true,
},
diff --git a/src/store/useMusicRequest.ts b/src/store/useMusicRequest.ts
new file mode 100644
index 0000000..6f517ec
--- /dev/null
+++ b/src/store/useMusicRequest.ts
@@ -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('Setting.MusicRequest.Waiting', [])
+ const originMusics = ref([])
+ const aplayerMusics = computed(() => originMusics.value.map((m) => songToMusic(m)))
+ const currentMusic = ref({
+ id: -1,
+ title: '',
+ artist: '',
+ src: '',
+ pic: '',
+ lrc: '',
+ } as Music)
+ const currentOriginMusic = ref()
+ const isPlayingOrderMusic = ref(false)
+ const aplayerRef = ref()
+ const settings = useStorage('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,
+ }
+})
diff --git a/src/views/ManageLayout.vue b/src/views/ManageLayout.vue
index 59662c4..5866727 100644
--- a/src/views/ManageLayout.vue
+++ b/src/views/ManageLayout.vue
@@ -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(() => {
-
-
-
-
-
-
-
-
- 面板
-
-
-
-
-
-
+
+
+
+
+
+
+
+ 面板
+
+
+
+
+
+
+
+
+
+ 反馈
+
+
+
+
+
+ 有更多功能建议请
+ 反馈
+
+
+ 关于本站
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- 反馈
-
-
-
-
-
- 有更多功能建议请
- 反馈
-
-
- 关于本站
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 请进行邮箱验证
-
-
- 重新发送验证邮件
-
+
+
+
+
+
+ 请进行邮箱验证
+
+
+ 重新发送验证邮件
+
-
-
- 登出
-
- 确定登出?
-
-
-
-
-
-
-
+
+
+ 登出
+
+ 确定登出?
+
+
+
+
+
+
+
+
+
+
+ 队列: {{ musicRquestStore.waitingMusics.length }}
+ 0 ? musicRquestStore.onMusicEnd() : musicRquestStore.aplayerRef?.onAudioEnded()"> 下一首
+
+
+
-
+
diff --git a/src/views/open_live/MusicRequest.vue b/src/views/open_live/MusicRequest.vue
index e7ef8e3..d1b7d93 100644
--- a/src/views/open_live/MusicRequest.vue
+++ b/src/views/open_live/MusicRequest.vue
@@ -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('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([])
const accountInfo = useAccount()
const message = useMessage()
-const aplayer = ref()
-
const listening = ref(false)
-
-const originMusics = ref(await get())
-const waitingMusics = useStorage('Setting.MusicRequest.Waiting', [])
-const musics = computed(() => {
- return originMusics.value.map((s) => songToMusic(s))
-})
-const currentMusic = ref({
- id: -1,
- title: '',
- artist: '',
- src: '',
- pic: '',
- lrc: '',
-} as Music)
-const currentOriginMusic = ref()
-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(() => {
-
- 暂无
+ 暂无
-
+
- 播放
- 取消
+ 播放
+ 取消
拉黑
网易
@@ -558,7 +452,13 @@ onUnmounted(() => {
获取输出设备
-
+
@@ -573,7 +473,7 @@ onUnmounted(() => {
从网易云歌单导入
- 暂无
+ 暂无
@@ -584,6 +484,8 @@ onUnmounted(() => {
确定删除?
+
+ 播放
{{ item.name }} - {{ item.author?.join('/') }}
diff --git a/src/views/open_live/ReadDanmaku.vue b/src/views/open_live/ReadDanmaku.vue
index f267eb4..4c2e082 100644
--- a/src/views/open_live/ReadDanmaku.vue
+++ b/src/views/open_live/ReadDanmaku.vue
@@ -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(() => {
你的浏览器不支持语音功能
-
+
建议在 Edge 浏览器使用
@@ -671,7 +683,7 @@ onUnmounted(() => {
看起来你正在使用本站提供的测试API (voice.vtsuru.live), 这个接口将会返回
Xz乔希
- 训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试 侵删
+ 训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试, 不保证可用性. 侵删
@@ -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'"
/>
- 测试
+ 测试
@@ -720,22 +732,22 @@ onUnmounted(() => {
弹幕模板
- 测试
+ 测试
礼物模板
- 测试
+ 测试
SC模板
- 测试
+ 测试
上舰模板
- 测试
+ 测试
设置