Skip to content
Open
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
30 changes: 30 additions & 0 deletions src/api/artistSearch/mergeCandidateSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,36 @@ describe("mergeCandidateSelection", () => {

expect(update.providerUrl).toEqual({ spotify: candidate.url });
});

it("carries soundcloud followers when a soundcloud url is selected", () => {
const candidate = makeCandidate({
url: "https://soundcloud.com/artist",
followers: 4200,
});

const update = mergeCandidateSelection(candidate, "soundcloud", ["url"]);

expect(update.soundcloudFollowers).toBe(4200);
});

it("writes nothing for soundcloud candidates with null followers", () => {
const candidate = makeCandidate({
url: "https://soundcloud.com/artist",
followers: null,
});

const update = mergeCandidateSelection(candidate, "soundcloud", ["url"]);

expect(update).not.toHaveProperty("soundcloudFollowers");
});

it("does not carry followers for spotify candidates", () => {
const candidate = makeCandidate({ followers: 1000 });

const update = mergeCandidateSelection(candidate, "spotify", ["url"]);

expect(update).not.toHaveProperty("soundcloudFollowers");
});
});

function makeCandidate(overrides: Partial<Candidate> = {}): Candidate {
Expand Down
7 changes: 7 additions & 0 deletions src/api/artistSearch/mergeCandidateSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface CandidateUpdate {
providerUrl?: Partial<Record<Provider, string>>;
image_url?: string | null;
description?: string | null;
soundcloudFollowers?: number;
}

export type SelectableField = "url" | "image" | "description";
Expand All @@ -23,6 +24,12 @@ export function mergeCandidateSelection(

if (fields.includes("url")) {
updates.providerUrl = { [provider]: candidate.url };

// Spotify stopped returning followers for non-extended apps, so only
// SoundCloud has a real value to persist.
if (provider === "soundcloud" && candidate.followers !== null) {
updates.soundcloudFollowers = candidate.followers;
}
}

if (fields.includes("image") && candidate.imageUrl && !staged.image_url) {
Expand Down
20 changes: 19 additions & 1 deletion src/api/artists/useUpdateArtist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type UpdateArtistUpdates = Partial<
| "time_end"
| "archived"
>
> & { genre_ids?: string[] };
> & { genre_ids?: string[]; soundcloud_followers?: number };

// Mutation function
async function updateArtist(variables: {
Expand Down Expand Up @@ -76,6 +76,24 @@ async function updateArtist(variables: {
}
}

if (updates.soundcloud_followers !== undefined) {
// Only the count is seeded, so last_sync stays null to keep the row
// eligible for the next sync-artist-data run, which may overwrite it.
const { error } = await supabase.from("soundcloud").upsert(
{
artist_id: id,
followers_count: updates.soundcloud_followers,
last_sync: null,
},
{ onConflict: "artist_id" },
);

if (error) {
console.error("Error saving soundcloud followers:", error);
throw new Error("Failed to save SoundCloud followers");
}
}

if (genre_ids !== undefined) {
const { data: currentGenres, error: currentGenresError } = await supabase
.from("artist_music_genres")
Expand Down
12 changes: 12 additions & 0 deletions src/pages/admin/festivals/LinkWizard/LinkWizardStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const linkStepSchema = z.object({
}),
image_url: z.string().nullable().optional(),
description: z.string().nullable().optional(),
soundcloud_followers: z.number().optional(),
});

export type LinkStepData = z.infer<typeof linkStepSchema>;
Expand Down Expand Up @@ -214,6 +215,13 @@ export function LinkWizardStep({
shouldDirty: true,
});
}
// Reassigned (even to undefined) on every SoundCloud URL pick so a
// previous candidate's count never pairs with a new URL.
if (update.providerUrl?.soundcloud !== undefined) {
form.setValue("soundcloud_followers", update.soundcloudFollowers, {
shouldDirty: true,
});
}
}

// The picked types must persist before the step advances — the artist save
Expand Down Expand Up @@ -247,6 +255,10 @@ export function LinkWizardStep({
updates.description = data.description;
}

if (data.soundcloud_followers !== undefined) {
updates.soundcloud_followers = data.soundcloud_followers;
}
Comment on lines +258 to +260

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Not adding this here: the existing Link Wizard integration suite stubs Spotify-only search results and I have no local Supabase to verify a new SoundCloud/RLS case. Unit tests cover the selection logic; the integration + non-admin RLS case is left for a follow-up.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

you should have access to staging supabase

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The session only has the staging anon key (VITE_SUPABASE_URL + publishable key). The integration fixtures (signInAsTestUser, grantAdminRole) need the service-role admin API, and the harness is built for a local stack, so I can't run the test, and I'd rather not push one unverified. If you can expose a staging service-role key to the routine environment (or run pnpm run test:integration locally), I'll add the SoundCloud follower + non-admin RLS cases. Otherwise it's a follow-up.


Generated by Claude Code


updateArtistMutation.mutate(
{ id: artist.id, updates },
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Link Wizard persists the follower count of a selected SoundCloud candidate
-- from the admin client; previously only the sync function (service role)
-- could write this table.
CREATE POLICY "Admins can insert soundcloud data" ON public.soundcloud
FOR INSERT TO authenticated
WITH CHECK (public.can_edit_artists(auth.uid()));

CREATE POLICY "Admins can update soundcloud data" ON public.soundcloud
FOR UPDATE TO authenticated
USING (public.can_edit_artists(auth.uid()))
WITH CHECK (public.can_edit_artists(auth.uid()));
Loading