|
1 | | -import { json, error } from '@sveltejs/kit'; |
| 1 | +import { json } from '@sveltejs/kit'; |
| 2 | +import scrapeIt, { type ScrapeResult } from 'scrape-it'; |
2 | 3 | import { BANDCAMP_USERNAME, USE_REDIS_CACHE } from '$env/static/private'; |
3 | 4 | import { redis } from '$lib/server/redis'; |
4 | 5 | import type { Album, BandCampResults } from '$lib/types/album'; |
5 | | -import scrapeIt, { type ScrapeResult } from 'scrape-it'; |
6 | 6 |
|
7 | | -export async function GET({ setHeaders, url }) { |
| 7 | +async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3, baseDelay = 500): Promise<T> { |
| 8 | + let lastError: Error | undefined; |
| 9 | + for (let attempt = 0; attempt <= maxRetries; attempt++) { |
| 10 | + try { |
| 11 | + return await fn(); |
| 12 | + } catch (err) { |
| 13 | + lastError = err as Error; |
| 14 | + if (attempt === maxRetries) break; |
| 15 | + const delay = baseDelay * 2 ** attempt; // 500ms, 1s, 2s |
| 16 | + await new Promise((r) => setTimeout(r, delay)); |
| 17 | + } |
| 18 | + } |
| 19 | + throw lastError; |
| 20 | +} |
| 21 | + |
| 22 | +export async function GET({ setHeaders }) { |
8 | 23 | try { |
9 | 24 | if (USE_REDIS_CACHE === 'true') { |
10 | 25 | const cached: string | null = await redis.get('bandcampAlbums'); |
11 | 26 |
|
12 | 27 | if (cached) { |
13 | | - const response: Album[] = JSON.parse(cached); |
14 | | - const ttl = await redis.ttl("bandcampAlbums"); |
| 28 | + const response: Album[] = JSON.parse(cached); |
| 29 | + const ttl = await redis.ttl('bandcampAlbums'); |
15 | 30 | if (ttl) { |
16 | 31 | setHeaders({ |
17 | | - "cache-control": `max-age=${ttl}`, |
| 32 | + 'cache-control': `max-age=${ttl}`, |
18 | 33 | }); |
19 | 34 | } else { |
20 | 35 | setHeaders({ |
21 | | - "cache-control": "max-age=43200", |
| 36 | + 'cache-control': 'max-age=43200', |
22 | 37 | }); |
23 | 38 | } |
24 | 39 | return json(response); |
25 | | - } |
| 40 | + } |
26 | 41 | } |
27 | 42 |
|
28 | | - const { data }: ScrapeResult<BandCampResults> = await scrapeIt(`https://bandcamp.com/${BANDCAMP_USERNAME}`, { |
29 | | - collectionItems: { |
30 | | - listItem: '.collection-item-container', |
31 | | - data: { |
32 | | - url: { |
33 | | - selector: '.collection-title-details > a.item-link', |
34 | | - attr: 'href', |
35 | | - }, |
36 | | - artwork: { |
37 | | - selector: 'div.collection-item-art-container a img', |
38 | | - attr: 'src', |
39 | | - }, |
40 | | - title: { |
41 | | - selector: 'span.item-link-alt > div.collection-item-title', |
42 | | - }, |
43 | | - artist: { |
44 | | - selector: 'span.item-link-alt > div.collection-item-artist', |
| 43 | + // Scrape Bandcamp with realistic headers, plus retry/backoff |
| 44 | + const { data }: ScrapeResult<BandCampResults> = await retryWithBackoff(async () => |
| 45 | + await scrapeIt(`https://bandcamp.com/${BANDCAMP_USERNAME}`, { |
| 46 | + collectionItems: { |
| 47 | + listItem: '.collection-item-container', |
| 48 | + data: { |
| 49 | + url: { selector: '.collection-title-details > a.item-link', attr: 'href' }, |
| 50 | + artwork: { selector: 'div.collection-item-art-container a img', attr: 'src' }, |
| 51 | + title: { selector: 'span.item-link-alt > div.collection-item-title' }, |
| 52 | + artist: { selector: 'span.item-link-alt > div.collection-item-artist' }, |
45 | 53 | }, |
46 | 54 | }, |
47 | | - }, |
48 | | - }); |
| 55 | + }) |
| 56 | + ); |
49 | 57 |
|
50 | 58 | const albums: Album[] = data?.collectionItems || []; |
51 | | - |
52 | | - if (albums && albums?.length > 0) { |
| 59 | + if (albums && albums.length > 0) { |
53 | 60 | if (USE_REDIS_CACHE === 'true') { |
54 | 61 | redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200); |
55 | 62 | } |
56 | | - setHeaders({ |
57 | | - "cache-control": "max-age=43200", |
58 | | - }); |
| 63 | + setHeaders({ 'cache-control': 'max-age=43200' }); |
59 | 64 | return json(albums); |
60 | 65 | } |
61 | | - return json([]); |
| 66 | + return json([]); |
62 | 67 | } catch (error) { |
63 | 68 | console.error(error); |
64 | 69 | return json([]); |
|
0 commit comments