Compare commits
No commits in common. "3c1a66eec175212c62a542348e4688592d592a51" and "6dece81a9316d957d060331ccaf8aa30620ccf04" have entirely different histories.
3c1a66eec1
...
6dece81a93
|
@ -132,5 +132,4 @@ dist
|
||||||
|
|
||||||
config.toml
|
config.toml
|
||||||
recordings
|
recordings
|
||||||
.idea
|
.idea
|
||||||
prisma/data.db*
|
|
|
@ -4,8 +4,7 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts"
|
||||||
"merge": "tsx src/merge.ts"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -24,12 +23,10 @@
|
||||||
"@discordjs/rest": "^1.7.1",
|
"@discordjs/rest": "^1.7.1",
|
||||||
"@discordjs/voice": "^0.16.0",
|
"@discordjs/voice": "^0.16.0",
|
||||||
"@discordjs/ws": "^0.8.3",
|
"@discordjs/ws": "^0.8.3",
|
||||||
"@prisma/client": "4.16.1",
|
|
||||||
"discord-api-types": "^0.37.46",
|
"discord-api-types": "^0.37.46",
|
||||||
"libsodium-wrappers": "^0.7.11",
|
"libsodium-wrappers": "^0.7.11",
|
||||||
"node-crc": "1.3.2",
|
"node-crc": "1.3.2",
|
||||||
"prism-media": "2.0.0-alpha.0",
|
"prism-media": "2.0.0-alpha.0",
|
||||||
"prisma": "^4.16.1",
|
|
||||||
"toml": "^3.0.0"
|
"toml": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,6 @@ dependencies:
|
||||||
'@discordjs/ws':
|
'@discordjs/ws':
|
||||||
specifier: ^0.8.3
|
specifier: ^0.8.3
|
||||||
version: 0.8.3
|
version: 0.8.3
|
||||||
'@prisma/client':
|
|
||||||
specifier: 4.16.1
|
|
||||||
version: 4.16.1(prisma@4.16.1)
|
|
||||||
discord-api-types:
|
discord-api-types:
|
||||||
specifier: ^0.37.46
|
specifier: ^0.37.46
|
||||||
version: 0.37.46
|
version: 0.37.46
|
||||||
|
@ -38,9 +35,6 @@ dependencies:
|
||||||
prism-media:
|
prism-media:
|
||||||
specifier: 2.0.0-alpha.0
|
specifier: 2.0.0-alpha.0
|
||||||
version: 2.0.0-alpha.0
|
version: 2.0.0-alpha.0
|
||||||
prisma:
|
|
||||||
specifier: ^4.16.1
|
|
||||||
version: 4.16.1
|
|
||||||
toml:
|
toml:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
@ -385,29 +379,6 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@prisma/client@4.16.1(prisma@4.16.1):
|
|
||||||
resolution: {integrity: sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==}
|
|
||||||
engines: {node: '>=14.17'}
|
|
||||||
requiresBuild: true
|
|
||||||
peerDependencies:
|
|
||||||
prisma: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
prisma:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@prisma/engines-version': 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
|
|
||||||
prisma: 4.16.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@prisma/engines-version@4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c:
|
|
||||||
resolution: {integrity: sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@prisma/engines@4.16.1:
|
|
||||||
resolution: {integrity: sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==}
|
|
||||||
requiresBuild: true
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@sapphire/async-queue@1.5.0:
|
/@sapphire/async-queue@1.5.0:
|
||||||
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||||
|
@ -832,15 +803,6 @@ packages:
|
||||||
duplex-child-process: 1.0.1
|
duplex-child-process: 1.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prisma@4.16.1:
|
|
||||||
resolution: {integrity: sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==}
|
|
||||||
engines: {node: '>=14.17'}
|
|
||||||
hasBin: true
|
|
||||||
requiresBuild: true
|
|
||||||
dependencies:
|
|
||||||
'@prisma/engines': 4.16.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/readable-stream@3.6.2:
|
/readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Session" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"channel" TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "File" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"filename" TEXT NOT NULL,
|
|
||||||
"timestamp" BIGINT NOT NULL,
|
|
||||||
"sessionId" TEXT,
|
|
||||||
CONSTRAINT "File_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
||||||
);
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Made the column `sessionId` on table `File` required. This step will fail if there are existing NULL values in that column.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- RedefineTables
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE "new_File" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"filename" TEXT NOT NULL,
|
|
||||||
"timestamp" BIGINT NOT NULL,
|
|
||||||
"sessionId" TEXT NOT NULL,
|
|
||||||
CONSTRAINT "File_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
INSERT INTO "new_File" ("filename", "id", "sessionId", "timestamp") SELECT "filename", "id", "sessionId", "timestamp" FROM "File";
|
|
||||||
DROP TABLE "File";
|
|
||||||
ALTER TABLE "new_File" RENAME TO "File";
|
|
||||||
PRAGMA foreign_key_check;
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (i.e. Git)
|
|
||||||
provider = "sqlite"
|
|
|
@ -1,27 +0,0 @@
|
||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "sqlite"
|
|
||||||
url = "file:data.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
model Session {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
channel String
|
|
||||||
files File[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model File {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
filename String
|
|
||||||
timestamp BigInt
|
|
||||||
|
|
||||||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
||||||
sessionId String
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import { Client, GatewayIntentBits } from "@discordjs/core"
|
||||||
import { config } from "./config.js"
|
import { config } from "./config.js"
|
||||||
import { VoiceStateManager } from "./utils/wrapper/VoiceState.js"
|
import { VoiceStateManager } from "./utils/wrapper/VoiceState.js"
|
||||||
import { RecordingSessionManager } from "./utils/sessions.js"
|
import { RecordingSessionManager } from "./utils/sessions.js"
|
||||||
import { PrismaClient } from "@prisma/client"
|
|
||||||
|
|
||||||
export const rest = new REST({ version: "10" }).setToken(config.token)
|
export const rest = new REST({ version: "10" }).setToken(config.token)
|
||||||
|
|
||||||
|
@ -21,5 +20,3 @@ export const client = new Client({ rest, gateway })
|
||||||
export const voiceStateManager = new VoiceStateManager()
|
export const voiceStateManager = new VoiceStateManager()
|
||||||
|
|
||||||
export const sessionManager = new RecordingSessionManager()
|
export const sessionManager = new RecordingSessionManager()
|
||||||
|
|
||||||
export const db = new PrismaClient()
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const startCommand = async ({
|
||||||
content: "먼저 음성 채널에 들어가주세요!",
|
content: "먼저 음성 채널에 들어가주세요!",
|
||||||
})
|
})
|
||||||
|
|
||||||
await sessionManager.addSession(data.guild_id, vc, data.channel_id, shardId)
|
sessionManager.addSession(data.guild_id, vc, shardId)
|
||||||
|
|
||||||
await api.interactions.reply(data.id, data.token, {
|
await api.interactions.reply(data.id, data.token, {
|
||||||
content: "냥냥",
|
content: "냥냥",
|
||||||
|
|
114
src/index.ts
114
src/index.ts
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { client, gateway, sessionManager, voiceStateManager } from "./api.js"
|
||||||
client,
|
|
||||||
db,
|
|
||||||
gateway,
|
|
||||||
sessionManager,
|
|
||||||
voiceStateManager,
|
|
||||||
} from "./api.js"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActivityType,
|
ActivityType,
|
||||||
|
@ -19,10 +13,6 @@ import { startCommand } from "./commands/start.js"
|
||||||
import { config } from "./config.js"
|
import { config } from "./config.js"
|
||||||
import { syncCommands } from "./utils/commands.js"
|
import { syncCommands } from "./utils/commands.js"
|
||||||
import { VoiceState } from "./utils/wrapper/VoiceState.js"
|
import { VoiceState } from "./utils/wrapper/VoiceState.js"
|
||||||
import { isMe } from "./utils/appid.js"
|
|
||||||
import { mergeAudio } from "./utils/merge.js"
|
|
||||||
import { readFile, rm } from "fs/promises"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => console.error(error))
|
process.on("uncaughtException", (error) => console.error(error))
|
||||||
process.on("unhandledRejection", (error) => console.error(error))
|
process.on("unhandledRejection", (error) => console.error(error))
|
||||||
|
@ -92,100 +82,20 @@ client.on(GatewayDispatchEvents.InteractionCreate, async (args) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
client.on(GatewayDispatchEvents.VoiceStateUpdate, async ({ data, api }) => {
|
client.on(GatewayDispatchEvents.VoiceStateUpdate, async (args) => {
|
||||||
if (data.guild_id) {
|
if (args.data.guild_id) {
|
||||||
const adapter = sessionManager.adapters.get(data.guild_id)
|
const adapter = sessionManager.adapters.get(args.data.guild_id)
|
||||||
if (adapter) adapter.onVoiceStateUpdate(data)
|
if (adapter) adapter.onVoiceStateUpdate(args.data)
|
||||||
const guild = voiceStateManager.guilds.get(data.guild_id)
|
const guild = voiceStateManager.guilds.get(args.data.guild_id)
|
||||||
if (guild) {
|
if (guild) {
|
||||||
const state = guild.get(data.user_id)
|
const state = guild.get(args.data.user_id)
|
||||||
if (state) {
|
if (state) {
|
||||||
state.update(data)
|
state.update(args.data)
|
||||||
} else {
|
} else {
|
||||||
guild.set(data.user_id, new VoiceState(data, data.guild_id))
|
guild.set(
|
||||||
}
|
args.data.user_id,
|
||||||
|
new VoiceState(args.data, args.data.guild_id)
|
||||||
const session = sessionManager.sessionMap.get(data.guild_id)
|
)
|
||||||
|
|
||||||
if (session) {
|
|
||||||
let counter = 0
|
|
||||||
let hasMe = false
|
|
||||||
|
|
||||||
for (const state of guild.values()) {
|
|
||||||
if (state.channelId !== session.voiceChannelId) continue
|
|
||||||
if (isMe(state.id)) {
|
|
||||||
hasMe = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter === 0 || !hasMe) {
|
|
||||||
session.vc.destroy()
|
|
||||||
sessionManager.removeSession(data.guild_id)
|
|
||||||
await api.channels.createMessage(session.textChannelId, {
|
|
||||||
content: `녹화가 종료되었어요! 오디오 파일 합치는 중...`,
|
|
||||||
})
|
|
||||||
setTimeout(async () => {
|
|
||||||
await mergeAudio(session.id, async (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
let succeeded = false
|
|
||||||
try {
|
|
||||||
await api.channels.createMessage(
|
|
||||||
session.textChannelId,
|
|
||||||
{
|
|
||||||
content:
|
|
||||||
"오디오 파일 합치기가 완료되었어요!",
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
contentType: "audio/ogg",
|
|
||||||
name: `${session.id}.ogg`,
|
|
||||||
data: await readFile(
|
|
||||||
path.resolve(
|
|
||||||
"recordings",
|
|
||||||
"final",
|
|
||||||
`${session.id}.ogg`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
succeeded = true
|
|
||||||
} catch {
|
|
||||||
await api.channels.createMessage(
|
|
||||||
session.textChannelId,
|
|
||||||
{
|
|
||||||
content: `오디오 파일 합치기가 완료되었...지만 파일 업로드에 실패했어요.\n파일 위치: ${path.join(
|
|
||||||
"recordings",
|
|
||||||
"final",
|
|
||||||
`${session.id}.ogg`
|
|
||||||
)}`,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (succeeded) {
|
|
||||||
await rm(
|
|
||||||
path.resolve("recordings", session.id),
|
|
||||||
{ recursive: true, force: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
await db.session.delete({
|
|
||||||
where: { id: session.id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await api.channels.createMessage(
|
|
||||||
session.textChannelId,
|
|
||||||
{
|
|
||||||
content: `ffmpeg 명령어 실행에 실패했어요! (exit code ${code}) ID: ${session.id}`,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/merge.ts
12
src/merge.ts
|
@ -1,12 +0,0 @@
|
||||||
import { db } from "./api.js"
|
|
||||||
import { mergeAudio } from "./utils/merge.js"
|
|
||||||
|
|
||||||
const id = process.argv[2]
|
|
||||||
|
|
||||||
const session = await db.session.findUnique({ where: { id } })
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new Error("session not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
await mergeAudio(id)
|
|
|
@ -1,8 +1,3 @@
|
||||||
import { config } from "../config.js"
|
|
||||||
|
|
||||||
export const getAppIdFromToken = (token: string) => {
|
export const getAppIdFromToken = (token: string) => {
|
||||||
return atob(token.split(".")[0])
|
return atob(token.split(".")[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMe = (userId: string) =>
|
|
||||||
userId === getAppIdFromToken(config.token)
|
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import path from "path"
|
|
||||||
import { db } from "../api.js"
|
|
||||||
import { spawn } from "child_process"
|
|
||||||
import { existsSync, mkdirSync } from "fs"
|
|
||||||
|
|
||||||
export const mergeAudio = async (
|
|
||||||
id: string,
|
|
||||||
callback?: (code: number | null) => void
|
|
||||||
) => {
|
|
||||||
const finalDir = "recordings/final"
|
|
||||||
const sourceDir = path.resolve("recordings", id)
|
|
||||||
|
|
||||||
if (!existsSync(finalDir)) {
|
|
||||||
mkdirSync(finalDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await db.file.findMany({
|
|
||||||
where: {
|
|
||||||
session: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let inputs: string[] = []
|
|
||||||
|
|
||||||
let filter: string[] = []
|
|
||||||
let trackNames: string[] = []
|
|
||||||
|
|
||||||
let counter = 0
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(sourceDir, file.filename)
|
|
||||||
const index = counter++
|
|
||||||
const trackName = `[c${index}]`
|
|
||||||
|
|
||||||
inputs.push("-i", filePath)
|
|
||||||
|
|
||||||
filter.push(
|
|
||||||
`[${index}:a] adelay=${file.timestamp}|${file.timestamp} ${trackName}`
|
|
||||||
)
|
|
||||||
|
|
||||||
trackNames.push(trackName)
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.push(
|
|
||||||
`${trackNames.join(" ")} amix=inputs=${
|
|
||||||
trackNames.length
|
|
||||||
}:normalize=0 [out]`
|
|
||||||
)
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
...inputs,
|
|
||||||
"-filter_complex",
|
|
||||||
filter.join(";"),
|
|
||||||
"-map",
|
|
||||||
"[out]",
|
|
||||||
"-async",
|
|
||||||
"1",
|
|
||||||
"-y",
|
|
||||||
path.join(finalDir, `${id}.ogg`),
|
|
||||||
]
|
|
||||||
|
|
||||||
const p = spawn("ffmpeg", args).on("exit", (code) => {
|
|
||||||
console.log("ffmpeg exited with code", code)
|
|
||||||
|
|
||||||
callback?.(code)
|
|
||||||
})
|
|
||||||
p.stderr.pipe(process.stderr)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Collection } from "@discordjs/collection"
|
import { Collection } from "@discordjs/collection"
|
||||||
import { db, gateway, voiceStateManager } from "../api.js"
|
import { gateway, voiceStateManager } from "../api.js"
|
||||||
import {
|
import {
|
||||||
AudioReceiveStream,
|
AudioReceiveStream,
|
||||||
DiscordGatewayAdapterLibraryMethods,
|
DiscordGatewayAdapterLibraryMethods,
|
||||||
|
@ -11,9 +11,9 @@ import { Snowflake } from "discord-api-types/v10"
|
||||||
import * as prism from "prism-media"
|
import * as prism from "prism-media"
|
||||||
import { pipeline } from "stream"
|
import { pipeline } from "stream"
|
||||||
import { createWriteStream, existsSync, mkdirSync } from "fs"
|
import { createWriteStream, existsSync, mkdirSync } from "fs"
|
||||||
|
import { getAppIdFromToken } from "./appid.js"
|
||||||
|
import { config } from "../config.js"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { isMe } from "./appid.js"
|
|
||||||
import { exec, spawn } from "child_process"
|
|
||||||
|
|
||||||
export class RecordingSessionManager {
|
export class RecordingSessionManager {
|
||||||
sessionMap = new Collection<Snowflake, RecordingSession>()
|
sessionMap = new Collection<Snowflake, RecordingSession>()
|
||||||
|
@ -23,16 +23,11 @@ export class RecordingSessionManager {
|
||||||
return this.sessionMap.has(guild)
|
return this.sessionMap.has(guild)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSession(
|
addSession(guild: Snowflake, channel: Snowflake, shardId: number) {
|
||||||
guild: Snowflake,
|
|
||||||
voiceChannel: Snowflake,
|
|
||||||
textChannel: Snowflake,
|
|
||||||
shardId: number
|
|
||||||
) {
|
|
||||||
const session = new RecordingSession(
|
const session = new RecordingSession(
|
||||||
joinVoiceChannel({
|
joinVoiceChannel({
|
||||||
guildId: guild,
|
guildId: guild,
|
||||||
channelId: voiceChannel,
|
channelId: channel,
|
||||||
adapterCreator: (methods) => {
|
adapterCreator: (methods) => {
|
||||||
this.adapters.set(guild, methods)
|
this.adapters.set(guild, methods)
|
||||||
return {
|
return {
|
||||||
|
@ -48,46 +43,22 @@ export class RecordingSessionManager {
|
||||||
selfDeaf: false,
|
selfDeaf: false,
|
||||||
selfMute: true,
|
selfMute: true,
|
||||||
debug: true,
|
debug: true,
|
||||||
}),
|
})
|
||||||
voiceChannel,
|
|
||||||
textChannel
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await session.init()
|
|
||||||
|
|
||||||
this.sessionMap.set(guild, session)
|
this.sessionMap.set(guild, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSession(guild: Snowflake) {
|
|
||||||
const session = this.sessionMap.get(guild)
|
|
||||||
if (session) session.shutdown()
|
|
||||||
this.sessionMap.delete(guild)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordingSession {
|
export class RecordingSession {
|
||||||
|
id: string
|
||||||
streams = new Collection<Snowflake, UserStream>()
|
streams = new Collection<Snowflake, UserStream>()
|
||||||
startedAt = Date.now()
|
startedAt = Date.now()
|
||||||
id!: string
|
|
||||||
|
|
||||||
constructor(
|
constructor(public vc: VoiceConnection) {
|
||||||
public vc: VoiceConnection,
|
this.id = Date.now().toString()
|
||||||
public voiceChannelId: Snowflake,
|
vc.receiver.speaking.on("start", (userId) => this.addReceiver(userId))
|
||||||
public textChannelId: Snowflake
|
vc.receiver.speaking.on("end", (userId) => this.removeReceiver(userId))
|
||||||
) {}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const result = await db.session.create({
|
|
||||||
data: {
|
|
||||||
id: this.id,
|
|
||||||
channel: this.voiceChannelId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.id = result.id
|
|
||||||
this.receiver.speaking.on("start", (userId) => this.addReceiver(userId))
|
|
||||||
this.receiver.speaking.on("end", (userId) =>
|
|
||||||
this.removeReceiver(userId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get receiver() {
|
get receiver() {
|
||||||
|
@ -95,9 +66,11 @@ export class RecordingSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
addReceiver(userId: string) {
|
addReceiver(userId: string) {
|
||||||
if (isMe(userId)) return
|
if (userId === getAppIdFromToken(config.token)) return
|
||||||
if (this.streams.has(userId)) return
|
if (this.streams.has(userId)) return
|
||||||
|
|
||||||
|
console.log("Starting record:", userId)
|
||||||
|
|
||||||
const receiver = this.receiver.subscribe(userId)
|
const receiver = this.receiver.subscribe(userId)
|
||||||
|
|
||||||
this.streams.set(userId, new UserStream(this, receiver, userId))
|
this.streams.set(userId, new UserStream(this, receiver, userId))
|
||||||
|
@ -107,12 +80,11 @@ export class RecordingSession {
|
||||||
const stream = this.streams.get(userId)
|
const stream = this.streams.get(userId)
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
console.log("Stopping record:", userId)
|
||||||
stream.subscription.push(null)
|
stream.subscription.push(null)
|
||||||
this.streams.delete(userId)
|
this.streams.delete(userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserStream {
|
class UserStream {
|
||||||
|
@ -133,9 +105,7 @@ class UserStream {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const dir = path.resolve("recordings", session.id)
|
const dir = path.resolve("recordings", session.id, userId)
|
||||||
const timestamp = Date.now() - session.startedAt
|
|
||||||
const file = `${userId}-${timestamp}.ogg`
|
|
||||||
|
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, {
|
mkdirSync(dir, {
|
||||||
|
@ -143,36 +113,20 @@ class UserStream {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = path.join(dir, file)
|
const filename = path.join(dir, `${Date.now() - session.startedAt}.ogg`)
|
||||||
|
|
||||||
console.log(`Recording ${filename} started`)
|
|
||||||
|
|
||||||
const outputStream = createWriteStream(filename)
|
const outputStream = createWriteStream(filename)
|
||||||
|
|
||||||
pipeline(subscription, this.oggStream, outputStream, async (err) => {
|
pipeline(subscription, this.oggStream, outputStream, (err) => {
|
||||||
outputStream.end()
|
outputStream.end()
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`Recording ${filename} ended with error: ${err}`)
|
console.log(`Recording ${userId} ended with error: ${err}`)
|
||||||
subscription.destroy(err)
|
subscription.destroy(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.file
|
console.log(`Recording ${userId} ended`)
|
||||||
.create({
|
|
||||||
data: {
|
|
||||||
session: {
|
|
||||||
connect: {
|
|
||||||
id: session.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filename: file,
|
|
||||||
timestamp,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
|
|
||||||
console.log(`Recording ${filename} ended`)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue