Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/server/src/db/mediaSourcesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ export function updateMediaSource(id: string, input: UpdateMediaSourceInput) {
return getMediaSource(id);
}

export function getMediaSource(id: string) {
function getMediaSource(id: string) {
return getMediaSourceWhere(eq(mediaSources.id, id));
}

function getMediaSourceForAccount(id: string, providerAccountId: string) {
export function getMediaSourceForAccount(id: string, providerAccountId: string) {
const where = and(
eq(mediaSources.id, id),
eq(mediaSources.providerAccountId, providerAccountId)
Expand Down
58 changes: 53 additions & 5 deletions apps/server/src/providers/plex/playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,53 @@ async function fetchCurrentlyPlayingData(source: MediaSource) {
);
}

function playbackFallbackIdentity(item: any) {
const userId = idValue(item?.User?.id);
const playerId = stringValue(item?.Player?.machineIdentifier) ?? stringValue(item?.Player?.title);
const itemId = idValue(item?.ratingKey) ?? stringValue(item?.key) ?? metadataPath(item);
const parts = [userId, playerId, itemId].filter((value): value is string => Boolean(value));

return parts.length > 0 ? parts.join(":") : undefined;
}

function playbackDeduplicationKey(item: any) {
const sessionKey = idValue(item?.sessionKey);
if (sessionKey) {
return `session:${sessionKey}`;
}

const sessionId = idValue(item?.Session?.id);
if (sessionId) {
return `session-id:${sessionId}`;
}

const fallbackIdentity = playbackFallbackIdentity(item);
if (fallbackIdentity) {
return `fallback:${fallbackIdentity}`;
}

return undefined;
}

function dedupeCurrentlyPlayingMetadata(metadata: any[]) {
// Plex can emit duplicate rows for one live session, especially from web clients.
const seen = new Set<string>();

return metadata.filter((item) => {
const dedupeKey = playbackDeduplicationKey(item);
if (!dedupeKey) {
return true;
}

if (seen.has(dedupeKey)) {
return false;
}

seen.add(dedupeKey);
return true;
});
}

function tagValues(value: unknown) {
return uniqueStrings(
asArray(value as any).map((entry: any) => {
Expand Down Expand Up @@ -451,10 +498,9 @@ async function resolveMediaPath(

function playbackSessionIdentity(item: any) {
return String(
item?.Session?.id
?? item?.ratingKey
?? item?.key
?? item?.Player?.machineIdentifier
item?.sessionKey
?? item?.Session?.id
?? playbackFallbackIdentity(item)
?? randomUUID()
);
}
Expand All @@ -481,7 +527,9 @@ async function normalizeCurrentPlayback(
return [];
}

return Promise.all(metadata.map(async (item: any) => {
const uniqueMetadata = dedupeCurrentlyPlayingMetadata(metadata);

return Promise.all(uniqueMetadata.map(async (item: any) => {
const mediaSelection = deriveMediaSelection(item);
const enrichedItem = await enrichMetadataItem(context, item);
const mediaPath = await resolveMediaPath(context, item, enrichedItem, mediaSelection);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/routes/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ mediaRouter.get(
const sourceErrors: SourcePlaybackError[] = [];
const sources = listMediaSources({
enabledOnly: true,
providerAccountId: session.providerAccountId,
})
.flatMap((source) => {
const provider = getProvider(source.providerId);
Expand Down
38 changes: 19 additions & 19 deletions apps/server/src/routes/sources.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Router } from "express";
import {
listMediaSources,
getMediaSource,
deleteMediaSourceForAccount,
updateMediaSourceForAccount,
getMediaSourceForAccount,
updateMediaSourceHealthForAccount,
type MediaSource,
type UpdateMediaSourceInput,
updateMediaSourceForAccount,
} from "../db/mediaSourcesRepository.js";
import { ApiError, asyncHandler } from "../http/errors.js";
import { getProvider } from "../providers/registry.js";
Expand All @@ -29,8 +29,8 @@ function serializeSource(source: MediaSource) {
};
}

function requireMediaSource(sourceId: string) {
const source = getMediaSource(sourceId);
function requireMediaSource(sourceId: string, providerAccountId: string) {
const source = getMediaSourceForAccount(sourceId, providerAccountId);
if (!source) {
throw new ApiError(404, "source_not_found", "Source was not found");
}
Expand Down Expand Up @@ -104,33 +104,35 @@ function parseSourceUpdate(body: unknown): UpdateMediaSourceInput {
sourcesRouter.get(
"/",
asyncHandler(async (req, res) => {
requireAccountSession(req);
const session = requireAccountSession(req);
setNoStore(res);
res.json({
sources: listMediaSources().map(serializeSource),
sources: listMediaSources({
providerAccountId: session.providerAccountId,
}).map(serializeSource),
});
})
);

sourcesRouter.get(
"/:id",
asyncHandler(async (req, res) => {
requireAccountSession(req);
const session = requireAccountSession(req);
setNoStore(res);
const source = requireMediaSource(req.params.id as string);
const source = requireMediaSource(req.params.id as string, session.providerAccountId);
res.json({ source: serializeSource(source) });
})
);

sourcesRouter.patch(
"/:id",
asyncHandler(async (req, res) => {
requireAccountSession(req);
const session = requireAccountSession(req);
setNoStore(res);
const existingSource = requireMediaSource(req.params.id as string);
requireMediaSource(req.params.id as string, session.providerAccountId);
const source = updateMediaSourceForAccount(
req.params.id as string,
existingSource.providerAccountId,
session.providerAccountId,
parseSourceUpdate(req.body)
Comment on lines +132 to 136
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PATCH handler, requireMediaSource(...) performs an extra DB read but the result is unused. Since updateMediaSourceForAccount(...) is already account-scoped and you already handle the !source case, you can drop the pre-check and rely on the update call to return undefined for non-existent/unauthorized sources (avoids an extra query).

Copilot uses AI. Check for mistakes.
);
if (!source) {
Expand All @@ -143,11 +145,9 @@ sourcesRouter.patch(
sourcesRouter.delete(
"/:id",
asyncHandler(async (req, res) => {
requireAccountSession(req);
const session = requireAccountSession(req);
setNoStore(res);
const source = requireMediaSource(req.params.id as string);

const deleted = deleteMediaSourceForAccount(source.id, source.providerAccountId);
const deleted = deleteMediaSourceForAccount(req.params.id as string, session.providerAccountId);
if (!deleted) {
throw new ApiError(404, "source_not_found", "Source was not found");
}
Expand All @@ -159,10 +159,10 @@ sourcesRouter.delete(
sourcesRouter.post(
"/:id/check",
asyncHandler(async (req, res) => {
requireAccountSession(req);
const session = requireAccountSession(req);
setNoStore(res);

const source = requireMediaSource(req.params.id as string);
const source = requireMediaSource(req.params.id as string, session.providerAccountId);
const provider = getProvider(source.providerId);
if (!provider) {
throw new ApiError(500, "provider_not_registered", "Source provider is not registered");
Expand All @@ -172,7 +172,7 @@ sourcesRouter.post(
const result = await provider.checkSource(source);

if (!result.ok) {
const updatedSource = updateMediaSourceHealthForAccount(source.id, source.providerAccountId, {
const updatedSource = updateMediaSourceHealthForAccount(source.id, session.providerAccountId, {
lastCheckedAt: checkedAt,
lastError: result.message,
});
Expand All @@ -191,7 +191,7 @@ sourcesRouter.post(
return;
}

const updatedSource = updateMediaSourceForAccount(source.id, source.providerAccountId, {
const updatedSource = updateMediaSourceForAccount(source.id, session.providerAccountId, {
...(result.name !== undefined ? { name: result.name } : {}),
...(result.baseUrl !== undefined ? { baseUrl: result.baseUrl } : {}),
...(result.connection !== undefined ? { connection: result.connection } : {}),
Expand Down
Loading