Enable room queue controls
All checks were successful
Template Compliance / compliance (push) Successful in 5s
Release Dry Run / release-dry-run (push) Successful in 1m35s
Build / build (push) Successful in 12m32s

This commit is contained in:
MrSphay
2026-05-15 22:06:33 +02:00
parent 7a5cc2f64b
commit 04d75c386f
8 changed files with 406 additions and 34 deletions

View File

@@ -3,7 +3,7 @@
import { revalidatePath } from "next/cache";
import { normalizeMediaUrl } from "./media";
import { prisma } from "./prisma";
import { requireCurrentUser } from "./session";
import { requireCurrentUser, userIsAdmin } from "./session";
import { getAppSettings } from "./settings";
export async function addMediaToRoom(formData: FormData) {
@@ -27,6 +27,8 @@ export async function addMediaToRoom(formData: FormData) {
const media = normalizeMediaUrl(sourceUrl);
if (!settings.allowedProviders.includes(media.provider)) return;
const nextPosition = await prisma.mediaSource.count({ where: { roomId: room.id } });
await prisma.mediaSource.create({
data: {
roomId: room.id,
@@ -34,6 +36,8 @@ export async function addMediaToRoom(formData: FormData) {
provider: media.provider,
originalUrl: media.originalUrl,
playbackUrl: media.playbackUrl,
thumbnailUrl: media.thumbnailUrl,
queuePosition: nextPosition + 1,
title: media.originalUrl
}
});
@@ -54,3 +58,149 @@ export async function addMediaToRoom(formData: FormData) {
revalidatePath(`/rooms/${encodeURIComponent(room.slug)}`);
revalidatePath("/dashboard");
}
export async function removeMediaFromRoom(formData: FormData) {
const { room, media } = await requireMediaManager(formData);
if (!room || !media) return;
await prisma.mediaSource.delete({ where: { id: media.id } });
await normalizeQueue(room.id);
revalidateRoom(room.slug);
}
export async function moveMediaUp(formData: FormData) {
await moveMedia(formData, -1);
}
export async function moveMediaDown(formData: FormData) {
await moveMedia(formData, 1);
}
export async function setCurrentMedia(formData: FormData) {
const user = await requireCurrentUser();
const mediaId = String(formData.get("mediaId") || "");
if (!mediaId) return;
const media = await prisma.mediaSource.findUnique({
where: { id: mediaId },
include: {
room: {
select: {
id: true,
slug: true,
ownerId: true,
visibility: true,
members: { where: { userId: user.id }, select: { canManage: true } }
}
}
}
});
if (!media || !canUseRoom(user.id, media.room, user)) return;
await prisma.room.update({
where: { id: media.roomId },
data: {
currentState: {
provider: media.provider,
originalUrl: media.originalUrl,
playbackUrl: media.playbackUrl,
mediaSourceId: media.id,
updatedBy: user.username,
updatedAt: Date.now()
}
}
});
revalidateRoom(media.room.slug);
}
async function moveMedia(formData: FormData, direction: -1 | 1) {
const { room, media } = await requireMediaManager(formData);
if (!room || !media) return;
const queue = await prisma.mediaSource.findMany({
where: { roomId: room.id },
orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }, { id: "asc" }],
select: { id: true }
});
const index = queue.findIndex((item) => item.id === media.id);
const target = index + direction;
if (index < 0 || target < 0 || target >= queue.length) return;
const reordered = [...queue];
[reordered[index], reordered[target]] = [reordered[target], reordered[index]];
await prisma.$transaction(
reordered.map((item, itemIndex) =>
prisma.mediaSource.update({
where: { id: item.id },
data: { queuePosition: itemIndex + 1 }
})
)
);
revalidateRoom(room.slug);
}
async function requireMediaManager(formData: FormData) {
const user = await requireCurrentUser();
const mediaId = String(formData.get("mediaId") || "");
if (!mediaId) return { room: null, media: null };
const media = await prisma.mediaSource.findUnique({
where: { id: mediaId },
include: {
room: {
select: {
id: true,
slug: true,
ownerId: true,
visibility: true,
members: { where: { userId: user.id }, select: { canManage: true } }
}
}
}
});
if (!media || !canManageRoom(user.id, media.room, user)) return { room: null, media: null };
return { room: media.room, media };
}
function canUseRoom(
userId: string,
room: { ownerId: string | null; visibility: string; members: Array<{ canManage: boolean }> },
user?: Awaited<ReturnType<typeof requireCurrentUser>>
) {
return room.ownerId === userId || Boolean(user && userIsAdmin(user)) || room.visibility === "PUBLIC" || room.members.length > 0;
}
function canManageRoom(
userId: string,
room: { ownerId: string | null; members: Array<{ canManage: boolean }> },
user?: Awaited<ReturnType<typeof requireCurrentUser>>
) {
return room.ownerId === userId || Boolean(user && userIsAdmin(user)) || room.members.some((member) => member.canManage);
}
async function normalizeQueue(roomId: string) {
const queue = await prisma.mediaSource.findMany({
where: { roomId },
orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }, { id: "asc" }],
select: { id: true }
});
if (queue.length === 0) return;
await prisma.$transaction(
queue.map((item, index) =>
prisma.mediaSource.update({
where: { id: item.id },
data: { queuePosition: index + 1 }
})
)
);
}
function revalidateRoom(slug: string) {
revalidatePath(`/rooms/${encodeURIComponent(slug)}`);
revalidatePath("/dashboard");
revalidatePath("/rooms");
}

