Skip to content

Commit db5273d

Browse files
committed
feat(artist): sync data from table
1 parent 04c4df2 commit db5273d

File tree

15 files changed

+236
-112
lines changed

15 files changed

+236
-112
lines changed

src/hooks/queries/artists/useArtists.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Database } from "@/integrations/supabase/types";
44

55
export type Artist = Database["public"]["Tables"]["artists"]["Row"] & {
66
artist_music_genres: { music_genre_id: string }[] | null;
7+
soundcloud_followers?: number;
78
};
89

910
// Query key factory
@@ -34,7 +35,27 @@ async function fetchArtists(): Promise<Artist[]> {
3435
throw new Error("Failed to fetch artists");
3536
}
3637

37-
return data || [];
38+
const { data: soundcloudData, error: soundcloudError } = await supabase
39+
.from("soundcloud")
40+
.select("artist_id, followers_count");
41+
42+
if (soundcloudError) {
43+
console.error("Error fetching soundcloud data:", soundcloudError);
44+
throw new Error("Failed to fetch soundcloud data");
45+
}
46+
47+
const soundcloudMap = new Map(
48+
soundcloudData?.map((sc) => [sc.artist_id, sc.followers_count]) || [],
49+
);
50+
51+
return (
52+
data.map((artist) => {
53+
return {
54+
...artist,
55+
soundcloud_followers: soundcloudMap.get(artist.id) || 0,
56+
};
57+
}) || []
58+
);
3859
}
3960

4061
// Hook

