mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
add music request
This commit is contained in:
@@ -98,25 +98,39 @@ export function downloadConfigDirect<T>(name: string) {
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
export async function downloadConfig<T>(name: string) {
|
||||
export async function DownloadConfig<T>(name: string) {
|
||||
try {
|
||||
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||
name: name,
|
||||
})
|
||||
if (resp.code == 200) {
|
||||
console.log('已获取配置文件: ' + name)
|
||||
return JSON.parse(resp.data) as T
|
||||
return {
|
||||
msg: undefined,
|
||||
data: JSON.parse(resp.data) as T,
|
||||
}
|
||||
} else if (resp.code == 404) {
|
||||
console.error(`未找到名为 ${name} 的配置文件`)
|
||||
return {
|
||||
msg: `未找到名为 ${name} 的配置文件, 需要先上传`,
|
||||
data: undefined,
|
||||
}
|
||||
} else {
|
||||
console.error(`无法获取配置文件 [${name}]: ` + resp.message)
|
||||
return {
|
||||
msg: `无法获取配置文件 [${name}]: ` + resp.message,
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`无法获取配置文件 [${name}]: ` + err)
|
||||
return {
|
||||
msg: `无法获取配置文件 [${name}]: ` + err,
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
export async function uploadConfig(name: string, data: unknown) {
|
||||
export async function UploadConfig(name: string, data: unknown) {
|
||||
try {
|
||||
const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
||||
name: name,
|
||||
|
||||
@@ -178,6 +178,7 @@ export enum SongFrom {
|
||||
Custom,
|
||||
Netease,
|
||||
FiveSing,
|
||||
Kugou
|
||||
}
|
||||
export interface SongsInfo {
|
||||
id: number
|
||||
|
||||
@@ -31,7 +31,7 @@ export const QUEUE_API_URL = { toString: () => `${BASE_API()}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API()}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
|
||||
|
||||
export const ScheduleTemplateMap = {
|
||||
|
||||
@@ -190,22 +190,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
danmaku: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'song-request',
|
||||
name: 'manage-songRequest',
|
||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||
meta: {
|
||||
title: '弹幕点歌',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'queue',
|
||||
name: 'manage-liveQueue',
|
||||
component: () => import('@/views/open_live/OpenQueue.vue'),
|
||||
meta: {
|
||||
title: '弹幕排队',
|
||||
title: '排队',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
},
|
||||
@@ -220,6 +210,26 @@ const routes: Array<RouteRecordRaw> = [
|
||||
danmaku: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'song-request',
|
||||
name: 'manage-songRequest',
|
||||
component: () => import('@/views/open_live/SongRequest.vue'),
|
||||
meta: {
|
||||
title: '点歌 (歌势',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'music-request',
|
||||
name: 'manage-musicRequest',
|
||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||
meta: {
|
||||
title: '点歌 (放歌',
|
||||
keepAlive: true,
|
||||
danmaku: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
name: 'manage-live',
|
||||
@@ -270,7 +280,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: 'song-request',
|
||||
name: 'open-live-song-request',
|
||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
||||
component: () => import('@/views/open_live/SongRequest.vue'),
|
||||
meta: {
|
||||
title: '点歌',
|
||||
},
|
||||
@@ -310,7 +320,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'obs-song-request',
|
||||
component: () => import('@/views/obs/SongRequestOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕点歌',
|
||||
title: '弹幕点歌 (歌势',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -321,6 +331,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
title: '弹幕排队',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'music-request',
|
||||
name: 'obs-music-request',
|
||||
component: () => import('@/views/obs/MusicRequestOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕排队 (播放',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
</NSpace>
|
||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||
<NTimeline>
|
||||
<NTimelineItem type="success" title="功能添加" content="弹幕点歌 (点播)" time="2023-12-24" />
|
||||
<NTimelineItem type="success" title="功能添加" content="读弹幕" time="2023-12-17" />
|
||||
<NTimelineItem type="success" title="功能添加" content="直播记录" time="2023-12-3" />
|
||||
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
||||
|
||||
@@ -44,8 +44,13 @@ const functions = [
|
||||
icon: Lottery24Filled,
|
||||
},
|
||||
{
|
||||
name: '弹幕点歌',
|
||||
desc: '可以让弹幕进行点歌!',
|
||||
name: '弹幕点歌 (歌势)',
|
||||
desc: '可以让弹幕进行点歌, 然后自己唱',
|
||||
icon: ListCircle,
|
||||
},
|
||||
{
|
||||
name: '弹幕点歌 (点播)',
|
||||
desc: '可以让弹幕进行点歌, 进行搜索后直接播放',
|
||||
icon: ListCircle,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -294,18 +294,53 @@ const menuOptions = [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
to: {
|
||||
name: 'manage-songRequest',
|
||||
},
|
||||
trigger: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'manage-songRequest',
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '点歌(歌势',
|
||||
},
|
||||
),
|
||||
default: () => '歌势用的, 观众点歌之后需要自己唱',
|
||||
},
|
||||
{ default: () => '点歌' },
|
||||
),
|
||||
key: 'manage-songRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
//disabled: accountInfo.value?.isEmailVerified == false,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
trigger: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'manage-musicRequest',
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '点歌(点播',
|
||||
},
|
||||
),
|
||||
default: () => '就是传统的点歌机, 发弹幕后播放指定的歌曲',
|
||||
},
|
||||
),
|
||||
key: 'manage-musicRequest',
|
||||
icon: renderIcon(MusicalNote),
|
||||
//disabled: accountInfo.value?.isEmailVerified == false,
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
|
||||
308
src/views/obs/MusicRequestOBS.vue
Normal file
308
src/views/obs/MusicRequestOBS.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import { DanmakuUserInfo, SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { AVATAR_URL, MUSIC_REQUEST_API_URL } from '@/data/constants'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { NDivider, NEmpty, useMessage } from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Vue3Marquee } from 'vue3-marquee'
|
||||
|
||||
type WaitMusicInfo = {
|
||||
from: DanmakuUserInfo
|
||||
music: SongsInfo
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const currentId = computed(() => {
|
||||
return props.id ?? route.query.id
|
||||
})
|
||||
|
||||
const listContainerRef = ref()
|
||||
const footerRef = ref()
|
||||
const footerListRef = ref()
|
||||
const { height, width } = useElementSize(listContainerRef)
|
||||
const footerSize = useElementSize(footerRef)
|
||||
const footerListSize = useElementSize(footerListRef)
|
||||
const itemHeight = 40
|
||||
|
||||
const key = ref(Date.now())
|
||||
|
||||
const originSongs = ref<{ playing?: WaitMusicInfo; waiting: WaitMusicInfo[] }>({
|
||||
waiting: [],
|
||||
})
|
||||
|
||||
async function get() {
|
||||
try {
|
||||
const data = await QueryGetAPI<{ playing: WaitMusicInfo; waiting: WaitMusicInfo[] }>(MUSIC_REQUEST_API_URL + 'get-waiting', {
|
||||
id: currentId.value,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
}
|
||||
} catch (err) {}
|
||||
return {} as { playing: WaitMusicInfo; waiting: WaitMusicInfo[] }
|
||||
}
|
||||
const isMoreThanContainer = computed(() => {
|
||||
return originSongs.value.waiting.length * itemHeight > height.value
|
||||
})
|
||||
async function update() {
|
||||
const r = await get()
|
||||
if (r) {
|
||||
originSongs.value = r
|
||||
}
|
||||
}
|
||||
|
||||
let timer: any
|
||||
onMounted(() => {
|
||||
update()
|
||||
timer = setInterval(update, 2000)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="music-request-background" v-bind="$attrs">
|
||||
<p class="music-request-header">点歌</p>
|
||||
<NDivider class="music-request-divider">
|
||||
<p class="music-request-header-count">已有 {{ originSongs.waiting.length ?? 0 }} 首</p>
|
||||
</NDivider>
|
||||
<div class="music-request-singing-container" :playing="originSongs.playing ? 'true' : 'false'" :from="originSongs.playing?.music.from ?? -1">
|
||||
<div class="music-request-singing-prefix"></div>
|
||||
<template v-if="originSongs.playing">
|
||||
<img class="music-request-singing-avatar" :src="AVATAR_URL + originSongs.playing.from?.uid" referrerpolicy="no-referrer" />
|
||||
<p class="music-request-singing-song-name">{{ originSongs.playing.music.name }}</p>
|
||||
<p class="music-request-singing-name">{{ originSongs.playing.from?.name }}</p>
|
||||
</template>
|
||||
<div v-else class="music-request-singing-empty">暂无点歌</div>
|
||||
<div class="music-request-singing-suffix"></div>
|
||||
</div>
|
||||
<div class="music-request-content" ref="listContainerRef">
|
||||
<template v-if="originSongs.waiting.length > 0">
|
||||
<Vue3Marquee class="music-request-list" :key="key" vertical :pause="!isMoreThanContainer" :duration="20" :style="`height: ${height}px;width: ${width}px;`">
|
||||
<span class="music-request-list-item" :from="item.music.from as number" v-for="(item, index) in originSongs.waiting" :key="item.music.id" :style="`height: ${itemHeight}px`">
|
||||
<div class="music-request-list-item-index" :index="index + 1">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="music-request-list-item-song-name">
|
||||
{{ item.music.name }}
|
||||
</div>
|
||||
<p class="music-request-list-item-name">{{ item.from ? item.from.name : '主播添加' }}</p>
|
||||
<div class="music-request-list-item-level" :has-level="(item.from?.fans_medal_level ?? 0) > 0">
|
||||
{{ `${item.from?.fans_medal_name} ${item.from?.fans_medal_level}` }}
|
||||
</div>
|
||||
</span>
|
||||
</Vue3Marquee>
|
||||
</template>
|
||||
<div v-else style="position: relative; top: 20%">
|
||||
<NEmpty class="music-request-empty" description="暂无人点歌" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.music-request-background {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
min-width: 100px;
|
||||
background-color: #0f0f0f48;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
}
|
||||
.music-request-header {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-shadow:
|
||||
0 0 10px #ca7b7b6e,
|
||||
0 0 20px #ffffff8e,
|
||||
0 0 30px #61606086,
|
||||
0 0 40px rgba(64, 156, 179, 0.555);
|
||||
}
|
||||
.music-request-header-count {
|
||||
color: #ffffffbd;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.music-request-divider {
|
||||
margin: 0 auto;
|
||||
margin-top: -15px;
|
||||
margin-bottom: -15px;
|
||||
width: 90%;
|
||||
}
|
||||
.music-request-singing-container {
|
||||
height: 35px;
|
||||
margin: 0 10px 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.music-request-singing-empty {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #ffffffbe;
|
||||
}
|
||||
.music-request-singing-prefix {
|
||||
border: 2px solid rgb(231, 231, 231);
|
||||
height: 30px;
|
||||
width: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.music-request-singing-container[playing='true'] .music-request-singing-prefix {
|
||||
background-color: #75c37f;
|
||||
animation: animated-border 3s linear infinite;
|
||||
}
|
||||
.music-request-singing-container[playing='false'] .music-request-singing-prefix {
|
||||
background-color: #c37575;
|
||||
}
|
||||
.music-request-singing-avatar {
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
/* 添加无限旋转动画 */
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
/* 网页点歌 */
|
||||
.music-request-singing-container[from='3'] .music-request-singing-avatar {
|
||||
display: none;
|
||||
}
|
||||
.music-request-singing-song-name {
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 80%;
|
||||
}
|
||||
.music-request-singing-name {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.n-divider__line {
|
||||
background-color: #ffffffd5;
|
||||
}
|
||||
.music-request-content {
|
||||
background-color: #0f0f0f4f;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.marquee {
|
||||
justify-items: left;
|
||||
}
|
||||
.music-request-list-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
gap: 10px;
|
||||
}
|
||||
.music-request-list-item-song-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
/* 手动添加 */
|
||||
.music-request-list-item[from='0'] .music-request-list-item-name {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
color: #d2d8d6;
|
||||
font-size: 12px;
|
||||
}
|
||||
.music-request-list-item[from='0'] .music-request-list-item-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 弹幕点歌 */
|
||||
.music-request-list-item[from='1'] {
|
||||
}
|
||||
|
||||
.music-request-list-item-name {
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
color: rgba(204, 204, 204, 0.993);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
.music-request-list-item-index {
|
||||
text-align: center;
|
||||
height: 18px;
|
||||
padding: 2px;
|
||||
min-width: 15px;
|
||||
border-radius: 5px;
|
||||
background-color: #0f0f0f48;
|
||||
color: rgba(204, 204, 204, 0.993);
|
||||
font-size: 12px;
|
||||
}
|
||||
.music-request-list-item-level {
|
||||
text-align: center;
|
||||
height: 18px;
|
||||
padding: 2px;
|
||||
min-width: 15px;
|
||||
border-radius: 5px;
|
||||
background-color: #0f0f0f48;
|
||||
color: rgba(204, 204, 204, 0.993);
|
||||
font-size: 12px;
|
||||
}
|
||||
.music-request-list-item-level[has-level='false'] {
|
||||
display: none;
|
||||
}
|
||||
.music-request-tag {
|
||||
display: flex;
|
||||
margin: 5px 0 5px 5px;
|
||||
height: 40px;
|
||||
border-radius: 3px;
|
||||
background-color: #0f0f0f4f;
|
||||
padding: 4px;
|
||||
padding-right: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: left;
|
||||
}
|
||||
.music-request-tag-key {
|
||||
font-style: italic;
|
||||
color: rgb(211, 211, 211);
|
||||
font-size: 12px;
|
||||
}
|
||||
.music-request-tag-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { EventModel, SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
||||
import { DanmakuUserInfo, EventModel, FunctionTypes, SongFrom, SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { MUSIC_REQUEST_API_URL } from '@/data/constants'
|
||||
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { List } from 'linqts'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCheckbox,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NDivider,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NInputNumber,
|
||||
NLi,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NPopconfirm,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
NTag,
|
||||
NText,
|
||||
NTransfer,
|
||||
NUl,
|
||||
NVirtualList,
|
||||
SelectOption,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import APlayer from 'vue3-aplayer'
|
||||
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
|
||||
artist: string
|
||||
src: string
|
||||
pic: string
|
||||
lrc: string
|
||||
}
|
||||
type WaitMusicInfo = {
|
||||
from: DanmakuUserInfo
|
||||
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 cooldown = useStorage<{ [id: number]: number }>('Setting.MusicRequest.Cooldown', {})
|
||||
|
||||
const props = defineProps<{
|
||||
client: DanmakuClient
|
||||
@@ -29,35 +94,559 @@ const props = defineProps<{
|
||||
isOpenLive?: boolean
|
||||
}>()
|
||||
|
||||
const deviceList = ref<SelectOption[]>([])
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
|
||||
const aplayer = ref()
|
||||
|
||||
const musics = ref<Music[]>([])
|
||||
const listening = ref(false)
|
||||
|
||||
function onGetEvent(data: EventModel) {}
|
||||
function searchMusic(keyword: string) {
|
||||
QueryGetAPI<SongsInfo>(MUSIC_REQUEST_API_URL + 'search-kugou', {
|
||||
keyword: keyword,
|
||||
}).then((data) => {
|
||||
if (data.code == 200) {
|
||||
musics.value.push({
|
||||
title: data.data.name,
|
||||
artist: data.data.author?.join('/'),
|
||||
src: data.data.url,
|
||||
pic: data.data.cover ?? '',
|
||||
lrc: '',
|
||||
})
|
||||
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 isLoading = ref(false)
|
||||
|
||||
const showNeteaseModal = ref(false)
|
||||
const showOBSModal = ref(false)
|
||||
const neteaseIdInput = ref('')
|
||||
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 neteaseSongs = ref<SongsInfo[]>([])
|
||||
const neteaseSongsOptions = computed(() => {
|
||||
return neteaseSongs.value.map((s) => ({
|
||||
label: `${s.name} - ${s.author.join('/')}`,
|
||||
value: s.key,
|
||||
disabled: originMusics.value.findIndex((exist) => exist.id == s.id) > -1,
|
||||
}))
|
||||
})
|
||||
const selectedNeteaseSongs = ref<string[]>([])
|
||||
|
||||
async function get() {
|
||||
try {
|
||||
const data = await QueryGetAPI<SongsInfo[]>(MUSIC_REQUEST_API_URL + 'get')
|
||||
if (data.code == 200) {
|
||||
console.log('[OPEN-LIVE-Music-Request] 已获取所有数据')
|
||||
return new List(data.data).OrderByDescending((s) => s.createTime).ToArray()
|
||||
} else {
|
||||
message.error('无法获取数据: ' + data.message)
|
||||
return []
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('无法获取数据')
|
||||
}
|
||||
return []
|
||||
}
|
||||
async function searchMusic(keyword: string) {
|
||||
const data = await QueryGetAPI<SongsInfo>(MUSIC_REQUEST_API_URL + 'search-' + settings.value.platform, {
|
||||
keyword: keyword,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
function switchTo() {}
|
||||
async function getNeteaseSongList() {
|
||||
isLoading.value = true
|
||||
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get-netease-list', {
|
||||
id: neteaseSongListId.value,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
neteaseSongs.value = data.data
|
||||
message.success(`成功获取歌曲信息, 共 ${data.data.length} 条, 歌单中已存在 ${neteaseSongsOptions.value.filter((s) => s.disabled).length} 首`)
|
||||
} else {
|
||||
message.error('获取歌单失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('获取歌单失败: ' + err)
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
async function addNeteaseSongs() {
|
||||
isLoading.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} 首歌曲`)
|
||||
originMusics.value.push(...data.data)
|
||||
} else {
|
||||
message.error('添加失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('添加失败')
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
async function addSongs(songsShoudAdd: SongsInfo[], from: SongFrom) {
|
||||
return QueryPostAPI<SongsInfo[]>(
|
||||
MUSIC_REQUEST_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,
|
||||
Cover: s.cover,
|
||||
})),
|
||||
)
|
||||
}
|
||||
function delMusic(song: SongsInfo) {
|
||||
QueryPostAPI(MUSIC_REQUEST_API_URL + 'del', [song.key])
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已删除')
|
||||
originMusics.value = originMusics.value.filter((s) => s.key != song.key)
|
||||
} else {
|
||||
message.error('删除失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('删除失败' + err)
|
||||
})
|
||||
}
|
||||
function clearMusic() {
|
||||
QueryGetAPI(MUSIC_REQUEST_API_URL + 'clear')
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('已清空')
|
||||
originMusics.value = []
|
||||
} else {
|
||||
message.error('清空失败: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('清空失败' + err)
|
||||
})
|
||||
}
|
||||
async function uploadConfig() {
|
||||
await UploadConfig('MusicRequest', JSON.stringify(settings.value))
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
message.success('已保存至服务器')
|
||||
} else {
|
||||
message.error('保存失败')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('保存失败')
|
||||
})
|
||||
}
|
||||
async function downloadConfig() {
|
||||
await DownloadConfig<MusicRequestSettings>('MusicRequest')
|
||||
.then((data) => {
|
||||
if (data.msg) {
|
||||
message.error('获取失败: ' + data.msg)
|
||||
} else {
|
||||
settings.value = data.data
|
||||
message.success('已获取配置文件')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('获取失败')
|
||||
})
|
||||
}
|
||||
function startListen() {
|
||||
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest)) {
|
||||
message.warning('使用这个点歌则需要先关闭歌势点歌 (SongRequest)')
|
||||
return
|
||||
}
|
||||
listening.value = true
|
||||
message.success('开始监听')
|
||||
}
|
||||
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]) {
|
||||
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)} 秒`)
|
||||
return
|
||||
}
|
||||
}
|
||||
const name = data.msg.replace(new RegExp(settings.value.orderPrefix.trimStart()), '').trim()
|
||||
const result = await searchMusic(name)
|
||||
if (result) {
|
||||
if (settings.value.blacklist.includes(result.name)) {
|
||||
message.warning(`[${data.name}] 点歌失败,因为 ${result.name} 在黑名单中`)
|
||||
return
|
||||
}
|
||||
cooldown.value[data.uid] = Date.now()
|
||||
const music = {
|
||||
from: {
|
||||
name: data.name,
|
||||
uid: data.uid,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
aplayer.value?.pause()
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
function playWaitingMusic(music: WaitMusicInfo) {
|
||||
isPlayingOrderMusic.value = true
|
||||
const index = waitingMusics.value.indexOf(music)
|
||||
if (index > -1) {
|
||||
waitingMusics.value.splice(index, 1)
|
||||
}
|
||||
aplayer.value?.pause()
|
||||
currentOriginMusic.value = music
|
||||
aplayer.value?.audio.addEventListener(
|
||||
'loadeddata',
|
||||
() => {
|
||||
aplayer.value?.play()
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
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,
|
||||
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
|
||||
}
|
||||
async function getOutputDevice() {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
// 请保留此代码以确保用户授权访问设备
|
||||
audio: true,
|
||||
})
|
||||
|
||||
const list = await navigator.mediaDevices.enumerateDevices()
|
||||
|
||||
deviceList.value = list.filter((device) => device.kind === 'audiooutput').map((d) => ({ label: d.label, value: d.deviceId }))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error('获取音频输出设备失败: ' + err)
|
||||
}
|
||||
}
|
||||
function blockMusic(song: SongsInfo) {
|
||||
settings.value.blacklist.push(song.name)
|
||||
waitingMusics.value.splice(
|
||||
waitingMusics.value.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,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let timer: number
|
||||
onMounted(async () => {
|
||||
props.client.onEvent('danmaku', onGetEvent)
|
||||
|
||||
if (originMusics.value.length > 0) {
|
||||
currentMusic.value = 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()
|
||||
}
|
||||
}
|
||||
timer = setInterval(updateWaiting, 2000)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
props.client.offEvent('danmaku', onGetEvent)
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<APlayer :list="musics" ref="aplayer" />
|
||||
<NSpace>
|
||||
<NAlert type="info"> 搜索时会优先选择非VIP歌曲, 所以点到付费曲目时可能会是猴版或者各种奇怪的歌 </NAlert>
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<NSpace>
|
||||
<NButton
|
||||
@click="listening ? stopListen() : startListen()"
|
||||
:type="listening ? 'error' : 'primary'"
|
||||
:style="{ animation: listening ? 'animated-border 2.5s infinite' : '' }"
|
||||
data-umami-event="Use Music Request"
|
||||
:data-umami-event-uid="accountInfo?.biliId"
|
||||
>
|
||||
{{ listening ? '停止监听' : '开始监听' }}
|
||||
</NButton>
|
||||
<NButton @click="showOBSModal = true" type="info"> OBS组件 </NButton>
|
||||
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入空闲歌单 </NButton>
|
||||
|
||||
<NButton @click="uploadConfig" type="primary" secondary :disabled="!accountInfo"> 保存配置到服务器 </NButton>
|
||||
<NPopconfirm @positive-click="downloadConfig">
|
||||
<template #trigger>
|
||||
<NButton type="primary" secondary :disabled="!accountInfo"> 从服务器获取配置 </NButton>
|
||||
</template>
|
||||
这将覆盖当前设置, 确定?
|
||||
</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>
|
||||
<NList v-else size="small" bordered>
|
||||
<NListItem v-for="item in 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="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
|
||||
<span>
|
||||
<NTag v-if="item.music.from == SongFrom.Netease" type="success" size="small"> 网易</NTag>
|
||||
<NTag v-else-if="item.music.from == SongFrom.Kugou" type="success" size="small"> 酷狗</NTag>
|
||||
</span>
|
||||
<NText>
|
||||
{{ item.from.name }}
|
||||
</NText>
|
||||
<NText depth="3"> {{ item.music.name }} - {{ item.music.author?.join('/') }} </NText>
|
||||
</NSpace>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
<NDivider />
|
||||
<NTabs>
|
||||
<NTabPane name="settings" tab="设置">
|
||||
<NSpace vertical>
|
||||
<NSpace align="center">
|
||||
<NRadioGroup v-model:value="settings.platform">
|
||||
<NRadioButton value="netease"> 网易云 </NRadioButton>
|
||||
<NRadioButton value="kugou"> 酷狗 </NRadioButton>
|
||||
</NRadioGroup>
|
||||
<NInputGroup style="width: 250px">
|
||||
<NInputGroupLabel> 点歌弹幕前缀 </NInputGroupLabel>
|
||||
<NInput v-model:value="settings.orderPrefix" />
|
||||
</NInputGroup>
|
||||
<NCheckbox
|
||||
:checked="settings.orderCooldown != undefined"
|
||||
@update:checked="
|
||||
(checked: boolean) => {
|
||||
settings.orderCooldown = checked ? 300 : undefined
|
||||
}
|
||||
"
|
||||
>
|
||||
是否启用点歌冷却
|
||||
</NCheckbox>
|
||||
<NInputGroup v-if="settings.orderCooldown" style="width: 200px">
|
||||
<NInputGroupLabel> 冷却时间 (秒) </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="settings.orderCooldown"
|
||||
@update:value="
|
||||
(value) => {
|
||||
if (!value || value <= 0) settings.orderCooldown = undefined
|
||||
}
|
||||
"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
<NSpace>
|
||||
<NCheckbox v-model:checked="settings.playMusicWhenFree"> 空闲时播放空闲歌单 </NCheckbox>
|
||||
<NCheckbox v-model:checked="settings.orderMusicFirst"> 优先播放点歌 </NCheckbox>
|
||||
</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" />
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NTabPane>
|
||||
<NTabPane name="list" tab="闲置歌单">
|
||||
<NSpace>
|
||||
<NPopconfirm @positive-click="clearMusic">
|
||||
<template #trigger>
|
||||
<NButton type="error"> 清空 </NButton>
|
||||
</template>
|
||||
确定清空吗?
|
||||
</NPopconfirm>
|
||||
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入 </NButton>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 15px 0 10px 0" />
|
||||
<NEmpty v-if="musics.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;`">
|
||||
<NSpace align="center" style="width: 100%">
|
||||
<NPopconfirm @positive-click="delMusic(item)">
|
||||
<template #trigger>
|
||||
<NButton type="error" secondary size="small"> 删除 </NButton>
|
||||
</template>
|
||||
确定删除?
|
||||
</NPopconfirm>
|
||||
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
|
||||
</NSpace>
|
||||
</p>
|
||||
</template>
|
||||
</NVirtualList>
|
||||
</NTabPane>
|
||||
<NTabPane name="blacklist" tab="黑名单">
|
||||
<NList>
|
||||
<NListItem v-for="item in settings.blacklist">
|
||||
<NSpace align="center" style="width: 100%">
|
||||
<NButton @click="settings.blacklist.splice(settings.blacklist.indexOf(item), 1)" type="error" secondary size="small"> 删除 </NButton>
|
||||
<NText> {{ item }} </NText>
|
||||
</NSpace>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<NDivider style="height: 100px" />
|
||||
<NModal v-model:show="showNeteaseModal" preset="card" :title="`获取歌单`" style="max-width: 600px">
|
||||
<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" :loading="isLoading"> 获取 </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" :loading="isLoading"> 添加到歌单 | {{ selectedNeteaseSongs.length }} 首 </NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
<NModal v-model:show="showOBSModal" title="OBS组件" preset="card" style="width: 800px">
|
||||
<NAlert title="这是什么? " type="info"> 将等待队列以及结果显示在OBS中 </NAlert>
|
||||
<NDivider> 浏览 </NDivider>
|
||||
<div style="height: 500px; width: 280px; position: relative; margin: 0 auto">
|
||||
<MusicRequestOBS :id="accountInfo?.id" />
|
||||
</div>
|
||||
<br />
|
||||
<NInput :value="'https://vtsuru.live/obs/music-request?id=' + accountInfo?.id" />
|
||||
<NDivider />
|
||||
<NCollapse>
|
||||
<NCollapseItem title="使用说明">
|
||||
<NUl>
|
||||
<NLi>在 OBS 来源中添加源, 选择 浏览器</NLi>
|
||||
<NLi>在 URL 栏填入上方链接</NLi>
|
||||
<NLi>根据自己的需要调整宽度和高度 (这里是宽 280px 高 500px)</NLi>
|
||||
<NLi>完事</NLi>
|
||||
</NUl>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.aplayer-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -320,7 +320,6 @@ async function updateSongStatus(song: SongRequestInfo, status: SongRequestStatus
|
||||
}
|
||||
|
||||
function onGetDanmaku(danmaku: DanmakuInfo) {
|
||||
console.log(danmaku)
|
||||
if (checkMessage(danmaku.msg)) {
|
||||
addSong({
|
||||
msg: danmaku.msg,
|
||||
@@ -357,6 +356,9 @@ function onGetSC(danmaku: SCInfo) {
|
||||
}
|
||||
}
|
||||
function checkMessage(msg: string) {
|
||||
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) != true) {
|
||||
return false
|
||||
}
|
||||
return msg
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
Reference in New Issue
Block a user