mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: thumbnail poc
This commit is contained in:
@@ -17,7 +17,7 @@ RUN turbo prune @hctv/web --docker
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM base AS installer
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache libc6-compat ffmpeg
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"media-chrome": "^4.8.0",
|
||||
"next": "^15.2.3",
|
||||
"next-themes": "^0.4.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"pg": "^8.14.1",
|
||||
"pg-boss": "^10.1.6",
|
||||
"react": "19",
|
||||
@@ -63,6 +64,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/ws": "^8.18.0",
|
||||
|
||||
@@ -2,11 +2,29 @@ export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await (await import('@/lib/instrumentation/streamInfo')).default();
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { registerWorkers } = await import('@/lib/workers/register');
|
||||
|
||||
await registerWorkers();
|
||||
|
||||
console.log('bullmq workers registered');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const cron = (await import('node-cron')).default;
|
||||
|
||||
const getLiveThumb = (await import('@/lib/instrumentation/getLiveThumb')).default;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log('running production cron job scheduling')
|
||||
cron.schedule('*/3 * * * *', async () => {
|
||||
await getLiveThumb();
|
||||
});
|
||||
} else {
|
||||
console.log('running local cron job scheduling')
|
||||
setInterval(async () => {
|
||||
await getLiveThumb();
|
||||
}, 5000);
|
||||
}
|
||||
console.log('cron stuff registered');
|
||||
}
|
||||
}
|
||||
|
||||
21
apps/web/src/lib/instrumentation/getLiveThumb.ts
Normal file
21
apps/web/src/lib/instrumentation/getLiveThumb.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { prisma } from "@hctv/db";
|
||||
import { getThumbnailQueue } from "../workers";
|
||||
|
||||
export default async function getLiveThumb() {
|
||||
const liveChannels = await prisma.streamInfo.findMany({
|
||||
where: {
|
||||
isLive: true,
|
||||
},
|
||||
include: {
|
||||
channel: true,
|
||||
}
|
||||
});
|
||||
const liveChannelNames = liveChannels.map((channel) => channel.channel.name);
|
||||
|
||||
const thumbQueue = getThumbnailQueue();
|
||||
for (const channel of liveChannelNames) {
|
||||
await thumbQueue.add("getLiveThumb", {
|
||||
name: channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
BIN
apps/web/src/lib/srizan2.jpg
Normal file
BIN
apps/web/src/lib/srizan2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -1,19 +1,19 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { getRedisConnection } from '@/lib/services/redis';
|
||||
|
||||
// Singleton instances for notifier
|
||||
const globalForNotifier = global as unknown as {
|
||||
notificationQueue: Queue | null;
|
||||
notificationWorker: Worker | null;
|
||||
|
||||
thumbnailQueue: Queue | null;
|
||||
thumbnailWorker: Worker | null;
|
||||
};
|
||||
|
||||
// Initialize if they don't exist
|
||||
if (!globalForNotifier.notificationQueue) {
|
||||
globalForNotifier.notificationQueue = null;
|
||||
globalForNotifier.notificationWorker = null;
|
||||
}
|
||||
|
||||
// Get or create the notification queue
|
||||
export function getNotificationQueue(): Queue {
|
||||
if (!globalForNotifier.notificationQueue) {
|
||||
globalForNotifier.notificationQueue = new Queue('notifications', {
|
||||
@@ -28,4 +28,20 @@ export function getNotificationQueue(): Queue {
|
||||
});
|
||||
}
|
||||
return globalForNotifier.notificationQueue;
|
||||
}
|
||||
|
||||
export function getThumbnailQueue(): Queue {
|
||||
if (!globalForNotifier.thumbnailQueue) {
|
||||
globalForNotifier.thumbnailQueue = new Queue('thumbnails', {
|
||||
connection: getRedisConnection(),
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 5000,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
return globalForNotifier.thumbnailQueue;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { registerNotificationWorker } from './worker/notification';
|
||||
import { registerThumbnailWorker } from './worker/thumbnails';
|
||||
|
||||
export async function registerWorkers(): Promise<void> {
|
||||
await registerNotificationWorker();
|
||||
await registerThumbnailWorker();
|
||||
console.log('All workers registered successfully');
|
||||
}
|
||||
67
apps/web/src/lib/workers/worker/thumbnails.ts
Normal file
67
apps/web/src/lib/workers/worker/thumbnails.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Worker } from 'bullmq';
|
||||
import { getRedisConnection } from '@/lib/services/redis';
|
||||
import { exec } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const globalForWorker = global as unknown as {
|
||||
thumbnailWorker: Worker | null;
|
||||
};
|
||||
|
||||
if (!globalForWorker.thumbnailWorker) {
|
||||
globalForWorker.thumbnailWorker = null;
|
||||
}
|
||||
|
||||
export async function registerThumbnailWorker(): Promise<void> {
|
||||
if (globalForWorker.thumbnailWorker) {
|
||||
console.log('Notification worker already registered');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Registering notification worker...');
|
||||
|
||||
const worker = new Worker(
|
||||
'thumbnails',
|
||||
async (job) => {
|
||||
try {
|
||||
// this is totally unnecessary, but i'll keep it for security purposes.
|
||||
const name = job.data.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const m3u8location = `/dev/shm/hls/${name}.m3u8`;
|
||||
if (!existsSync(m3u8location)) return;
|
||||
|
||||
exec(
|
||||
`/usr/bin/ffmpeg -i ${m3u8location} -vframes 1 -an -y -f image2 /home/srizan/Documents/Development/hclive/apps/web/src/lib/${name}.jpg`,
|
||||
(error) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
console.error('Slack notification failed:', e);
|
||||
// @ts-ignore e is unknown
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
connection: getRedisConnection(),
|
||||
concurrency: 3,
|
||||
limiter: {
|
||||
max: 50,
|
||||
duration: 30000,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
globalForWorker.thumbnailWorker = worker;
|
||||
}
|
||||
|
||||
// Close the worker
|
||||
export async function closeThumbnailWorker(): Promise<void> {
|
||||
if (globalForWorker.thumbnailWorker) {
|
||||
await globalForWorker.thumbnailWorker.close();
|
||||
globalForWorker.thumbnailWorker = null;
|
||||
}
|
||||
}
|
||||
17
yarn.lock
17
yarn.lock
@@ -1661,6 +1661,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/node-cron@^3.0.11":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344"
|
||||
integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==
|
||||
|
||||
"@types/node@*":
|
||||
version "22.13.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.11.tgz#f0ed6b302dcf0f4229d44ea707e77484ad46d234"
|
||||
@@ -4278,6 +4283,13 @@ node-abort-controller@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
|
||||
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
|
||||
|
||||
node-cron@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2"
|
||||
integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==
|
||||
dependencies:
|
||||
uuid "8.3.2"
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
@@ -5928,6 +5940,11 @@ util-utils@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/util-utils/-/util-utils-1.0.3.tgz#abde6c79d373eb7fae42f28933273eab6d895dd3"
|
||||
integrity sha512-KXQzb5Y1cmQOubnYn2TMSJDwX+cLrFNi3CRp0bm+yTUdR4tV4LDkk2RAzGSweucwvTdhuoDdZrY67ds7QDRYXQ==
|
||||
|
||||
uuid@8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
|
||||
Reference in New Issue
Block a user