feat(mcp): add MCP server with whitelist, archive, and metadata tools
This commit is contained in:
parent
86e2dc41f3
commit
eb535c3405
|
|
@ -5,7 +5,8 @@
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "bun run --watch src/index.ts"
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"mcp": "bun run src/mcp.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.1"
|
"@types/bun": "^1.3.1"
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/static": "^1.4.0",
|
"@elysiajs/static": "^1.4.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/html-minifier-next": "^2.1.0",
|
"@types/html-minifier-next": "^2.1.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import transparency from '@/router/transparency'
|
||||||
import video from '@/router/video'
|
import video from '@/router/video'
|
||||||
import websocket from '@/router/websocket'
|
import websocket from '@/router/websocket'
|
||||||
import html from '@/router/html'
|
import html from '@/router/html'
|
||||||
|
import mcp from '@/router/mcp'
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
app.use(latest)
|
app.use(latest)
|
||||||
|
|
@ -15,8 +16,9 @@ app.use(transparency)
|
||||||
app.use(video)
|
app.use(video)
|
||||||
app.use(websocket)
|
app.use(websocket)
|
||||||
app.use(html)
|
app.use(html)
|
||||||
app.onRequest(({ set, url }: any) => {
|
app.use(mcp)
|
||||||
set.headers['Onion-Location'] = 'http://tubey5btlzxkcjpxpj2c7irrbhvgu3noouobndafuhbw4i5ndvn4v7qd.onion/' + url.split('/').at(-1)
|
app.onRequest(({ set, request }: { set: { headers: Record<string, string> }, request: Request }) => {
|
||||||
|
set.headers['Onion-Location'] = 'http://tubey5btlzxkcjpxpj2c7irrbhvgu3noouobndafuhbw4i5ndvn4v7qd.onion/' + request.url.split('/').at(-1)
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('uncaughtException', err => {
|
process.on('uncaughtException', err => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { addToSizeWhitelist, archiveVideo, getVideoMetadata } from '@/utils/archive'
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'preservetube-mcp',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'add_to_size_whitelist',
|
||||||
|
description: 'Add a YouTube video ID to the size whitelist in preservetube-metadata config.json to bypass max video size limits.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'YouTube video ID (11 characters) or full YouTube URL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'archive_video',
|
||||||
|
description: 'Archive a YouTube video by ID or URL on PreserveTube.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'YouTube video ID (11 characters) or full YouTube URL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_video_metadata',
|
||||||
|
description: 'Fetch video metadata including title, duration/length, channel, publish date, and preserved database record if archived.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'YouTube video ID (11 characters) or full YouTube URL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
const inputArg = args && typeof args === 'object' && ('videoId' in args || 'url' in args)
|
||||||
|
? String((args as Record<string, unknown>).videoId || (args as Record<string, unknown>).url || '')
|
||||||
|
: ''
|
||||||
|
|
||||||
|
if (name === 'add_to_size_whitelist') {
|
||||||
|
const result = await addToSizeWhitelist(inputArg)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'archive_video') {
|
||||||
|
const result = await archiveVideo(inputArg)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'get_video_metadata') {
|
||||||
|
const result = await getVideoMetadata(inputArg)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown tool: ${name}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await server.connect(transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error)
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { addToSizeWhitelist, archiveVideo, getVideoMetadata } from '@/utils/archive'
|
||||||
|
|
||||||
|
function isAuthorized(headers: Record<string, string | undefined>): boolean {
|
||||||
|
const authHeader = headers['authorization'] || headers['Authorization']
|
||||||
|
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() : authHeader
|
||||||
|
const secret = process.env.MCP_SECRET || process.env.MCP_KEY
|
||||||
|
|
||||||
|
if (!secret || !token) return false
|
||||||
|
return token === secret
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMcpServer() {
|
||||||
|
const server = new Server(
|
||||||
|
{ name: 'preservetube-mcp', version: '1.0.0' },
|
||||||
|
{ capabilities: { tools: {} } }
|
||||||
|
)
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'add_to_size_whitelist',
|
||||||
|
description: 'Add a YouTube video ID to the size whitelist in preservetube-metadata config.json',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'archive_video',
|
||||||
|
description: 'Archive a YouTube video on PreserveTube by video ID or URL',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_video_metadata',
|
||||||
|
description: 'Fetch video metadata including title, duration/length, channel, publish date, and preserved database record if archived.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||||
|
const { name, arguments: args } = req.params
|
||||||
|
const inputArg = String((args as Record<string, unknown>)?.videoId || (args as Record<string, unknown>)?.url || '')
|
||||||
|
|
||||||
|
if (name === 'add_to_size_whitelist') {
|
||||||
|
const result = await addToSizeWhitelist(inputArg)
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'archive_video') {
|
||||||
|
const result = await archiveVideo(inputArg)
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'get_video_metadata') {
|
||||||
|
const result = await getVideoMetadata(inputArg)
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown tool: ${name}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMcpStreamRequest(request: Request): Promise<Response> {
|
||||||
|
const transport = new WebStandardStreamableHTTPServerTransport()
|
||||||
|
const mcpServer = createMcpServer()
|
||||||
|
await mcpServer.connect(transport)
|
||||||
|
return await transport.handleRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Elysia({ prefix: '/api/mcp' })
|
||||||
|
|
||||||
|
app.onBeforeHandle(({ headers, set }) => {
|
||||||
|
if (!isAuthorized(headers as Record<string, string | undefined>)) {
|
||||||
|
set.status = 401
|
||||||
|
return { success: false, message: 'Unauthorized: Invalid or missing Bearer authorization token.' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/tools', () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'add_to_size_whitelist',
|
||||||
|
description: 'Add a YouTube video ID to the size whitelist in preservetube-metadata config.json',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'archive_video',
|
||||||
|
description: 'Archive a YouTube video on PreserveTube by video ID or URL',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_video_metadata',
|
||||||
|
description: 'Fetch video metadata including title, duration/length, channel, publish date, and preserved database record if archived.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
videoId: { type: 'string', description: 'YouTube video ID or URL' }
|
||||||
|
},
|
||||||
|
required: ['videoId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/call', async ({ body }: { body: { name: string, arguments?: { videoId?: string, url?: string } } }) => {
|
||||||
|
const { name, arguments: args } = body
|
||||||
|
const inputArg = String(args?.videoId || args?.url || '')
|
||||||
|
|
||||||
|
if (name === 'add_to_size_whitelist') {
|
||||||
|
return await addToSizeWhitelist(inputArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'archive_video') {
|
||||||
|
return await archiveVideo(inputArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'get_video_metadata') {
|
||||||
|
return await getVideoMetadata(inputArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: `Unknown tool: ${name}` }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
arguments: t.Optional(t.Object({
|
||||||
|
videoId: t.Optional(t.String()),
|
||||||
|
url: t.Optional(t.String())
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.all('/', async ({ request }) => {
|
||||||
|
return await handleMcpStreamRequest(request)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.all('/*', async ({ request }) => {
|
||||||
|
return await handleMcpStreamRequest(request)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
|
export { isAuthorized }
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
import * as fs from 'node:fs'
|
||||||
|
import { db } from '@/utils/database'
|
||||||
|
import { validateVideo } from '@/utils/regex'
|
||||||
|
import { createDatabaseVideo } from '@/utils/common'
|
||||||
|
import { downloadVideo } from '@/utils/download'
|
||||||
|
import { uploadVideo } from '@/utils/upload'
|
||||||
|
import { getChannel, getVideo } from '@/utils/metadata'
|
||||||
|
import redis from '@/utils/redis'
|
||||||
|
|
||||||
|
function extractVideoId(input: string): string | null {
|
||||||
|
if (!input) return null
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (/^[\w\-_]{11}$/.test(trimmed)) return trimmed
|
||||||
|
const validated = validateVideo(trimmed)
|
||||||
|
if (validated && /^[\w\-_]{11}$/.test(validated)) return validated
|
||||||
|
const match = trimmed.match(/[\w\-_]{11}/)
|
||||||
|
return match ? match[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function archiveVideo(input: string) {
|
||||||
|
const videoId = extractVideoId(input)
|
||||||
|
if (!videoId) {
|
||||||
|
return { success: false, message: 'Invalid video URL or ID.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await db.selectFrom('videos')
|
||||||
|
.select(['id', 'deletion_stage', 'title'])
|
||||||
|
.where('id', '=', videoId)
|
||||||
|
.executeTakeFirst()
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.deletion_stage !== null) {
|
||||||
|
await db.updateTable('videos')
|
||||||
|
.set({ deletion_stage: null })
|
||||||
|
.where('id', '=', videoId)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
await redis.del(`watch:${videoId}:html`)
|
||||||
|
await redis.del('deletion:html')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Video '${existing.title || videoId}' (${videoId}) restored from deletion stage '${existing.deletion_stage}'.`,
|
||||||
|
videoId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Video '${existing.title || videoId}' (${videoId}) is already archived.`,
|
||||||
|
videoId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await redis.get(`blacklist:${videoId}`)) {
|
||||||
|
return { success: false, message: 'This video is blacklisted.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await redis.get(`save:${videoId}`)) {
|
||||||
|
return { success: false, message: 'Someone is currently archiving or downloading this video.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.set(`save:${videoId}`, 'downloading', 'EX', 300)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getVideo(videoId)
|
||||||
|
if (data.error) {
|
||||||
|
return { success: false, message: `Unable to retrieve video info from YouTube: ${data.error}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelData = await getChannel(data.videoDetails.channelId)
|
||||||
|
if (channelData.error) {
|
||||||
|
return { success: false, message: `Unable to retrieve channel info from YouTube: ${channelData.error}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsMock = {
|
||||||
|
send: (msg: string) => console.log(`[Archive ${videoId}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadResult = await downloadVideo(wsMock, videoId)
|
||||||
|
if (downloadResult.fail) {
|
||||||
|
return { success: false, message: `Download failed: ${downloadResult.message}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath = fs.readdirSync('./videos/').find(f => f.includes(`${videoId}.`))
|
||||||
|
if (!filePath) {
|
||||||
|
return { success: false, message: `Downloaded video file for ${videoId} not found.` }
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = './videos/' + filePath
|
||||||
|
const videoUrl = await uploadVideo(filePath)
|
||||||
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
|
||||||
|
|
||||||
|
const uploaded = await createDatabaseVideo(videoId, videoUrl, { data, channelData })
|
||||||
|
if (uploaded !== 'success') {
|
||||||
|
return { success: false, message: `Failed to create database record: ${JSON.stringify(uploaded)}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully archived video '${data.videoDetails.title}' (${videoId}).`,
|
||||||
|
videoId,
|
||||||
|
title: data.videoDetails.title,
|
||||||
|
watchUrl: `https://preservetube.com/watch?v=${videoId}`
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as Error
|
||||||
|
return { success: false, message: `Archiving failed: ${err.message}` }
|
||||||
|
} finally {
|
||||||
|
await redis.del(`save:${videoId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToSizeWhitelist(input: string) {
|
||||||
|
const videoId = extractVideoId(input)
|
||||||
|
if (!videoId) {
|
||||||
|
return { success: false, message: 'Invalid video URL or ID.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = [process.env.METADATA, process.env.ALTERNATIVE_METADATA].filter(Boolean) as string[]
|
||||||
|
|
||||||
|
const results = await Promise.all(servers.map(async (serverHost) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${serverHost}/whitelist`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: videoId })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return { server: serverHost, success: false, message: `Metadata service returned status ${res.status}: ${await res.text()}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json() as { success: boolean, message: string, whitelist?: string[] }
|
||||||
|
return { server: serverHost, ...data }
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const err = error as Error
|
||||||
|
return { server: serverHost, success: false, message: `Failed to connect: ${err.message}` }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const allSuccessful = results.length > 0 && results.every(r => r.success)
|
||||||
|
return {
|
||||||
|
success: allSuccessful,
|
||||||
|
message: allSuccessful
|
||||||
|
? `Successfully added video ${videoId} to size whitelist on all metadata servers (${results.map(r => r.server).join(', ')}).`
|
||||||
|
: `Whitelisting video ${videoId} completed with issues on some servers: ${JSON.stringify(results)}`,
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVideoMetadata(input: string) {
|
||||||
|
const videoId = extractVideoId(input)
|
||||||
|
if (!videoId) {
|
||||||
|
return { success: false, message: 'Invalid video URL or ID.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbVideo = await db.selectFrom('videos')
|
||||||
|
.selectAll()
|
||||||
|
.where('id', '=', videoId)
|
||||||
|
.executeTakeFirst()
|
||||||
|
|
||||||
|
let fileInfo = null
|
||||||
|
if (dbVideo && dbVideo.deletion_stage === 'cold_storage') {
|
||||||
|
fileInfo = await db.selectFrom('files')
|
||||||
|
.selectAll()
|
||||||
|
.where('videoId', '=', videoId)
|
||||||
|
.executeTakeFirst() || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await getVideo(videoId)
|
||||||
|
const ytDetails = metadata && !metadata.error ? {
|
||||||
|
title: metadata.videoDetails?.title,
|
||||||
|
lengthSeconds: metadata.videoDetails?.lengthSeconds ? parseInt(metadata.videoDetails.lengthSeconds, 10) : null,
|
||||||
|
channel: metadata.videoDetails?.author,
|
||||||
|
channelId: metadata.videoDetails?.channelId,
|
||||||
|
viewCount: metadata.videoDetails?.viewCount,
|
||||||
|
isLive: metadata.videoDetails?.isLive || false,
|
||||||
|
published: metadata.microformat?.playerMicroformatRenderer?.publishDate?.slice(0, 10) || null,
|
||||||
|
description: metadata.microformat?.playerMicroformatRenderer?.description?.simpleText || null
|
||||||
|
} : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
videoId,
|
||||||
|
isArchived: Boolean(dbVideo),
|
||||||
|
watchUrl: `https://preservetube.com/watch?v=${videoId}`,
|
||||||
|
databaseRecord: dbVideo ? {
|
||||||
|
...dbVideo,
|
||||||
|
coldStorageFile: fileInfo
|
||||||
|
} : null,
|
||||||
|
youtubeMetadata: ytDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { archiveVideo, addToSizeWhitelist, getVideoMetadata, extractVideoId }
|
||||||
|
|
@ -22,6 +22,6 @@ setInterval(async () => {
|
||||||
console.log(`deleted file ${f} because there is no active download of it`)
|
console.log(`deleted file ${f} because there is no active download of it`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 5 * 60000)
|
}, 5 * 60000).unref()
|
||||||
|
|
||||||
export default redis
|
export default redis
|
||||||
Loading…
Reference in New Issue