feat: Enhance card metadata handling, implement persistent Scryfall caching, and update pack generation logic for new booster structure.
Some checks failed
Build and Deploy / build (push) Failing after 55s
Some checks failed
Build and Deploy / build (push) Failing after 55s
This commit is contained in:
@@ -28,3 +28,4 @@
|
|||||||
- [Persist Metadata](./devlog/2025-12-16-230000_persist_metadata.md): Completed. Implemented IndexedDB persistence for Scryfall metadata to ensure offline availability and reduce API calls.
|
- [Persist Metadata](./devlog/2025-12-16-230000_persist_metadata.md): Completed. Implemented IndexedDB persistence for Scryfall metadata to ensure offline availability and reduce API calls.
|
||||||
- [Bulk Parse Feedback](./devlog/2025-12-16-231500_bulk_parse_feedback.md): Completed. Updated `CubeManager` to handle metadata generation properly and provide feedback on missing cards.
|
- [Bulk Parse Feedback](./devlog/2025-12-16-231500_bulk_parse_feedback.md): Completed. Updated `CubeManager` to handle metadata generation properly and provide feedback on missing cards.
|
||||||
- [Full Metadata Passthrough](./devlog/2025-12-16-234500_full_metadata_passthrough.md): Completed. `DraftCard` now includes a `definition` property containing the complete, raw Scryfall object for future-proofing and advanced algorithm usage.
|
- [Full Metadata Passthrough](./devlog/2025-12-16-234500_full_metadata_passthrough.md): Completed. `DraftCard` now includes a `definition` property containing the complete, raw Scryfall object for future-proofing and advanced algorithm usage.
|
||||||
|
- [Server-Side Caching](./devlog/2025-12-16-235900_server_side_caching.md): Completed. Implemented logic to cache images and metadata on the server upon bulk parsing, and updated client to use local assets.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Plan: Server-Side Caching of Bulk Data
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Implement server-side caching of both card images and metadata upon bulk parsing, ensuring the application relies on local assets rather than external Scryfall URLs.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **Refactor Server Architecture (`CardService.ts`)**
|
||||||
|
* Update storage paths to `public/cards/images` (previously `public/cards`) and `public/cards/metadata`.
|
||||||
|
* Implement `cacheMetadata` to save JSON files alongside images.
|
||||||
|
|
||||||
|
2. **Update API Endpoint (`index.ts`)**
|
||||||
|
* Modify `POST /api/cards/cache` to handle metadata saving in addition to image downloading.
|
||||||
|
* Update static file serving to map `/cards` to `public/cards`, making images accessible at `/cards/images/{id}.jpg`.
|
||||||
|
|
||||||
|
3. **Update Client Logic (`CubeManager.tsx`, `PackGeneratorService.ts`, `LobbyManager.tsx`)**
|
||||||
|
* **Generation**: Pass a flag (`useLocalImages`) to the generator service.
|
||||||
|
* **Url Construction**: Generator now produces URLs like `${origin}/cards/images/{id}.jpg` when the flag is set.
|
||||||
|
* **Triggers**: `CubeManager` immediately sends parsed data to the server for caching before generating packs.
|
||||||
|
* **Consistency**: `LobbyManager` updated to look for images in the new `/cards/images` path for multiplayer sessions.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
* **Performance**: Initial "Parse Bulk" takes slightly longer (due to server cache call), but subsequent interactions are instant and local.
|
||||||
|
* **Reliability**: Application works offline or without Scryfall after initial parse.
|
||||||
|
* **Precision**: Metadata is now persisted as individual JSONs on the backend, ready for future complex backend algorithms.
|
||||||
@@ -111,7 +111,8 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
|||||||
// --- Effects ---
|
// --- Effects ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rawScryfallData) {
|
if (rawScryfallData) {
|
||||||
const result = generatorService.processCards(rawScryfallData, filters);
|
// Use local images: true
|
||||||
|
const result = generatorService.processCards(rawScryfallData, filters, true);
|
||||||
setProcessedData(result);
|
setProcessedData(result);
|
||||||
}
|
}
|
||||||
}, [filters, rawScryfallData]);
|
}, [filters, rawScryfallData]);
|
||||||
@@ -189,6 +190,20 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRawScryfallData(expandedCards);
|
setRawScryfallData(expandedCards);
|
||||||
|
|
||||||
|
// Cache to server
|
||||||
|
if (expandedCards.length > 0) {
|
||||||
|
setProgress('Loading...');
|
||||||
|
// Deduplicate for shipping to server
|
||||||
|
const uniqueCards = Array.from(new Map(expandedCards.map(c => [c.id, c])).values());
|
||||||
|
|
||||||
|
await fetch('/api/cards/cache', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cards: uniqueCards }) // Send full metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setProgress('');
|
setProgress('');
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
|||||||
|
|
||||||
// Transform packs to use local URLs
|
// Transform packs to use local URLs
|
||||||
// Note: For multiplayer, clients need to access this URL.
|
// Note: For multiplayer, clients need to access this URL.
|
||||||
const baseUrl = `${window.location.protocol}//${window.location.host}/cards`;
|
const baseUrl = `${window.location.protocol}//${window.location.host}/cards/images`;
|
||||||
|
|
||||||
const updatedPacks = generatedPacks.map(pack => ({
|
const updatedPacks = generatedPacks.map(pack => ({
|
||||||
...pack,
|
...pack,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export interface PackGenerationSettings {
|
|||||||
|
|
||||||
export class PackGeneratorService {
|
export class PackGeneratorService {
|
||||||
|
|
||||||
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }): { pools: ProcessedPools, sets: SetsMap } {
|
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }, useLocalImages: boolean = false): { pools: ProcessedPools, sets: SetsMap } {
|
||||||
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
||||||
const setsMap: SetsMap = {};
|
const setsMap: SetsMap = {};
|
||||||
|
|
||||||
@@ -104,7 +104,9 @@ export class PackGeneratorService {
|
|||||||
typeLine: typeLine,
|
typeLine: typeLine,
|
||||||
layout: layout,
|
layout: layout,
|
||||||
colors: cardData.colors || [],
|
colors: cardData.colors || [],
|
||||||
image: cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || '',
|
image: useLocalImages
|
||||||
|
? `${window.location.origin}/cards/images/${cardData.id}.jpg`
|
||||||
|
: (cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || ''),
|
||||||
set: cardData.set_name,
|
set: cardData.set_name,
|
||||||
setCode: cardData.set,
|
setCode: cardData.set,
|
||||||
setType: setType,
|
setType: setType,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
|
|
||||||
app.use(express.json({ limit: '50mb' })); // Increase limit for large card lists
|
app.use(express.json({ limit: '50mb' })); // Increase limit for large card lists
|
||||||
|
|
||||||
// Serve static images
|
// Serve static images (Nested)
|
||||||
app.use('/cards', express.static(path.join(__dirname, 'public/cards')));
|
app.use('/cards', express.static(path.join(__dirname, 'public/cards')));
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
@@ -51,9 +51,10 @@ app.post('/api/cards/cache', async (req: Request, res: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Caching images for ${cards.length} cards...`);
|
console.log(`Caching images and metadata for ${cards.length} cards...`);
|
||||||
const count = await cardService.cacheImages(cards);
|
const imgCount = await cardService.cacheImages(cards);
|
||||||
res.json({ success: true, downloaded: count });
|
const metaCount = await cardService.cacheMetadata(cards);
|
||||||
|
res.json({ success: true, downloadedImages: imgCount, savedMetadata: metaCount });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error in cache route:', err);
|
console.error('Error in cache route:', err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
|
|||||||
@@ -8,9 +8,18 @@ const __dirname = path.dirname(__filename);
|
|||||||
const CARDS_DIR = path.join(__dirname, '../public/cards');
|
const CARDS_DIR = path.join(__dirname, '../public/cards');
|
||||||
|
|
||||||
export class CardService {
|
export class CardService {
|
||||||
|
private imagesDir: string;
|
||||||
|
private metadataDir: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!fs.existsSync(CARDS_DIR)) {
|
this.imagesDir = path.join(CARDS_DIR, 'images');
|
||||||
fs.mkdirSync(CARDS_DIR, { recursive: true });
|
this.metadataDir = path.join(CARDS_DIR, 'metadata');
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.imagesDir)) {
|
||||||
|
fs.mkdirSync(this.imagesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(this.metadataDir)) {
|
||||||
|
fs.mkdirSync(this.metadataDir, { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +47,7 @@ export class CardService {
|
|||||||
|
|
||||||
if (!imageUrl) continue;
|
if (!imageUrl) continue;
|
||||||
|
|
||||||
const filePath = path.join(CARDS_DIR, `${uuid}.jpg`);
|
const filePath = path.join(this.imagesDir, `${uuid}.jpg`);
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
// Already cached
|
// Already cached
|
||||||
@@ -67,4 +76,21 @@ export class CardService {
|
|||||||
|
|
||||||
return downloadedCount;
|
return downloadedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cacheMetadata(cards: any[]): Promise<number> {
|
||||||
|
let cachedCount = 0;
|
||||||
|
for (const card of cards) {
|
||||||
|
if (!card.id) continue;
|
||||||
|
const filePath = path.join(this.metadataDir, `${card.id}.json`);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(card, null, 2));
|
||||||
|
cachedCount++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save metadata for ${card.id}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user