diff --git a/controllers/ArtistSocialsController.ts b/controllers/ArtistSocialsController/getArtistSocialsHandler.ts similarity index 66% rename from controllers/ArtistSocialsController.ts rename to controllers/ArtistSocialsController/getArtistSocialsHandler.ts index bc6a98a0..eba3d40e 100644 --- a/controllers/ArtistSocialsController.ts +++ b/controllers/ArtistSocialsController/getArtistSocialsHandler.ts @@ -1,18 +1,16 @@ -import { Request, Response } from "express"; -import { getArtistSocials } from "../lib/supabase/getArtistSocials"; +import type { RequestHandler } from "express"; +import { getArtistSocials } from "../../lib/supabase/getArtistSocials"; /** - * Handler for GET /api/artist/socials + * Handler for GET /artist/socials * Retrieves all social media profiles associated with an artist account */ -export const getArtistSocialsHandler = async (req: Request, res: Response) => { +export const getArtistSocialsHandler: RequestHandler = async (req, res) => { try { - // Get query parameters const { artist_account_id, page, limit } = req.query; - // Validate required parameters if (!artist_account_id || typeof artist_account_id !== "string") { - return res.status(400).json({ + res.status(400).json({ status: "error", message: "Missing required parameter: artist_account_id", socials: [], @@ -23,21 +21,22 @@ export const getArtistSocialsHandler = async (req: Request, res: Response) => { total_pages: 0, }, }); + return; } - // Call the database function with parameters const result = await getArtistSocials({ artist_account_id, page: typeof page === "string" ? parseInt(page, 10) : undefined, limit: typeof limit === "string" ? parseInt(limit, 10) : undefined, }); - // Return the response - return res.status(result.status === "success" ? 200 : 500).json(result); + const statusCode = result.status === "success" ? 200 : 500; + res.status(statusCode).json(result); + return; } catch (error) { console.error("[ERROR] getArtistSocialsHandler error:", error); - return res.status(500).json({ + res.status(500).json({ status: "error", message: error instanceof Error ? error.message : "An unknown error occurred", @@ -49,5 +48,6 @@ export const getArtistSocialsHandler = async (req: Request, res: Response) => { total_pages: 0, }, }); + return; } }; diff --git a/controllers/ArtistSocialsController/index.ts b/controllers/ArtistSocialsController/index.ts new file mode 100644 index 00000000..a1da8b3b --- /dev/null +++ b/controllers/ArtistSocialsController/index.ts @@ -0,0 +1,2 @@ +export { getArtistSocialsHandler } from "./getArtistSocialsHandler"; +export { postArtistSocialsScrapeHandler } from "./postArtistSocialsScrapeHandler"; diff --git a/controllers/ArtistSocialsController/postArtistSocialsScrapeHandler.ts b/controllers/ArtistSocialsController/postArtistSocialsScrapeHandler.ts new file mode 100644 index 00000000..f976913c --- /dev/null +++ b/controllers/ArtistSocialsController/postArtistSocialsScrapeHandler.ts @@ -0,0 +1,52 @@ +import type { RequestHandler } from "express"; +import { getAccountSocials } from "../../lib/supabase/getAccountSocials"; +import { scrapeProfileUrlBatch } from "../../lib/apify/scrapeProfileUrlBatch"; + +export const postArtistSocialsScrapeHandler: RequestHandler = async ( + req, + res +) => { + try { + const { artist_account_id } = req.body ?? {}; + + if (!artist_account_id || typeof artist_account_id !== "string") { + res.status(400).json({ + status: "error", + message: "artist_account_id body parameter is required", + }); + return; + } + + const { status, socials } = await getAccountSocials(artist_account_id); + + if (status === "error") { + res.status(500).json({ + status: "error", + message: "Failed to fetch artist socials", + }); + return; + } + + if (!socials.length) { + res.json([]); + return; + } + + const results = await scrapeProfileUrlBatch( + socials.map((social) => ({ + profileUrl: social.profile_url, + username: social.username, + })) + ); + + res.json(results); + return; + } catch (error) { + console.error("[ERROR] postArtistSocialsScrapeHandler error:", error); + res.status(500).json({ + status: "error", + message: "Internal server error", + }); + return; + } +}; diff --git a/lib/apify/scrapeProfileUrl.ts b/lib/apify/scrapeProfileUrl.ts index 7df93fe3..b786dd6d 100644 --- a/lib/apify/scrapeProfileUrl.ts +++ b/lib/apify/scrapeProfileUrl.ts @@ -16,9 +16,12 @@ export interface ProfileScrapeResult { runId: string | null; datasetId: string | null; error: string | null; - supported: boolean; } +export type ScrapeProfileResult = ProfileScrapeResult & { + supported: boolean; +}; + const PLATFORM_SCRAPERS: Array<{ match: (url: string) => boolean; scraper: ScrapeRunner; @@ -54,7 +57,7 @@ const PLATFORM_SCRAPERS: Array<{ export const scrapeProfileUrl = async ( profileUrl: string | null | undefined, username: string -): Promise => { +): Promise => { if (!profileUrl) { return null; } diff --git a/lib/apify/scrapeProfileUrlBatch.ts b/lib/apify/scrapeProfileUrlBatch.ts new file mode 100644 index 00000000..2ccbf4a2 --- /dev/null +++ b/lib/apify/scrapeProfileUrlBatch.ts @@ -0,0 +1,28 @@ +import { + ProfileScrapeResult, + ScrapeProfileResult, + scrapeProfileUrl, +} from "./scrapeProfileUrl"; + +type ScrapeProfileUrlBatchInput = { + profileUrl: string | null | undefined; + username: string | null | undefined; +}; + +export const scrapeProfileUrlBatch = async ( + inputs: ScrapeProfileUrlBatchInput[] +): Promise => { + const results = await Promise.all( + inputs.map(({ profileUrl, username }) => + scrapeProfileUrl(profileUrl ?? null, username ?? "") + ) + ); + + return results + .filter((result): result is ScrapeProfileResult => result !== null) + .map(({ runId, datasetId, error }) => ({ + runId, + datasetId, + error, + })); +}; diff --git a/routes.ts b/routes.ts index c2417c8f..a634cf74 100644 --- a/routes.ts +++ b/routes.ts @@ -7,7 +7,10 @@ import { generateImageHandler } from "./controllers/ImageGenerationController"; import { getCommentsHandler } from "./controllers/CommentsController"; import { getArtistSegmentsHandler } from "./controllers/ArtistSegmentsController"; import { getSegmentFansHandler } from "./controllers/SegmentFansController"; -import { getArtistSocialsHandler } from "./controllers/ArtistSocialsController"; +import { + getArtistSocialsHandler, + postArtistSocialsScrapeHandler, +} from "./controllers/ArtistSocialsController"; import { getSocialPostsHandler } from "./controllers/SocialPostsController"; import { getPostCommentsHandler } from "./controllers/PostCommentsController"; import { @@ -94,7 +97,8 @@ routes.get("/comments", getCommentsHandler as any); routes.get("/artist/segments", getArtistSegmentsHandler as any); routes.get("/segment/fans", getSegmentFansHandler as any); -routes.get("/artist/socials", getArtistSocialsHandler as any); +routes.get("/artist/socials", getArtistSocialsHandler); +routes.post("/artist/socials/scrape", postArtistSocialsScrapeHandler); routes.get("/social/posts", getSocialPostsHandler as any); routes.post("/social/scrape", postSocialScrapeHandler as any); routes.get("/post/comments", getPostCommentsHandler as any);