biblioplex 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -27
- package/src/__tests__/args.test.js +39 -0
- package/src/__tests__/csv.test.js +33 -0
- package/src/__tests__/mcp.test.js +84 -0
- package/src/__tests__/mutate.test.js +91 -0
- package/src/__tests__/render.test.js +103 -0
- package/src/args.mjs +29 -6
- package/src/cli.mjs +38 -25
- package/src/commands/add.mjs +30 -51
- package/src/commands/deck.mjs +13 -47
- package/src/commands/edit.mjs +26 -38
- package/src/commands/export.mjs +57 -41
- package/src/commands/history.mjs +24 -0
- package/src/commands/import.mjs +52 -80
- package/src/commands/index.mjs +44 -20
- package/src/commands/login.mjs +29 -38
- package/src/commands/logout.mjs +13 -10
- package/src/commands/ls.mjs +13 -25
- package/src/commands/move.mjs +15 -35
- package/src/commands/prices.mjs +21 -0
- package/src/commands/recover.mjs +53 -0
- package/src/commands/resolve.mjs +37 -0
- package/src/commands/rm.mjs +15 -32
- package/src/commands/search.mjs +25 -35
- package/src/commands/summary.mjs +25 -28
- package/src/commands/undo.mjs +13 -28
- package/src/commands/value.mjs +30 -0
- package/src/commands/whoami.mjs +27 -19
- package/src/constants.mjs +34 -7
- package/src/csv.mjs +71 -0
- package/src/errors.mjs +13 -2
- package/src/mcp.mjs +227 -0
- package/src/mutate.mjs +142 -67
- package/src/oauth.mjs +200 -62
- package/src/output.mjs +27 -18
- package/src/render.mjs +135 -30
- package/src/store.mjs +39 -23
- package/README.md +0 -92
- package/src/api.mjs +0 -110
- package/src/commands/container.mjs +0 -83
- package/src/commands/deckExport.mjs +0 -48
- package/src/commands/show.mjs +0 -33
- package/src/commands/tag.mjs +0 -40
- package/src/commands/writeHelpers.mjs +0 -82
- package/src/scryfall.mjs +0 -81
- package/src/snapshot.mjs +0 -77
- package/vendor/README.md +0 -20
- package/vendor/adapters.js +0 -443
- package/vendor/collection.js +0 -665
- package/vendor/deckExport.js +0 -219
- package/vendor/importMerge.js +0 -22
- package/vendor/importParsing.js +0 -119
- package/vendor/portableArchive.js +0 -151
- package/vendor/searchCore.js +0 -223
- package/vendor/state.js +0 -91
- package/vendor/storageSchema.js +0 -75
- package/vendor/syncOps.js +0 -188
package/vendor/adapters.js
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
// Format adapters for CSV import/export. Each adapter:
|
|
2
|
-
// - detect(headers): returns true if the headers look like its format
|
|
3
|
-
// - parse(rows): returns array of { entry, source } objects
|
|
4
|
-
// - export(entries): returns full CSV text
|
|
5
|
-
//
|
|
6
|
-
// Source metadata is stashed on each entry as `entry._source[adapterId] = row`
|
|
7
|
-
// so that exports back to the same format can preserve fields the canonical
|
|
8
|
-
// model doesn't capture (Tradelist Count, Last Modified, Misprint, etc.).
|
|
9
|
-
//
|
|
10
|
-
// Adapter precedence (used by detectAdapter): moxfield > deckbox > manabox > canonical.
|
|
11
|
-
// More-specific adapters detect first; canonical is the catch-all.
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
makeEntry,
|
|
15
|
-
normalizeFinish,
|
|
16
|
-
normalizeCondition,
|
|
17
|
-
normalizeLanguage,
|
|
18
|
-
formatLocationLabel,
|
|
19
|
-
} from './collection.js';
|
|
20
|
-
import { parseTagsCell, serializeTagsCell } from './importParsing.js';
|
|
21
|
-
|
|
22
|
-
// ---- shared helpers ----
|
|
23
|
-
const lower = arr => arr.map(s => String(s || '').toLowerCase().trim());
|
|
24
|
-
const csvCell = v => {
|
|
25
|
-
const s = v == null ? '' : String(v);
|
|
26
|
-
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
27
|
-
};
|
|
28
|
-
const csvRow = arr => arr.map(csvCell).join(',');
|
|
29
|
-
|
|
30
|
-
function rowToObject(headerRow, dataRow) {
|
|
31
|
-
const out = {};
|
|
32
|
-
for (let i = 0; i < headerRow.length; i++) {
|
|
33
|
-
out[headerRow[i]] = dataRow[i] == null ? '' : String(dataRow[i]);
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function attachSource(entry, adapterId, headerRow, dataRow) {
|
|
39
|
-
if (!entry._source || typeof entry._source !== 'object') entry._source = {};
|
|
40
|
-
entry._source[adapterId] = rowToObject(headerRow, dataRow);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getSource(entry, adapterId) {
|
|
44
|
-
return entry?._source?.[adapterId] || null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---- Canonical adapter ----
|
|
48
|
-
// The format the app already produces via exportCsv. Header aliases in
|
|
49
|
-
// importParsing.js handles minor variations (manabox/deckbox/moxfield-like CSVs that
|
|
50
|
-
// don't trip the more specific adapters' detect()).
|
|
51
|
-
const CANONICAL_HEADER = [
|
|
52
|
-
'Name', 'Set code', 'Set name', 'Collector number', 'Foil', 'Rarity', 'Quantity',
|
|
53
|
-
'Scryfall ID', 'Condition', 'Language', 'Location',
|
|
54
|
-
'Purchase price', 'Purchase price currency', 'Purchase price note', 'Tags',
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const ALIASES = {
|
|
58
|
-
name: ['name', 'card name', 'card'],
|
|
59
|
-
setCode: ['set code', 'set', 'edition', 'setcode', 'set_code'],
|
|
60
|
-
setName: ['set name', 'setname', 'edition name'],
|
|
61
|
-
cn: ['collector number', 'card number', 'cn', 'collector_number', 'number'],
|
|
62
|
-
finish: ['foil', 'finish', 'printing'],
|
|
63
|
-
qty: ['quantity', 'count', 'qty'],
|
|
64
|
-
condition: ['condition'],
|
|
65
|
-
language: ['language', 'lang'],
|
|
66
|
-
location: ['location', 'place', 'storage', 'where'],
|
|
67
|
-
scryfallId: ['scryfall id', 'scryfall_id', 'scryfallid'],
|
|
68
|
-
rarity: ['rarity'],
|
|
69
|
-
price: ['purchase price', 'price', 'tcg market price'],
|
|
70
|
-
tags: ['tags'],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function mapHeaders(headerRow) {
|
|
74
|
-
const idx = {};
|
|
75
|
-
const lc = lower(headerRow);
|
|
76
|
-
for (const [key, aliases] of Object.entries(ALIASES)) {
|
|
77
|
-
for (const a of aliases) {
|
|
78
|
-
const i = lc.indexOf(a);
|
|
79
|
-
if (i !== -1) { idx[key] = i; break; }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return idx;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const canonicalAdapter = {
|
|
86
|
-
id: 'canonical',
|
|
87
|
-
label: 'canonical CSV',
|
|
88
|
-
// Canonical is the catch-all — it accepts any CSV that has a name/id column
|
|
89
|
-
// plus enough to identify a printing.
|
|
90
|
-
detect(headerRow) {
|
|
91
|
-
const idx = mapHeaders(headerRow);
|
|
92
|
-
const hasNameOrId = idx.name !== undefined || idx.scryfallId !== undefined;
|
|
93
|
-
const hasSetAndCn = idx.setCode !== undefined && idx.cn !== undefined;
|
|
94
|
-
return hasNameOrId || hasSetAndCn;
|
|
95
|
-
},
|
|
96
|
-
parse(rows) {
|
|
97
|
-
if (!rows.length) return [];
|
|
98
|
-
const headerRow = rows[0];
|
|
99
|
-
const idx = mapHeaders(headerRow);
|
|
100
|
-
const out = [];
|
|
101
|
-
for (let r = 1; r < rows.length; r++) {
|
|
102
|
-
const row = rows[r];
|
|
103
|
-
const get = k => idx[k] !== undefined ? (row[idx[k]] || '').trim() : '';
|
|
104
|
-
const entry = makeEntry({
|
|
105
|
-
name: get('name'),
|
|
106
|
-
setCode: get('setCode').toLowerCase(),
|
|
107
|
-
setName: get('setName'),
|
|
108
|
-
cn: get('cn'),
|
|
109
|
-
finish: normalizeFinish(get('finish')),
|
|
110
|
-
qty: parseInt(get('qty'), 10) || 1,
|
|
111
|
-
condition: normalizeCondition(get('condition')),
|
|
112
|
-
language: normalizeLanguage(get('language')),
|
|
113
|
-
location: get('location'),
|
|
114
|
-
scryfallId: get('scryfallId'),
|
|
115
|
-
rarity: get('rarity').toLowerCase(),
|
|
116
|
-
price: parseFloat(get('price')) || null,
|
|
117
|
-
tags: parseTagsCell(get('tags')),
|
|
118
|
-
});
|
|
119
|
-
if (!entry.name && !entry.scryfallId && !(entry.setCode && entry.cn)) continue;
|
|
120
|
-
attachSource(entry, 'canonical', headerRow, row);
|
|
121
|
-
out.push(entry);
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
},
|
|
125
|
-
export(entries) {
|
|
126
|
-
const rows = entries.map(c => csvRow([
|
|
127
|
-
c.resolvedName || c.name,
|
|
128
|
-
c.setCode,
|
|
129
|
-
c.setName,
|
|
130
|
-
c.cn,
|
|
131
|
-
c.finish,
|
|
132
|
-
c.rarity,
|
|
133
|
-
c.qty,
|
|
134
|
-
c.scryfallId,
|
|
135
|
-
c.condition,
|
|
136
|
-
c.language,
|
|
137
|
-
formatLocationLabel(c.location),
|
|
138
|
-
c.price ?? '',
|
|
139
|
-
c.price ? 'USD' : '',
|
|
140
|
-
c.priceFallback ? 'regular usd fallback; exact finish price unavailable' : '',
|
|
141
|
-
serializeTagsCell(c.tags),
|
|
142
|
-
]));
|
|
143
|
-
return csvRow(CANONICAL_HEADER) + '\n' + rows.join('\n');
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// ---- Moxfield adapter ----
|
|
148
|
-
// Reference headers: Count, Tradelist Count, Name, Edition, Condition,
|
|
149
|
-
// Language, Foil, Tags, Last Modified, Collector Number, Alter, Proxy,
|
|
150
|
-
// Purchase Price.
|
|
151
|
-
const MOXFIELD_HEADER = [
|
|
152
|
-
'Count', 'Tradelist Count', 'Name', 'Edition', 'Condition', 'Language',
|
|
153
|
-
'Foil', 'Tags', 'Last Modified', 'Collector Number', 'Alter', 'Proxy',
|
|
154
|
-
'Purchase Price',
|
|
155
|
-
];
|
|
156
|
-
const MOXFIELD_REQUIRED = ['count', 'name', 'edition', 'collector number'];
|
|
157
|
-
const MOXFIELD_HALLMARKS = ['tradelist count', 'last modified', 'alter', 'proxy'];
|
|
158
|
-
|
|
159
|
-
function moxfieldFinishOf(raw) {
|
|
160
|
-
const v = String(raw || '').toLowerCase().trim();
|
|
161
|
-
if (v === 'foil') return 'foil';
|
|
162
|
-
if (v === 'etched') return 'etched';
|
|
163
|
-
return 'normal';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function moxfieldConditionOf(raw) {
|
|
167
|
-
const v = String(raw || '').toLowerCase().trim();
|
|
168
|
-
// Moxfield uses NM/LP/MP/HP/DMG abbreviations.
|
|
169
|
-
return normalizeCondition(v);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export const moxfieldAdapter = {
|
|
173
|
-
id: 'moxfield',
|
|
174
|
-
label: 'Moxfield',
|
|
175
|
-
detect(headerRow) {
|
|
176
|
-
const lc = lower(headerRow);
|
|
177
|
-
const hasRequired = MOXFIELD_REQUIRED.every(h => lc.includes(h));
|
|
178
|
-
if (!hasRequired) return false;
|
|
179
|
-
// At least one of the moxfield-only columns must be present so we don't
|
|
180
|
-
// false-positive on a generic CSV that happens to have those four headers.
|
|
181
|
-
return MOXFIELD_HALLMARKS.some(h => lc.includes(h));
|
|
182
|
-
},
|
|
183
|
-
parse(rows) {
|
|
184
|
-
if (!rows.length) return [];
|
|
185
|
-
const headerRow = rows[0];
|
|
186
|
-
const lc = lower(headerRow);
|
|
187
|
-
const i = name => lc.indexOf(name);
|
|
188
|
-
const colCount = i('count');
|
|
189
|
-
const colName = i('name');
|
|
190
|
-
const colEdition = i('edition');
|
|
191
|
-
const colCondition = i('condition');
|
|
192
|
-
const colLanguage = i('language');
|
|
193
|
-
const colFoil = i('foil');
|
|
194
|
-
const colTags = i('tags');
|
|
195
|
-
const colCn = i('collector number');
|
|
196
|
-
const colPrice = i('purchase price');
|
|
197
|
-
const out = [];
|
|
198
|
-
for (let r = 1; r < rows.length; r++) {
|
|
199
|
-
const row = rows[r];
|
|
200
|
-
const get = idx => idx >= 0 ? (row[idx] || '').trim() : '';
|
|
201
|
-
const name = get(colName);
|
|
202
|
-
if (!name) continue;
|
|
203
|
-
const entry = makeEntry({
|
|
204
|
-
name,
|
|
205
|
-
setCode: get(colEdition).toLowerCase(),
|
|
206
|
-
cn: get(colCn),
|
|
207
|
-
finish: moxfieldFinishOf(get(colFoil)),
|
|
208
|
-
qty: parseInt(get(colCount), 10) || 1,
|
|
209
|
-
condition: moxfieldConditionOf(get(colCondition)),
|
|
210
|
-
language: normalizeLanguage(get(colLanguage)),
|
|
211
|
-
price: parseFloat(get(colPrice)) || null,
|
|
212
|
-
tags: get(colTags) ? get(colTags).split(',').map(t => t.trim()).filter(Boolean) : [],
|
|
213
|
-
});
|
|
214
|
-
attachSource(entry, 'moxfield', headerRow, row);
|
|
215
|
-
out.push(entry);
|
|
216
|
-
}
|
|
217
|
-
return out;
|
|
218
|
-
},
|
|
219
|
-
export(entries) {
|
|
220
|
-
const rows = entries.map(c => {
|
|
221
|
-
const src = getSource(c, 'moxfield') || {};
|
|
222
|
-
const finishOut = c.finish === 'foil' ? 'foil' : c.finish === 'etched' ? 'etched' : '';
|
|
223
|
-
const condMap = { near_mint: 'NM', lightly_played: 'LP', moderately_played: 'MP', heavily_played: 'HP', damaged: 'DMG' };
|
|
224
|
-
return csvRow([
|
|
225
|
-
c.qty,
|
|
226
|
-
src['Tradelist Count'] ?? 0,
|
|
227
|
-
c.resolvedName || c.name,
|
|
228
|
-
(c.setCode || '').toLowerCase(),
|
|
229
|
-
condMap[c.condition] || 'NM',
|
|
230
|
-
(c.language || 'en').toLowerCase(),
|
|
231
|
-
finishOut,
|
|
232
|
-
(Array.isArray(c.tags) ? c.tags.join(',') : ''),
|
|
233
|
-
src['Last Modified'] ?? '',
|
|
234
|
-
c.cn || '',
|
|
235
|
-
src['Alter'] ?? 'False',
|
|
236
|
-
src['Proxy'] ?? 'False',
|
|
237
|
-
c.price ?? '',
|
|
238
|
-
]);
|
|
239
|
-
});
|
|
240
|
-
return csvRow(MOXFIELD_HEADER) + '\n' + rows.join('\n');
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// ---- ManaBox adapter ----
|
|
245
|
-
// Reference headers: Name, Set code, Set name, Collector number, Foil, Rarity,
|
|
246
|
-
// Quantity, ManaBox ID, Scryfall ID, Purchase price, Misprint, Altered,
|
|
247
|
-
// Condition, Language, Purchase price currency.
|
|
248
|
-
const MANABOX_HEADER = [
|
|
249
|
-
'Name', 'Set code', 'Set name', 'Collector number', 'Foil', 'Rarity',
|
|
250
|
-
'Quantity', 'ManaBox ID', 'Scryfall ID', 'Purchase price', 'Misprint',
|
|
251
|
-
'Altered', 'Condition', 'Language', 'Purchase price currency',
|
|
252
|
-
];
|
|
253
|
-
const MANABOX_HALLMARKS = ['manabox id', 'misprint', 'altered', 'purchase price currency'];
|
|
254
|
-
|
|
255
|
-
export const manaboxAdapter = {
|
|
256
|
-
id: 'manabox',
|
|
257
|
-
label: 'ManaBox',
|
|
258
|
-
detect(headerRow) {
|
|
259
|
-
const lc = lower(headerRow);
|
|
260
|
-
// Need at least two manabox-specific columns to claim a manabox CSV.
|
|
261
|
-
return MANABOX_HALLMARKS.filter(h => lc.includes(h)).length >= 2;
|
|
262
|
-
},
|
|
263
|
-
parse(rows) {
|
|
264
|
-
if (!rows.length) return [];
|
|
265
|
-
const headerRow = rows[0];
|
|
266
|
-
const lc = lower(headerRow);
|
|
267
|
-
const i = name => lc.indexOf(name);
|
|
268
|
-
const out = [];
|
|
269
|
-
for (let r = 1; r < rows.length; r++) {
|
|
270
|
-
const row = rows[r];
|
|
271
|
-
const get = idx => idx >= 0 ? (row[idx] || '').trim() : '';
|
|
272
|
-
const name = get(i('name'));
|
|
273
|
-
if (!name && !get(i('scryfall id'))) continue;
|
|
274
|
-
const entry = makeEntry({
|
|
275
|
-
name,
|
|
276
|
-
setCode: get(i('set code')).toLowerCase(),
|
|
277
|
-
setName: get(i('set name')),
|
|
278
|
-
cn: get(i('collector number')),
|
|
279
|
-
finish: normalizeFinish(get(i('foil'))),
|
|
280
|
-
qty: parseInt(get(i('quantity')), 10) || 1,
|
|
281
|
-
condition: normalizeCondition(get(i('condition'))),
|
|
282
|
-
language: normalizeLanguage(get(i('language'))),
|
|
283
|
-
scryfallId: get(i('scryfall id')),
|
|
284
|
-
rarity: get(i('rarity')).toLowerCase(),
|
|
285
|
-
price: parseFloat(get(i('purchase price'))) || null,
|
|
286
|
-
});
|
|
287
|
-
attachSource(entry, 'manabox', headerRow, row);
|
|
288
|
-
out.push(entry);
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
},
|
|
292
|
-
export(entries) {
|
|
293
|
-
const finishOut = f => f === 'foil' ? 'foil' : f === 'etched' ? 'etched' : 'normal';
|
|
294
|
-
const condOut = {
|
|
295
|
-
near_mint: 'near_mint', lightly_played: 'lightly_played',
|
|
296
|
-
moderately_played: 'moderately_played', heavily_played: 'heavily_played',
|
|
297
|
-
damaged: 'damaged',
|
|
298
|
-
};
|
|
299
|
-
const rows = entries.map(c => {
|
|
300
|
-
const src = getSource(c, 'manabox') || {};
|
|
301
|
-
return csvRow([
|
|
302
|
-
c.resolvedName || c.name,
|
|
303
|
-
(c.setCode || '').toLowerCase(),
|
|
304
|
-
c.setName || src['Set name'] || '',
|
|
305
|
-
c.cn || '',
|
|
306
|
-
finishOut(c.finish),
|
|
307
|
-
c.rarity || '',
|
|
308
|
-
c.qty,
|
|
309
|
-
src['ManaBox ID'] ?? '',
|
|
310
|
-
c.scryfallId || '',
|
|
311
|
-
c.price ?? '',
|
|
312
|
-
src['Misprint'] ?? 'false',
|
|
313
|
-
src['Altered'] ?? 'false',
|
|
314
|
-
condOut[c.condition] || 'near_mint',
|
|
315
|
-
c.language || 'en',
|
|
316
|
-
c.price ? 'USD' : (src['Purchase price currency'] || ''),
|
|
317
|
-
]);
|
|
318
|
-
});
|
|
319
|
-
return csvRow(MANABOX_HEADER) + '\n' + rows.join('\n');
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
// ---- Deckbox adapter ----
|
|
324
|
-
// Reference headers: Count, Tradelist Count, Name, Edition, Card Number,
|
|
325
|
-
// Condition, Language, Foil, Signed, Artist Proof, Altered Art, Misprint,
|
|
326
|
-
// Promo, Textless, My Price.
|
|
327
|
-
const DECKBOX_HEADER = [
|
|
328
|
-
'Count', 'Tradelist Count', 'Name', 'Edition', 'Card Number', 'Condition',
|
|
329
|
-
'Language', 'Foil', 'Signed', 'Artist Proof', 'Altered Art', 'Misprint',
|
|
330
|
-
'Promo', 'Textless', 'My Price',
|
|
331
|
-
];
|
|
332
|
-
const DECKBOX_REQUIRED = ['count', 'name', 'edition', 'card number'];
|
|
333
|
-
// "Card Number" + "Tradelist Count" + "Signed/Artist Proof/Altered Art" hallmarks.
|
|
334
|
-
const DECKBOX_HALLMARKS = ['signed', 'artist proof', 'altered art', 'textless', 'my price'];
|
|
335
|
-
|
|
336
|
-
function deckboxConditionOf(raw) {
|
|
337
|
-
const v = String(raw || '').toLowerCase().trim();
|
|
338
|
-
// Deckbox uses long names: "Near Mint", "Mint", "Good (Lightly Played)", etc.
|
|
339
|
-
if (v.includes('near mint') || v === 'mint') return 'near_mint';
|
|
340
|
-
if (v.includes('lightly') || v.includes('good')) return 'lightly_played';
|
|
341
|
-
if (v.includes('played') || v.includes('moderate')) return 'moderately_played';
|
|
342
|
-
if (v.includes('heavily') || v.includes('poor')) return 'heavily_played';
|
|
343
|
-
if (v.includes('damaged')) return 'damaged';
|
|
344
|
-
return normalizeCondition(v);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export const deckboxAdapter = {
|
|
348
|
-
id: 'deckbox',
|
|
349
|
-
label: 'Deckbox',
|
|
350
|
-
detect(headerRow) {
|
|
351
|
-
const lc = lower(headerRow);
|
|
352
|
-
const hasRequired = DECKBOX_REQUIRED.every(h => lc.includes(h));
|
|
353
|
-
if (!hasRequired) return false;
|
|
354
|
-
return DECKBOX_HALLMARKS.some(h => lc.includes(h));
|
|
355
|
-
},
|
|
356
|
-
parse(rows) {
|
|
357
|
-
if (!rows.length) return [];
|
|
358
|
-
const headerRow = rows[0];
|
|
359
|
-
const lc = lower(headerRow);
|
|
360
|
-
const i = name => lc.indexOf(name);
|
|
361
|
-
const out = [];
|
|
362
|
-
for (let r = 1; r < rows.length; r++) {
|
|
363
|
-
const row = rows[r];
|
|
364
|
-
const get = idx => idx >= 0 ? (row[idx] || '').trim() : '';
|
|
365
|
-
const name = get(i('name'));
|
|
366
|
-
if (!name) continue;
|
|
367
|
-
const foilRaw = get(i('foil'));
|
|
368
|
-
const foilLc = foilRaw.toLowerCase();
|
|
369
|
-
const finish = foilLc === 'foil' || foilLc === 'true' || foilLc === 'yes' ? 'foil' : 'normal';
|
|
370
|
-
const entry = makeEntry({
|
|
371
|
-
name,
|
|
372
|
-
// Deckbox 'Edition' is the SET NAME, not the code. Stash and let
|
|
373
|
-
// scryfall resolve via name+number.
|
|
374
|
-
setName: get(i('edition')),
|
|
375
|
-
cn: get(i('card number')),
|
|
376
|
-
finish,
|
|
377
|
-
qty: parseInt(get(i('count')), 10) || 1,
|
|
378
|
-
condition: deckboxConditionOf(get(i('condition'))),
|
|
379
|
-
language: normalizeLanguage(get(i('language'))),
|
|
380
|
-
price: parseFloat(get(i('my price'))) || null,
|
|
381
|
-
});
|
|
382
|
-
attachSource(entry, 'deckbox', headerRow, row);
|
|
383
|
-
out.push(entry);
|
|
384
|
-
}
|
|
385
|
-
return out;
|
|
386
|
-
},
|
|
387
|
-
export(entries) {
|
|
388
|
-
const condMap = {
|
|
389
|
-
near_mint: 'Near Mint', lightly_played: 'Good (Lightly Played)',
|
|
390
|
-
moderately_played: 'Played', heavily_played: 'Heavily Played',
|
|
391
|
-
damaged: 'Poor',
|
|
392
|
-
};
|
|
393
|
-
const rows = entries.map(c => {
|
|
394
|
-
const src = getSource(c, 'deckbox') || {};
|
|
395
|
-
return csvRow([
|
|
396
|
-
c.qty,
|
|
397
|
-
src['Tradelist Count'] ?? 0,
|
|
398
|
-
c.resolvedName || c.name,
|
|
399
|
-
c.setName || src['Edition'] || (c.setCode || '').toUpperCase(),
|
|
400
|
-
c.cn || '',
|
|
401
|
-
condMap[c.condition] || 'Near Mint',
|
|
402
|
-
c.language || 'English',
|
|
403
|
-
c.finish === 'foil' ? 'foil' : '',
|
|
404
|
-
src['Signed'] ?? '',
|
|
405
|
-
src['Artist Proof'] ?? '',
|
|
406
|
-
src['Altered Art'] ?? '',
|
|
407
|
-
src['Misprint'] ?? '',
|
|
408
|
-
src['Promo'] ?? '',
|
|
409
|
-
src['Textless'] ?? '',
|
|
410
|
-
c.price ?? '',
|
|
411
|
-
]);
|
|
412
|
-
});
|
|
413
|
-
return csvRow(DECKBOX_HEADER) + '\n' + rows.join('\n');
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// ---- Registry + dispatch ----
|
|
418
|
-
// Order matters — most-specific first. detectAdapter walks in order and
|
|
419
|
-
// returns the first match. Canonical is the fallback catch-all.
|
|
420
|
-
export const ADAPTERS = [moxfieldAdapter, deckboxAdapter, manaboxAdapter, canonicalAdapter];
|
|
421
|
-
|
|
422
|
-
export function getAdapter(id) {
|
|
423
|
-
return ADAPTERS.find(a => a.id === id) || null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export function detectAdapter(headerRow) {
|
|
427
|
-
for (const a of ADAPTERS) {
|
|
428
|
-
if (a.detect(headerRow)) return a;
|
|
429
|
-
}
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Merge source metadata across two entries (used by mergeIntoCollection so
|
|
434
|
-
// re-imports don't drop earlier-import preserved fields). The newer source
|
|
435
|
-
// wins per-format.
|
|
436
|
-
export function mergeSource(existing, incoming) {
|
|
437
|
-
if (!incoming?._source) return existing;
|
|
438
|
-
if (!existing._source || typeof existing._source !== 'object') existing._source = {};
|
|
439
|
-
for (const [id, row] of Object.entries(incoming._source)) {
|
|
440
|
-
existing._source[id] = row;
|
|
441
|
-
}
|
|
442
|
-
return existing;
|
|
443
|
-
}
|