src/integrations/supabase/types.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,8 @@ export type Database = {
159159
estimated_date: string | null;
160160
id: string;
161161
image_url: string | null;
162-
last_soundcloud_sync: string | null;
163162
name: string;
164163
slug: string;
165-
soundcloud_followers: number | null;
166164
soundcloud_url: string | null;
167165
spotify_url: string | null;
168166
stage: string | null;
@@ -178,10 +176,8 @@ export type Database = {
178176
estimated_date?: string | null;
179177
id?: string;
180178
image_url?: string | null;
181-
last_soundcloud_sync?: string | null;
182179
name: string;
183180
slug: string;
184-
soundcloud_followers?: number | null;
185181
soundcloud_url?: string | null;
186182
spotify_url?: string | null;
187183
stage?: string | null;
@@ -197,10 +193,8 @@ export type Database = {
197193
estimated_date?: string | null;
198194
id?: string;
199195
image_url?: string | null;
200-
last_soundcloud_sync?: string | null;
201196
name?: string;
202197
slug?: string;
203-
soundcloud_followers?: number | null;
204198
soundcloud_url?: string | null;
205199
spotify_url?: string | null;
206200
stage?: string | null;
@@ -637,6 +631,59 @@ export type Database = {
637631
},
638632
];
639633
};
634+
soundcloud: {
635+
Row: {
636+
artist_id: string;
637+
created_at: string | null;
638+
display_name: string | null;
639+
followers_count: number | null;
640+
id: string;
641+
last_sync: string | null;
642+
playlist_title: string | null;
643+
playlist_url: string | null;
644+
soundcloud_id: number | null;
645+
soundcloud_url: string;
646+
updated_at: string | null;
647+
username: string | null;
648+
};
649+
Insert: {
650+
artist_id: string;
651+
created_at?: string | null;
652+
display_name?: string | null;
653+
followers_count?: number | null;
654+
id?: string;
655+
last_sync?: string | null;
656+
playlist_title?: string | null;
657+
playlist_url?: string | null;
658+
soundcloud_id?: number | null;
659+
soundcloud_url: string;
660+
updated_at?: string | null;
661+
username?: string | null;
662+
};
663+
Update: {
664+
artist_id?: string;
665+
created_at?: string | null;
666+
display_name?: string | null;
667+
followers_count?: number | null;
668+
id?: string;
669+
last_sync?: string | null;
670+
playlist_title?: string | null;
671+
playlist_url?: string | null;
672+
soundcloud_id?: number | null;
673+
soundcloud_url?: string;
674+
updated_at?: string | null;
675+
username?: string | null;
676+
};
677+
Relationships: [
678+
{
679+
foreignKeyName: "soundcloud_artist_id_fkey";
680+
columns: ["artist_id"];
681+
isOneToOne: true;
682+
referencedRelation: "artists";
683+
referencedColumns: ["id"];
684+
},
685+
];
686+
};
640687
stages: {
641688
Row: {
642689
archived: boolean;

src/pages/admin/ArtistsManagement/BulkEditor/BulkActionsToolbar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Button } from "@/components/ui/button";
22
import { Badge } from "@/components/ui/badge";
33
import { CheckSquare, Square } from "lucide-react";
44
import { ArchiveButton } from "../components/ArchiveButton";
5-
import { SoundCloudSyncButton } from "../components/SoundCloudSyncButton";
65

76
interface BulkActionsToolbarProps {
87
selectedCount: number;
@@ -40,8 +39,6 @@ export function BulkActionsToolbar({
4039
</>
4140
)}
4241

43-
<SoundCloudSyncButton />
44-
4542
<Button variant="outline" size="sm" onClick={onSelectAll}>
4643
{allSelected ? (
4744
<>

src/pages/admin/ArtistsManagement/components/BulkEditorHeader.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CardHeader, CardTitle } from "@/components/ui/card";
22
import { Button } from "@/components/ui/button";
33
import { Grid3X3, Plus, Copy } from "lucide-react";
44
import { Link } from "react-router-dom";
5+
import { SoundCloudSyncButton } from "./SoundCloudSyncButton";
56

67
interface BulkEditorHeaderProps {
78
onAddArtist: () => void;
@@ -13,9 +14,11 @@ export function BulkEditorHeader({ onAddArtist }: BulkEditorHeaderProps) {
1314
<CardTitle className="flex items-center justify-between">
1415
<div className="flex items-center gap-3">
1516
<Grid3X3 className="h-5 w-5 text-blue-600" />
16-
<span>Artists Management</span>
17+
<span>Artists</span>
1718
</div>
1819
<div className="flex items-center gap-2">
20+
<SoundCloudSyncButton />
21+
1922
<Link to="/admin/artists/duplicates">
2023
<Button
2124
variant="outline"

src/pages/admin/ArtistsManagement/components/BulkEditorTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Table, TableBody } from "@/components/ui/table";
22
import type { Artist } from "@/hooks/queries/artists/useArtists";
3-
import type { SortConfig } from "../hooks/useArtistSorting";
3+
import type { SortConfig, SortingKey } from "../hooks/useArtistSorting";
44
import { BulkEditorTableHeader } from "./BulkEditorTableHeader";
55
import { BulkEditorTableRow } from "./BulkEditorTableRow";
66

@@ -9,7 +9,7 @@ interface BulkEditorTableProps {
99
selectedIds: Set<string>;
1010
sortConfig: SortConfig;
1111
searchTerm: string;
12-
onSort: (key: keyof Artist | "genres") => void;
12+
onSort: (key: SortingKey) => void;
1313
onSelectAll: () => void;
1414
onSelectArtist: (artistId: string, isSelected: boolean) => void;
1515
}

src/pages/admin/ArtistsManagement/components/BulkEditorTableHeader.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { TableHeader, TableRow, TableHead } from "@/components/ui/table";
22
import { Checkbox } from "@/components/ui/checkbox";
3-
import type { Artist } from "@/hooks/queries/artists/useArtists";
4-
import type { SortConfig } from "../hooks/useArtistSorting";
3+
import type { SortConfig, SortingKey } from "../hooks/useArtistSorting";
54

65
interface BulkEditorTableHeaderProps {
76
selectedCount: number;
87
totalCount: number;
98
onSelectAll: () => void;
109
sortConfig: SortConfig;
11-
onSort: (key: keyof Artist | "genres") => void;
10+
onSort: (key: SortingKey) => void;
1211
}
1312

1413
export function BulkEditorTableHeader({
@@ -18,7 +17,7 @@ export function BulkEditorTableHeader({
1817
sortConfig,
1918
onSort,
2019
}: BulkEditorTableHeaderProps) {
21-
function getSortIndicator(key: keyof Artist | "genres") {
20+
function getSortIndicator(key: SortingKey) {
2221
if (sortConfig?.key !== key) return null;
2322
return sortConfig.direction === "asc" ? " ↑" : " ↓";
2423
}

src/pages/admin/ArtistsManagement/components/SoundCloudSyncButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export function SoundCloudSyncButton({ className }: SoundCloudSyncButtonProps) {
2626
) : (
2727
<Music className="h-3 w-3 mr-1" />
2828
)}
29-
{syncMutation.isPending ? "Syncing..." : "Sync SoundCloud"}
29+
30+
<span className="hidden md:block">
31+
{syncMutation.isPending ? "Syncing..." : "Sync SoundCloud"}
32+
</span>
3033
</Button>
3134
);
3235
}

src/pages/admin/ArtistsManagement/hooks/useArtistSorting.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { useState, useMemo } from "react";
22
import type { Artist } from "@/hooks/queries/artists/useArtists";
33

4+
export type SortingKey = keyof Omit<Artist, "artist_music_genres"> | "genres";
5+
46
export type SortConfig = {
5-
key: keyof Artist | "genres";
7+
key: SortingKey;
68
direction: "asc" | "desc";
79
} | null;
810

911
export function useArtistSorting() {
1012
const [sortConfig, setSortConfig] = useState<SortConfig>(null);
1113

12-
function handleSort(key: keyof Artist | "genres") {
14+
function handleSort(key: SortingKey) {
1315
setSortConfig((current) => {
1416
if (current?.key === key) {
1517
return current.direction === "asc" ? { key, direction: "desc" } : null;
@@ -40,8 +42,8 @@ export function useArtistSorting() {
4042
aValue = a.artist_music_genres?.length || 0;
4143
bValue = b.artist_music_genres?.length || 0;
4244
} else {
43-
aValue = a[sortConfig.key];
44-
bValue = b[sortConfig.key];
45+
aValue = a[sortConfig.key] || null;
46+
bValue = b[sortConfig.key] || null;
4547
}
4648

4749
// Handle null/undefined values
Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,83 @@
11
import { SoundCloudTokenResponseSchema } from "./schemas.ts";
22

3+
let cachedToken: {
4+
token: string;
5+
expiresAt: number;
6+
refreshToken: string;
7+
} | null = null;
8+
39
export async function getSoundCloudAccessToken(
410
clientId: string,
511
clientSecret: string,
612
): Promise<string> {
13+
if (
14+
cachedToken &&
15+
cachedToken.expiresAt > Date.now() + 60 * 1000 // 1 minute buffer
16+
) {
17+
console.log("[getSoundCloudAccessToken] Using cached access token");
18+
return cachedToken.token;
19+
}
20+
721
console.log("[getSoundCloudAccessToken] Requesting access token...");
822

923
const tokenUrl = "https://api.soundcloud.com/oauth2/token";
10-
11-
const response = await fetch(tokenUrl, {
12-
method: "POST",
13-
headers: {
14-
"Content-Type": "application/x-www-form-urlencoded",
15-
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
16-
},
17-
body: "grant_type=client_credentials",
18-
});
19-
20-
if (!response.ok) {
21-
const errorBody = await response
22-
.text()
23-
.catch(() => "Unable to read error response");
24-
console.error("[getSoundCloudAccessToken] Failed to get access token:", {
25-
status: response.status,
26-
statusText: response.statusText,
27-
body: errorBody,
24+
try {
25+
const response = await fetch(tokenUrl, {
26+
method: "POST",
27+
headers: {
28+
"Content-Type": "application/x-www-form-urlencoded",
29+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
30+
},
31+
body: "grant_type=client_credentials",
2832
});
29-
throw new Error(
30-
`Failed to get SoundCloud access token: ${response.statusText}`,
31-
);
32-
}
3333

34-
const rawData = await response.json();
35-
36-
// Validate the token response structure
37-
try {
38-
const tokenData = SoundCloudTokenResponseSchema.parse(rawData);
3934
console.log(
40-
"[getSoundCloudAccessToken] Successfully obtained and validated access token",
35+
`[getSoundCloudAccessToken] Token response status: ${response.status} ${response.statusText}`,
4136
);
42-
return tokenData.access_token;
43-
} catch (validationError) {
44-
console.error("[getSoundCloudAccessToken] Invalid token response format:", {
45-
error: validationError,
46-
rawData: JSON.stringify(rawData).slice(0, 200) + "...",
37+
38+
if (!response.ok) {
39+
const errorBody = await response
40+
.text()
41+
.catch(() => "Unable to read error response");
42+
console.error("[getSoundCloudAccessToken] Failed to get access token:", {
43+
status: response.status,
44+
statusText: response.statusText,
45+
body: errorBody,
46+
});
47+
throw new Error(
48+
`Failed to get SoundCloud access token: ${response.statusText}`,
49+
);
50+
}
51+
52+
const rawData = await response.json();
53+
54+
// Validate the token response structure
55+
try {
56+
const tokenData = SoundCloudTokenResponseSchema.parse(rawData);
57+
console.log(
58+
"[getSoundCloudAccessToken] Successfully obtained and validated access token",
59+
);
60+
const token = tokenData.access_token;
61+
const expiresIn = tokenData.expires_in || 60; // Default to 1 minute if not provided
62+
const expiresAt = Date.now() + expiresIn * 1000;
63+
64+
// Cache the token
65+
cachedToken = { token, expiresAt, refreshToken: tokenData.refresh_token };
66+
return token;
67+
} catch (validationError) {
68+
console.error(
69+
"[getSoundCloudAccessToken] Invalid token response format:",
70+
{
71+
error: validationError,
72+
rawData: JSON.stringify(rawData).slice(0, 200) + "...",
73+
},
74+
);
75+
throw new Error("Invalid access token response from SoundCloud");
76+
}
77+
} catch (error) {
78+
console.error("[getSoundCloudAccessToken] Error obtaining access token:", {
79+
error,
4780
});
48-
throw new Error("Invalid access token response from SoundCloud");
81+
throw error;
4982
}
5083
}

supabase/functions/_shared/soundcloud-api/schemas.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export const SoundCloudPlaylistSchema = z.object({
4949

5050
export const SoundCloudTokenResponseSchema = z.object({
5151
access_token: z.string(),
52-
token_type: z.string().optional(),
53-
expires_in: z.number().optional(),
54-
scope: z.string().optional(),
52+
token_type: z.string(),
53+
expires_in: z.number(),
54+
scope: z.string(),
55+
refresh_token: z.string(),
5556
});
5657

5758
export const SoundCloudErrorResponseSchema = z.object({

0 commit comments

Comments
 (0)