Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/nperez0111/bookhive. Track your books, share your shelves, see what others are reading bookhive.buzz
atproto bluesky books bookshelf goodreads management-system
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302import { parse } from "csv-parse";
export interface GoodreadsBook { bookId: string; title: string; author: string; authorLastFirst: string; additionalAuthors: string[]; isbn: string; isbn13: string; myRating: number; averageRating: number; publisher: string; binding: string; numberOfPages: number; yearPublished: number; originalPublicationYear: number; dateRead: Date | null; dateAdded: Date; bookshelves: string[]; bookshelvesWithPositions: string; exclusiveShelf: string; myReview: string; spoiler: boolean; privateNotes: string; readCount: number; ownedCopies: number;}
export interface StorygraphBook { title: string; authors: string; contributors: string; isbn: string; format: string; readStatus: string; dateAdded: Date | null; lastDateRead: Date | null; datesRead: string; readCount: number; moods: string; pace: string; characterOrPlot: string; strongCharacterDevelopment: string; loveableCharacters: string; diverseCharacters: string; flawedCharacters: string; starRating: number; review: string; contentWarnings: string; contentWarningDescription: string; tags: string; owned: boolean;}
export function getStorygraphCsvParser() { const parser = parse({ skip_empty_lines: true, trim: true, columns: true, // Use first line as column headers // Add error handling options skip_records_with_error: true, skip_records_with_empty_values: false, relax_column_count: true, // Allow inconsistent column counts relax_quotes: true, // Be more lenient with quotes cast: (value: string, { column }): any => { // Handle empty values if (value === "" || value === '""') { if (column === "Star Rating") return 0; if (column === "Read Count") return 0; if (column === "Owned?") return false; return null; }
// Remove surrounding quotes if present if (value.startsWith('"') && value.endsWith('"')) { value = value.slice(1, -1); }
// Handle different columns appropriately switch (column) { case "Star Rating": // StoryGraph uses 0-5 scale, multiply by 2 to match Goodreads 0-10 scale internally return parseFloat(value) || 0; case "Read Count": return parseInt(value) || 0; case "Date Added": case "Last Date Read": // StoryGraph uses YYYY/MM/DD format return value && value !== '""' ? new Date(value) : null; case "Owned?": return value.toLowerCase() === "yes"; default: return value || ""; } }, });
return new TransformStream<Uint8Array, StorygraphBook>({ transform(chunk, controller) { try { parser.write(chunk);
// Process any records that are ready let record: any; while ((record = parser.read())) { // Validate the record before enqueueing if (record && record["Title"] && record["Authors"]) { // Map CSV columns to StorygraphBook interface const storygraphBook: StorygraphBook = { title: record["Title"] || "", authors: record["Authors"] || "", contributors: record["Contributors"] || "", isbn: record["ISBN/UID"] || "", format: record["Format"] || "", readStatus: record["Read Status"] || "", dateAdded: record["Date Added"], lastDateRead: record["Last Date Read"], datesRead: record["Dates Read"] || "", readCount: record["Read Count"] || 0, moods: record["Moods"] || "", pace: record["Pace"] || "", characterOrPlot: record["Character- or Plot-Driven?"] || "", strongCharacterDevelopment: record["Strong Character Development?"] || "", loveableCharacters: record["Loveable Characters?"] || "", diverseCharacters: record["Diverse Characters?"] || "", flawedCharacters: record["Flawed Characters?"] || "", starRating: record["Star Rating"] || 0, review: record["Review"] || "", contentWarnings: record["Content Warnings"] || "", contentWarningDescription: record["Content Warning Description"] || "", tags: record["Tags"] || "", owned: record["Owned?"] || false, }; controller.enqueue(storygraphBook); } else { console.warn("Skipping invalid StoryGraph record:", record); } } } catch (error) { console.warn("Error processing StoryGraph CSV chunk:", error); // Continue processing other chunks instead of crashing } }, flush(controller) { try { parser.end();
// Get any remaining records let record: any; while ((record = parser.read())) { // Validate the record before enqueueing if (record && record["Title"] && record["Authors"]) { const storygraphBook: StorygraphBook = { title: record["Title"] || "", authors: record["Authors"] || "", contributors: record["Contributors"] || "", isbn: record["ISBN/UID"] || "", format: record["Format"] || "", readStatus: record["Read Status"] || "", dateAdded: record["Date Added"], lastDateRead: record["Last Date Read"], datesRead: record["Dates Read"] || "", readCount: record["Read Count"] || 0, moods: record["Moods"] || "", pace: record["Pace"] || "", characterOrPlot: record["Character- or Plot-Driven?"] || "", strongCharacterDevelopment: record["Strong Character Development?"] || "", loveableCharacters: record["Loveable Characters?"] || "", diverseCharacters: record["Diverse Characters?"] || "", flawedCharacters: record["Flawed Characters?"] || "", starRating: record["Star Rating"] || 0, review: record["Review"] || "", contentWarnings: record["Content Warnings"] || "", contentWarningDescription: record["Content Warning Description"] || "", tags: record["Tags"] || "", owned: record["Owned?"] || false, }; controller.enqueue(storygraphBook); } else { console.warn("Skipping invalid StoryGraph record during flush:", record); } } } catch (error) { console.warn("Error during StoryGraph CSV parser flush:", error); // Don't crash, just log the error } }, });}
export function getGoodreadsCsvParser() { const parser = parse({ skip_empty_lines: true, trim: true, from: 2, columns: [ "bookId", "title", "author", "authorLastFirst", "additionalAuthors", "isbn", "isbn13", "myRating", "averageRating", "publisher", "binding", "numberOfPages", "yearPublished", "originalPublicationYear", "dateRead", "dateAdded", "bookshelves", "bookshelvesWithPositions", "exclusiveShelf", "myReview", "spoiler", "privateNotes", "readCount", "ownedCopies", ], cast: (value: string, { column }): any => { // First handle the quoted values if (value.startsWith('="') && value.endsWith('"')) { value = value.slice(2, -1); }
// Handle different columns appropriately switch (column) { case "bookId": return value; case "myRating": case "numberOfPages": case "yearPublished": case "originalPublicationYear": case "readCount": case "ownedCopies": return parseInt(value) || 0; case "averageRating": return parseFloat(value) || 0; case "dateRead": case "dateAdded": return value ? new Date(value) : null; case "additionalAuthors": case "bookshelves": return value ? value.split(", ").filter(Boolean) : []; case "spoiler": return value.toLowerCase() === "true"; default: return value; } }, // Add error handling options skip_records_with_error: true, skip_records_with_empty_values: false, relax_column_count: true, // Allow inconsistent column counts relax_quotes: true, // Be more lenient with quotes });
return new TransformStream<Uint8Array, GoodreadsBook>({ transform(chunk, controller) { try { parser.write(chunk);
// Process any records that are ready let record: GoodreadsBook; while ((record = parser.read() as GoodreadsBook)) { // Validate the record before enqueueing if (record && record.title && record.author) { controller.enqueue(record); } else { console.warn("Skipping invalid Goodreads record:", record); } } } catch (error) { console.warn("Error processing CSV chunk:", error); // Continue processing other chunks instead of crashing } }, flush(controller) { try { parser.end();
// Get any remaining records let record: GoodreadsBook; while ((record = parser.read() as GoodreadsBook)) { // Validate the record before enqueueing if (record && record.title && record.author) { controller.enqueue(record); } else { console.warn("Skipping invalid Goodreads record during flush:", record); } } } catch (error) { console.warn("Error during CSV parser flush:", error); // Don't crash, just log the error } }, });}