fix: review remediations -- scan overlap guard, Subsonic star best-effort, podcast upsert

- Library auto-sync cron skips enqueuing when a scan is already active/waiting,
  so it can't stack a redundant full rescan behind a manual or webhook scan.
- Subsonic star.view is now best-effort: it attempts every id, skips missing
  tracks (P2003), logs genuine failures, and never early-returns mid-loop
  (which left some tracks starred while reporting failure). It reports an error
  only when a real failure occurred and nothing got starred.
- refreshPodcastFeed upserts episodes on (podcastId, guid) instead of
  find-then-create, closing a TOCTOU race between the manual refresh route and
  the auto-refresh job that could throw on the unique constraint.
- Onboarding: rename the shadowing 'user' var in the recovery path for clarity.
This commit is contained in:
chevron7
2026-06-15 14:04:42 -05:00
parent 8d62f30151
commit 3bf7563ffa
4 changed files with 36 additions and 8 deletions
+9 -2
View File
@@ -1764,8 +1764,14 @@ export async function refreshPodcastFeed(podcastId: string): Promise<{ newEpisod
for (const ep of result.episodes) {
if (existingGuids.has(ep.guid)) continue;
await prisma.podcastEpisode.create({
data: {
// upsert, not create: the manual refresh route and the auto-refresh job
// can run concurrently, and find-then-create is a TOCTOU race -- both
// would see "not existing" and the second create would throw on the
// (podcastId, guid) unique constraint. The update branch is a no-op so
// an episode that already exists is left untouched.
await prisma.podcastEpisode.upsert({
where: { podcastId_guid: { podcastId, guid: ep.guid } },
create: {
podcastId,
guid: ep.guid,
title: ep.title,
@@ -1779,6 +1785,7 @@ export async function refreshPodcastFeed(podcastId: string): Promise<{ newEpisod
fileSize: ep.fileSize,
mimeType: ep.mimeType,
},
update: {},
});
newEpisodesCount++;
}
+15 -3
View File
@@ -81,6 +81,15 @@ starredRouter.all("/star.view", wrap(async (req, res) => {
const userId = req.user!.id;
const ids = parseRepeatedQueryParam(req.query.id);
// Best-effort: attempt every id, skip ones whose track doesn't exist
// (P2003), and log genuine failures rather than swallowing them. Don't
// early-return mid-loop -- that would leave some tracks starred while
// telling the client it failed. Only report an error if a real failure
// occurred and nothing got starred (e.g. the DB is down); a partial
// failure is logged and reported ok so the client keeps its successes
// (the upsert is idempotent, so a retry is safe).
let anySucceeded = false;
let realFailure = false;
for (const trackId of ids) {
try {
await prisma.likedTrack.upsert({
@@ -88,13 +97,16 @@ starredRouter.all("/star.view", wrap(async (req, res) => {
create: { userId, trackId },
update: {},
});
anySucceeded = true;
} catch (err) {
// P2003 = FK violation: trackId doesn't exist. Absorb silently.
if ((err as { code?: string }).code === "P2003") continue;
logger.warn("[Subsonic] star failed:", err);
return subsonicError(req, res, SubsonicError.GENERIC, "Failed to star track");
realFailure = true;
logger.warn(`[Subsonic] star failed for track ${trackId}:`, err);
}
}
if (realFailure && !anySucceeded) {
return subsonicError(req, res, SubsonicError.GENERIC, "Failed to star track");
}
return subsonicOk(req, res);
}));
+9
View File
@@ -31,6 +31,15 @@ export function startLibrarySyncCron() {
return;
}
// Skip if a scan (manual, webhook, or a prior auto-sync) is already
// active or waiting -- no point queuing a redundant full rescan
// behind one that's about to cover the same files.
const counts = await scanQueue.getJobCounts("active", "waiting");
if ((counts.active ?? 0) + (counts.waiting ?? 0) > 0) {
logger.debug("[LibrarySync] scan already in progress, skipping");
return;
}
await scanQueue.add("scan", {
musicPath: config.music.musicPath,
source: "auto-sync",
+3 -3
View File
@@ -109,12 +109,12 @@ export default function OnboardingPage() {
// "refresh" instruction that can't recover the session, try
// logging in with the same credentials and continue.
try {
const user = await api.login(username, password);
if (user.requires2FA) {
const loggedInUser = await api.login(username, password);
if (loggedInUser.requires2FA) {
router.push("/login");
return;
}
if (user.onboardingComplete) {
if (loggedInUser.onboardingComplete) {
router.push("/");
return;
}