Enable room queue controls
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user