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,
|
name: name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export async function downloadConfig<T>(name: string) {
|
export async function DownloadConfig<T>(name: string) {
|
||||||
try {
|
try {
|
||||||
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
const resp = await QueryGetAPI<string>(VTSURU_API_URL + 'get-config', {
|
||||||
name: name,
|
name: name,
|
||||||
})
|
})
|
||||||
if (resp.code == 200) {
|
if (resp.code == 200) {
|
||||||
console.log('已获取配置文件: ' + name)
|
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) {
|
} else if (resp.code == 404) {
|
||||||
console.error(`未找到名为 ${name} 的配置文件`)
|
console.error(`未找到名为 ${name} 的配置文件`)
|
||||||
|
return {
|
||||||
|
msg: `未找到名为 ${name} 的配置文件, 需要先上传`,
|
||||||
|
data: undefined,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`无法获取配置文件 [${name}]: ` + resp.message)
|
console.error(`无法获取配置文件 [${name}]: ` + resp.message)
|
||||||
|
return {
|
||||||
|
msg: `无法获取配置文件 [${name}]: ` + resp.message,
|
||||||
|
data: undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`无法获取配置文件 [${name}]: ` + 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 {
|
try {
|
||||||
const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
const resp = await QueryPostAPI(VTSURU_API_URL + 'set-config', {
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export enum SongFrom {
|
|||||||
Custom,
|
Custom,
|
||||||
Netease,
|
Netease,
|
||||||
FiveSing,
|
FiveSing,
|
||||||
|
Kugou
|
||||||
}
|
}
|
||||||
export interface SongsInfo {
|
export interface SongsInfo {
|
||||||
id: number
|
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 EVENT_API_URL = { toString: () => `${BASE_API()}event/` }
|
||||||
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
|
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
|
||||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` }
|
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 VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
|
||||||
|
|
||||||
export const ScheduleTemplateMap = {
|
export const ScheduleTemplateMap = {
|
||||||
|
|||||||
@@ -190,22 +190,12 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
danmaku: true,
|
danmaku: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'song-request',
|
|
||||||
name: 'manage-songRequest',
|
|
||||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '弹幕点歌',
|
|
||||||
keepAlive: true,
|
|
||||||
danmaku: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'queue',
|
path: 'queue',
|
||||||
name: 'manage-liveQueue',
|
name: 'manage-liveQueue',
|
||||||
component: () => import('@/views/open_live/OpenQueue.vue'),
|
component: () => import('@/views/open_live/OpenQueue.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '弹幕排队',
|
title: '排队',
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
danmaku: true,
|
danmaku: true,
|
||||||
},
|
},
|
||||||
@@ -220,6 +210,26 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
danmaku: true,
|
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',
|
path: 'live',
|
||||||
name: 'manage-live',
|
name: 'manage-live',
|
||||||
@@ -270,7 +280,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: 'song-request',
|
path: 'song-request',
|
||||||
name: 'open-live-song-request',
|
name: 'open-live-song-request',
|
||||||
component: () => import('@/views/open_live/MusicRequest.vue'),
|
component: () => import('@/views/open_live/SongRequest.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '点歌',
|
title: '点歌',
|
||||||
},
|
},
|
||||||
@@ -310,7 +320,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'obs-song-request',
|
name: 'obs-song-request',
|
||||||
component: () => import('@/views/obs/SongRequestOBS.vue'),
|
component: () => import('@/views/obs/SongRequestOBS.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '弹幕点歌',
|
title: '弹幕点歌 (歌势',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -321,6 +331,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
title: '弹幕排队',
|
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>
|
</NSpace>
|
||||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||||
<NTimeline>
|
<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-17" />
|
||||||
<NTimelineItem type="success" title="功能添加" content="直播记录" time="2023-12-3" />
|
<NTimelineItem type="success" title="功能添加" content="直播记录" time="2023-12-3" />
|
||||||
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
||||||
|
|||||||
@@ -44,8 +44,13 @@ const functions = [
|
|||||||
icon: Lottery24Filled,
|
icon: Lottery24Filled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '弹幕点歌',
|
name: '弹幕点歌 (歌势)',
|
||||||
desc: '可以让弹幕进行点歌!',
|
desc: '可以让弹幕进行点歌, 然后自己唱',
|
||||||
|
icon: ListCircle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '弹幕点歌 (点播)',
|
||||||
|
desc: '可以让弹幕进行点歌, 进行搜索后直接播放',
|
||||||
icon: ListCircle,
|
icon: ListCircle,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -293,6 +293,11 @@ const menuOptions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
|
h(
|
||||||
|
NTooltip,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
trigger: () =>
|
||||||
h(
|
h(
|
||||||
RouterLink,
|
RouterLink,
|
||||||
{
|
{
|
||||||
@@ -300,12 +305,42 @@ const menuOptions = [
|
|||||||
name: 'manage-songRequest',
|
name: 'manage-songRequest',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ default: () => '点歌' },
|
{
|
||||||
|
default: () => '点歌(歌势',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
default: () => '歌势用的, 观众点歌之后需要自己唱',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
key: 'manage-songRequest',
|
key: 'manage-songRequest',
|
||||||
icon: renderIcon(MusicalNote),
|
icon: renderIcon(MusicalNote),
|
||||||
//disabled: accountInfo.value?.isEmailVerified == false,
|
//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: () =>
|
label: () =>
|
||||||
h(
|
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">
|
<script setup lang="ts">
|
||||||
import { EventModel, SongsInfo } from '@/api/api-models'
|
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
||||||
import { QueryGetAPI } from '@/api/query'
|
import { DanmakuUserInfo, EventModel, FunctionTypes, SongFrom, SongsInfo } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
|
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 { 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 APlayer from 'vue3-aplayer'
|
||||||
|
import { clearInterval, setInterval } from 'worker-timers'
|
||||||
|
import MusicRequestOBS from '../obs/MusicRequestOBS.vue'
|
||||||
|
|
||||||
type MusicRequestSettings = {
|
type MusicRequestSettings = {
|
||||||
playMusicWhenFree: boolean
|
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
|
||||||
title: string
|
title: string
|
||||||
artist: string
|
artist: string
|
||||||
src: string
|
src: string
|
||||||
pic: string
|
pic: string
|
||||||
lrc: string
|
lrc: string
|
||||||
}
|
}
|
||||||
|
type WaitMusicInfo = {
|
||||||
|
from: DanmakuUserInfo
|
||||||
|
music: SongsInfo
|
||||||
|
}
|
||||||
|
|
||||||
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
|
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
|
||||||
playMusicWhenFree: true,
|
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<{
|
const props = defineProps<{
|
||||||
client: DanmakuClient
|
client: DanmakuClient
|
||||||
@@ -29,35 +94,559 @@ const props = defineProps<{
|
|||||||
isOpenLive?: boolean
|
isOpenLive?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const deviceList = ref<SelectOption[]>([])
|
||||||
|
|
||||||
|
const accountInfo = useAccount()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
const aplayer = ref()
|
const aplayer = ref()
|
||||||
|
|
||||||
const musics = ref<Music[]>([])
|
const listening = ref(false)
|
||||||
|
|
||||||
function onGetEvent(data: EventModel) {}
|
const originMusics = ref<SongsInfo[]>(await get())
|
||||||
function searchMusic(keyword: string) {
|
const waitingMusics = useStorage<WaitMusicInfo[]>('Setting.MusicRequest.Waiting', [])
|
||||||
QueryGetAPI<SongsInfo>(MUSIC_REQUEST_API_URL + 'search-kugou', {
|
const musics = computed(() => {
|
||||||
keyword: keyword,
|
return originMusics.value.map((s) => songToMusic(s))
|
||||||
}).then((data) => {
|
})
|
||||||
if (data.code == 200) {
|
const currentMusic = ref<Music>({
|
||||||
musics.value.push({
|
id: -1,
|
||||||
title: data.data.name,
|
title: '',
|
||||||
artist: data.data.author?.join('/'),
|
artist: '',
|
||||||
src: data.data.url,
|
src: '',
|
||||||
pic: data.data.cover ?? '',
|
pic: '',
|
||||||
lrc: '',
|
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)
|
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(() => {
|
onUnmounted(() => {
|
||||||
props.client.offEvent('danmaku', onGetEvent)
|
props.client.offEvent('danmaku', onGetEvent)
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</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) {
|
function onGetDanmaku(danmaku: DanmakuInfo) {
|
||||||
console.log(danmaku)
|
|
||||||
if (checkMessage(danmaku.msg)) {
|
if (checkMessage(danmaku.msg)) {
|
||||||
addSong({
|
addSong({
|
||||||
msg: danmaku.msg,
|
msg: danmaku.msg,
|
||||||
@@ -357,6 +356,9 @@ function onGetSC(danmaku: SCInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkMessage(msg: string) {
|
function checkMessage(msg: string) {
|
||||||
|
if (accountInfo.value?.settings.enableFunctions.includes(FunctionTypes.SongRequest) != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return msg
|
return msg
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user