View File

@@ -4,6 +4,7 @@ export type NormalizedMedia = {
provider: "YOUTUBE" | "TWITCH" | "DIRECT" | "UNKNOWN";
originalUrl: string;
playbackUrl: string;
thumbnailUrl?: string;
title?: string;
};
@@ -34,6 +35,7 @@ export function normalizeMediaUrl(input: string): NormalizedMedia {
return {
provider: "YOUTUBE",
originalUrl,
thumbnailUrl: `https://img.youtube.com/vi/${id}/hqdefault.jpg`,
playbackUrl: `https://www.youtube.com/embed/${id}?enablejsapi=1&origin=${encodeURIComponent(
process.env.NEXTAUTH_URL || "http://localhost:3000"
)}`
@@ -48,6 +50,7 @@ export function normalizeMediaUrl(input: string): NormalizedMedia {
return {
provider: "TWITCH",
originalUrl,
thumbnailUrl: "/watchlink-icon-64.png",
playbackUrl: `https://player.twitch.tv/?video=${parts[1]}&parent=${parent}`
};
}
@@ -55,6 +58,7 @@ export function normalizeMediaUrl(input: string): NormalizedMedia {
return {
provider: "TWITCH",
originalUrl,
thumbnailUrl: "/watchlink-icon-64.png",
playbackUrl: `https://player.twitch.tv/?channel=${parts[0]}&parent=${parent}`
};
}
@@ -64,6 +68,7 @@ export function normalizeMediaUrl(input: string): NormalizedMedia {
return {
provider: "DIRECT",
originalUrl,
thumbnailUrl: "/watchlink-icon-64.png",
playbackUrl: originalUrl
};
}

View File

@@ -4,7 +4,7 @@ import { RoomVisibility } from "@prisma/client";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { prisma } from "./prisma";
import { requireCurrentUser } from "./session";
import { requireCurrentUser, userIsAdmin } from "./session";
import { getAppSettings } from "./settings";
function normalizeSlug(value: string) {
@@ -53,3 +53,74 @@ export async function createRoom(formData: FormData) {
revalidatePath("/dashboard");
redirect(`/rooms/${encodeURIComponent(room.slug)}`);
}
export async function updateRoomSettings(formData: FormData) {
const user = await requireCurrentUser();
const roomId = String(formData.get("roomId") || "");
const name = String(formData.get("name") || "").trim().slice(0, 80);
const visibility = String(formData.get("visibility") || "FRIENDS");
const room = await prisma.room.findUnique({
where: { id: roomId },
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
});
if (!room || !canManageRoom(user, room) || !name) return;
const allowedVisibility: RoomVisibility[] = ["PUBLIC", "FRIENDS", "EXPLICIT", "ROLE_RESTRICTED"];
const roomVisibility = allowedVisibility.includes(visibility as RoomVisibility)
? (visibility as RoomVisibility)
: room.visibility;
await prisma.room.update({
where: { id: room.id },
data: {
name,
visibility: roomVisibility
}
});
revalidateRoom(room.slug);
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Settings&saved=1`);
}
export async function addRoomMember(formData: FormData) {
const user = await requireCurrentUser();
const roomId = String(formData.get("roomId") || "");
const username = String(formData.get("username") || "").trim().toLowerCase();
const canManage = formData.get("canManage") === "on";
const room = await prisma.room.findUnique({
where: { id: roomId },
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
});
if (!room || !canManageRoom(user, room) || !username) return;
const invited = await prisma.user.findUnique({ where: { username }, select: { id: true } });
if (!invited || invited.id === room.ownerId) {
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&error=user`);
}
await prisma.roomMember.upsert({
where: { roomId_userId: { roomId: room.id, userId: invited.id } },
update: { canManage },
create: { roomId: room.id, userId: invited.id, canManage }
});
revalidateRoom(room.slug);
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&saved=1`);
}
function canManageRoom(
user: Awaited<ReturnType<typeof requireCurrentUser>>,
room: { ownerId: string | null; members: Array<{ canManage: boolean }> }
) {
return room.ownerId === user.id || userIsAdmin(user) || room.members.some((member) => member.canManage);
}
function revalidateRoom(slug: string) {
revalidatePath(`/rooms/${encodeURIComponent(slug)}`);
revalidatePath("/rooms");
revalidatePath("/dashboard");
}