mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: show viewers
This commit is contained in:
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -37,11 +37,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Emit a webhook to the server
|
||||
env:
|
||||
|
||||
@@ -12,7 +12,8 @@ const nextConfig = {
|
||||
},
|
||||
env: {
|
||||
LIVE_SERVER_URL: process.env.NODE_ENV === 'production' ? 'https://backend.hctv.srizan.dev' : 'http://localhost:8888',
|
||||
}
|
||||
},
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { lucia } from '@/lib/auth';
|
||||
import prisma from '@/lib/db';
|
||||
import { resolveUserPersonalChannel } from '@/lib/db/resolve';
|
||||
import type { WebSocket } from 'ws';
|
||||
|
||||
@@ -54,6 +55,40 @@ export async function SOCKET(
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.streamInfo.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
data: {
|
||||
viewers: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
client.on('close', async () => {
|
||||
console.log('client disconnected');
|
||||
const { viewers } = (await prisma.streamInfo.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
viewers: true,
|
||||
},
|
||||
}))!;
|
||||
await prisma.streamInfo.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
data: {
|
||||
viewers: {
|
||||
decrement: viewers > 0 ? 1 : 0,
|
||||
set: viewers === 0 ? 0 : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseCookieString(cookie: string) {
|
||||
@@ -66,4 +101,4 @@ function parseCookieString(cookie: string) {
|
||||
|
||||
interface ExtendedWebSocket extends WebSocket {
|
||||
targetUsername: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function GET(request: Request): Promise<Response> {
|
||||
const state = url.searchParams.get("state");
|
||||
const storedState = cookies.get("slack_oauth_state")?.value ?? null;
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
console.log('invalid state stuff');
|
||||
return new Response(null, {
|
||||
status: 400
|
||||
});
|
||||
|
||||
@@ -22,11 +22,6 @@ export default function Sidebar({ ...props }: React.ComponentProps<typeof UISide
|
||||
const { stream, isLoading } = useStreams();
|
||||
const [followedExpanded, setFollowedExpanded] = React.useState(true);
|
||||
|
||||
// console log stream every time it changes
|
||||
React.useEffect(() => {
|
||||
console.log('stream info', stream);
|
||||
}, [stream]);
|
||||
|
||||
if (isLoading) return <SidebarSkeleton />;
|
||||
|
||||
const liveStreamers = stream?.filter((s) => s.isLive) || [];
|
||||
@@ -97,7 +92,7 @@ function StreamerItem({ streamer }: { streamer: StreamInfoResponse[0] }) {
|
||||
<div className="flex-1">
|
||||
<p className="font-medium truncate">{streamer.username}</p>
|
||||
<p className="text-sm truncate">{streamer.category}</p>
|
||||
{false && streamer.isLive && (
|
||||
{streamer.isLive && (
|
||||
<p className="text-sm">
|
||||
{streamer.viewers} viewer{streamer.viewers === 1 ? '' : 's'}
|
||||
</p>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
||||
import type { StreamInfo, User } from '@prisma/client';
|
||||
import FollowButton from './follow';
|
||||
import FollowCountText from './followCount';
|
||||
import ViewerCount from './viewerCount';
|
||||
|
||||
export default function UserInfoCard(props: Props) {
|
||||
return (
|
||||
@@ -17,23 +18,12 @@ export default function UserInfoCard(props: Props) {
|
||||
<FollowCountText channel={props.streamInfo.username} />
|
||||
</div>
|
||||
</div>
|
||||
<FollowButton channel={props.streamInfo.username} />
|
||||
<div className="flex items-center space-x-4">
|
||||
<ViewerCount />
|
||||
<FollowButton channel={props.streamInfo.username} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4">markdown description here</p>
|
||||
{/* <div className="flex items-center space-x-4 text-gray-400">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-5 w-5 mr-2" />
|
||||
<span>1.2K viewers</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-gray-400 hover:text-white">
|
||||
<Heart className="h-5 w-5 mr-2" />
|
||||
Like
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-gray-400 hover:text-white">
|
||||
<Share2 className="h-5 w-5 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
17
src/components/app/UserInfoCard/viewerCount.tsx
Normal file
17
src/components/app/UserInfoCard/viewerCount.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useStreams } from "@/lib/providers/StreamInfoProvider";
|
||||
import { User } from "lucide-react";
|
||||
|
||||
export default function ViewerCount() {
|
||||
const streamInfo = useStreams();
|
||||
|
||||
if (streamInfo.isLoading) return null;
|
||||
|
||||
const viewerCount = streamInfo.stream!.reduce((acc, stream) => acc + stream.viewers, 0);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 *:text-destructive">
|
||||
<span className="text-sm font-semibold"><User /></span>
|
||||
<span className="text-sm">{viewerCount}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,6 @@ export async function syncStream() {
|
||||
const data = await response.json();
|
||||
const httpFlv = data['http-flv'] as HttpFlv;
|
||||
|
||||
// Handle case where the RTMP server is not available or doesn't have the expected data structure
|
||||
if (!httpFlv?.servers?.[0]?.applications) {
|
||||
return;
|
||||
}
|
||||
@@ -68,12 +67,10 @@ export async function syncStream() {
|
||||
const channelLiveApp = httpFlv.servers[0].applications.find(app => app.name === 'channel-live');
|
||||
const activeStreams = channelLiveApp?.live?.streams || [];
|
||||
|
||||
// Get all streams that are currently marked as live in the database
|
||||
const currentLiveStreams = await prisma.streamInfo.findMany({
|
||||
where: { isLive: true },
|
||||
});
|
||||
|
||||
// Create a map of active streams from the RTMP server
|
||||
|
||||
const activeStreamMap = new Map();
|
||||
for (const stream of activeStreams) {
|
||||
activeStreamMap.set(stream.name, {
|
||||
@@ -82,12 +79,10 @@ export async function syncStream() {
|
||||
});
|
||||
}
|
||||
|
||||
// Update all streams
|
||||
for (const dbStream of currentLiveStreams) {
|
||||
const streamStats = activeStreamMap.get(dbStream.username);
|
||||
|
||||
if (!streamStats || !streamStats.isLive) {
|
||||
// Stream is no longer active, mark it as offline
|
||||
await prisma.streamInfo.update({
|
||||
where: { username: dbStream.username },
|
||||
data: {
|
||||
@@ -96,18 +91,9 @@ export async function syncStream() {
|
||||
startedAt: new Date(0),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Stream is still active, update viewers
|
||||
await prisma.streamInfo.update({
|
||||
where: { username: dbStream.username },
|
||||
data: {
|
||||
viewers: streamStats.viewers,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process new streams that aren't in the database yet
|
||||
for (const stream of activeStreams) {
|
||||
if (stream.active) {
|
||||
const existingStream = await prisma.streamInfo.findUnique({
|
||||
@@ -115,13 +101,11 @@ export async function syncStream() {
|
||||
});
|
||||
|
||||
if (existingStream && !existingStream.isLive) {
|
||||
// Stream just went live
|
||||
await prisma.streamInfo.update({
|
||||
where: { username: stream.name },
|
||||
data: {
|
||||
isLive: true,
|
||||
startedAt: new Date(),
|
||||
viewers: stream.clients.filter(c => !c.publishing).length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user