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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495import { describe, it, expect } from "bun:test";import { getGoodreadsCsvParser, getHardcoverCsvParser, getStorygraphCsvParser } from "./csv";import { HARDCOVER_CSV } from "../workers/import/__fixtures__/hardcover-csv";
describe("CSV Parsers", () => { describe("Goodreads CSV Parser", () => { it("should parse basic Goodreads CSV data correctly", async () => { // Note: Goodreads parser expects data starting from line 2 (skips header) and uses commas const csvData = `Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Owned Copies18143945,Europe in Autumn,Dave Hutchinson,"Hutchinson, Dave",,,,0,3.71,Solaris,Paperback,429,2014,2014,,2025/02/21,to-read,"to-read (#171)",to-read,,,,0,036510196,Old Man's War,John Scalzi,"Scalzi, John",,,,5,4.23,Tor Books,Kindle Edition,318,2007,2005,2024/12/15,2025/02/14,read,"read (#1)",read,Great book!,false,,1,1`;
const parser = getGoodreadsCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(2);
// Test first book (to-read) expect(books[0]).toMatchObject({ bookId: "18143945", title: "Europe in Autumn", author: "Dave Hutchinson", myRating: 0, averageRating: 3.71, publisher: "Solaris", numberOfPages: 429, yearPublished: 2014, originalPublicationYear: 2014, dateRead: null, exclusiveShelf: "to-read", bookshelves: ["to-read"], spoiler: false, readCount: 0, ownedCopies: 0, });
// Test second book (read with rating and review) expect(books[1]).toMatchObject({ bookId: "36510196", title: "Old Man's War", author: "John Scalzi", myRating: 5, averageRating: 4.23, publisher: "Tor Books", numberOfPages: 318, yearPublished: 2007, originalPublicationYear: 2005, exclusiveShelf: "read", myReview: "Great book!", spoiler: false, readCount: 1, ownedCopies: 1, });
expect(books[1]!.dateRead).toBeInstanceOf(Date); expect(books[1]!.dateAdded).toBeInstanceOf(Date); });
it("should handle arrays and bookshelves correctly", async () => { const csvData = `Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Owned Copies123,Test Book,Author Name,"Name, Author","Co-Author, Second Author",,,4,4.5,Test Publisher,Paperback,300,2024,2024,2024/01/15,2024/01/01,"read, favorites","read (#1), favorites (#5)",read,Great book!,false,Private note,1,1`;
const parser = getGoodreadsCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(1); expect(books[0]).toMatchObject({ bookId: "123", title: "Test Book", author: "Author Name", authorLastFirst: "Name, Author", additionalAuthors: ["Co-Author", "Second Author"], myRating: 4, averageRating: 4.5, bookshelves: ["read", "favorites"], exclusiveShelf: "read", myReview: "Great book!", spoiler: false, privateNotes: "Private note", }); });
it("should parse Goodreads exports when Average Rating column is absent", async () => { const csvData = `Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Owned Copies456,Future Reading,Example Author,"Author, Example",Second Author,"=""0123456789""","=""9780123456789""",0,Test Publisher,Hardcover,250,2024,2024,,2026/06/07,to-read,to-read (#158),to-read,,,,0,0`;
const parser = getGoodreadsCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(1); expect(books[0]).toMatchObject({ bookId: "456", title: "Future Reading", author: "Example Author", authorLastFirst: "Author, Example", additionalAuthors: ["Second Author"], isbn: "0123456789", isbn13: "9780123456789", myRating: 0, averageRating: 0, publisher: "Test Publisher", binding: "Hardcover", numberOfPages: 250, yearPublished: 2024, originalPublicationYear: 2024, dateRead: null, bookshelves: ["to-read"], bookshelvesWithPositions: "to-read (#158)", exclusiveShelf: "to-read", readCount: 0, ownedCopies: 0, }); expect(books[0]!.dateAdded).toBeInstanceOf(Date); expect(books[0]!.dateAdded?.getFullYear()).toBe(2026); }); });
describe("StoryGraph CSV Parser", () => { it("should parse basic StoryGraph CSV data correctly", async () => { const csvData = `Title,Authors,Contributors,ISBN/UID,Format,Read Status,Date Added,Last Date Read,Dates Read,Read Count,Moods,Pace,Character- or Plot-Driven?,Strong Character Development?,Loveable Characters?,Diverse Characters?,Flawed Characters?,Star Rating,Review,Content Warnings,Content Warning Description,Tags,Owned?Nemesis Games,James S. A. Corey,"",9780316217590,digital,read,2017/12/13,"","",1,"",,,,,,,4.0,,"",,"",NoArtificial Condition,Martha Wells,"","",audio,read,2021/08/31,2021/01/05,2021/01/05,1,"",,,,,,,,,"",,"",NoWhere the Axe Is Buried,Ray Nayler,"",9780374615369,hardcover,to-read,2025/04/14,"","",0,"",,,,,,,,,"",,"",No`;
const parser = getStorygraphCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(3);
// Test first book (read with rating) expect(books[0]).toMatchObject({ title: "Nemesis Games", authors: "James S. A. Corey", contributors: "", isbn: "9780316217590", format: "digital", readStatus: "read", starRating: 4.0, readCount: 1, owned: false, review: "", lastDateRead: null, });
expect(books[0]!.dateAdded).toBeInstanceOf(Date); expect(books[0]!.dateAdded?.getFullYear()).toBe(2017);
// Test second book (read with last date read) expect(books[1]).toMatchObject({ title: "Artificial Condition", authors: "Martha Wells", isbn: "", format: "audio", readStatus: "read", starRating: 0, readCount: 1, owned: false, });
expect(books[1]!.lastDateRead).toBeInstanceOf(Date); expect(books[1]!.lastDateRead?.getFullYear()).toBe(2021);
// Test third book (to-read) expect(books[2]).toMatchObject({ title: "Where the Axe Is Buried", authors: "Ray Nayler", isbn: "9780374615369", format: "hardcover", readStatus: "to-read", starRating: 0, readCount: 0, owned: false, lastDateRead: null, }); });
it("should handle empty values and different read statuses correctly", async () => { const csvData = `Title,Authors,Contributors,ISBN/UID,Format,Read Status,Date Added,Last Date Read,Dates Read,Read Count,Moods,Pace,Character- or Plot-Driven?,Strong Character Development?,Loveable Characters?,Diverse Characters?,Flawed Characters?,Star Rating,Review,Content Warnings,Content Warning Description,Tags,Owned?Test Book,Test Author,"","",ebook,currently-reading,2024/01/01,"","",2,fast,slow,character,yes,yes,no,yes,3.5,"Great so far!",violence,"Some violence",sci-fi,Yes`;
const parser = getStorygraphCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(1);
// Test book with various filled fields expect(books[0]).toMatchObject({ title: "Test Book", authors: "Test Author", readStatus: "currently-reading", starRating: 3.5, readCount: 2, review: "Great so far!", moods: "fast", pace: "slow", characterOrPlot: "character", strongCharacterDevelopment: "yes", contentWarnings: "violence", contentWarningDescription: "Some violence", tags: "sci-fi", owned: true, }); }); });
describe("Hardcover CSV Parser", () => { it("should parse basic Hardcover CSV data correctly", async () => { const csvData = HARDCOVER_CSV; const parser = getHardcoverCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(3);
// Test first book (want to read) expect(books[0]).toMatchObject({ asin: "0374100144", author: "Roberto Bolaño, Natasha Wimmer (Translator)", binding: "", compilation: false, contentWarnings: "", countryCode: "us", dateAdded: new Date("2025-01-15T00:00:00.000Z"), dateFinished: null, dateStarted: null, durationInSeconds: 0, genres: "", hardcoverBookId: "75726", hardcoverEditionId: "25012766", isbn10: "0374100144", isbn13: "9780374100148", languageCode: "en", lists: "", media: "Book", moods: "", owned: false, pages: 898, privacy: "Public", privateNotes: "", publishDate: new Date("2008-11-11T00:00:00.000Z"), publisher: "Farrar, Straus and Giroux", rating: 0, review: "", reviewContainsSpoilers: false, reviewDate: new Date("2025-01-15T13:56:31.000Z"), reviewMediaUrl: "", reviewSlate: "{}", reviewUrl: "", series: "2666 (#1.0)", sponsoredReview: false, status: "buzz.bookhive.defs#wantToRead", title: "2666", });
expect(books[0]!.dateAdded).toBeInstanceOf(Date); expect(books[0]!.dateAdded?.getFullYear()).toBe(2025);
// Test second book (read with date finished) expect(books[1]).toMatchObject({ asin: "B0036G94XY", author: "William Gibson", binding: "", compilation: false, contentWarnings: "", countryCode: "us", dateAdded: new Date("2025-01-10T00:00:00.000Z"), dateFinished: new Date("2025-01-23T00:00:00.000Z"), dateStarted: new Date("2025-01-01T00:00:00.000Z"), durationInSeconds: 25686, genres: "", hardcoverBookId: "2440", hardcoverEditionId: "31159321", isbn10: "", isbn13: "", languageCode: "en", lists: "Owned, Shelved By Genre Reading List (#19)", media: "Audio", moods: "", owned: true, pages: 191, privacy: "Public", privateNotes: "", publishDate: new Date("1986-04-01T00:00:00.000Z"), publisher: "Audible Frontiers", rating: 9, review: "", reviewContainsSpoilers: false, reviewDate: new Date("2025-01-10T17:21:40.000Z"), reviewMediaUrl: "", reviewSlate: "{}", reviewUrl: "", series: "Sprawl (#0.0)", sponsoredReview: false, status: "buzz.bookhive.defs#finished", tags: "", title: "Burning Chrome", });
expect(books[1]!.dateFinished).toBeInstanceOf(Date); expect(books[1]!.dateFinished?.getFullYear()).toBe(2025);
// Test third book (currently-read) expect(books[2]).toMatchObject({ asin: "", author: "Dan Simmons", binding: "", compilation: false, contentWarnings: "", countryCode: "us", dateAdded: new Date("2025-01-15T00:00:00.000Z"), dateFinished: null, dateStarted: new Date("2025-01-25T00:00:00.000Z"), durationInSeconds: 0, genres: "", hardcoverBookId: "427460", hardcoverEditionId: "30428122", isbn10: "0385263481", isbn13: "9780385263481", languageCode: "en", lists: "Owned", media: "Book", moods: "", owned: true, pages: 492, privacy: "Public", privateNotes: "", publishDate: new Date("1989-05-26T00:00:00.000Z"), publisher: "Crown", rating: 0, review: "", reviewContainsSpoilers: false, reviewDate: new Date("2025-01-15T13:56:57.000Z"), reviewMediaUrl: "", reviewSlate: "{}", reviewUrl: "", series: "Hyperion Cantos (#1.0)", sponsoredReview: false, status: "buzz.bookhive.defs#reading", tags: "", title: "Hyperion", }); });
it("should handle empty values and different read statuses correctly", async () => { const csvData = `Title,Author,Series,Status,Privacy,Hardcover Book ID,Hardcover Edition ID,ISBN 10,ISBN 13,ASIN,Media,Country Code,Language Code,Binding,Pages,Duration in Seconds,Publish Date,Publisher,Genres,Moods,Tags,Content Warnings,Lists,Date Added,Date Started,Date Finished,Rating,Review,Review Contains Spoilers,Sponsored Review,Review Date,Review URL,Review Media URL,Private Notes,Owned,Compilation,Review SlateMistborn: The Final Empire,Brandon Sanderson,The Mistborn Saga: The Original Trilogy (#1.0),Stopped,Public,369692,30432878,0765377136,9780765377135,,Book,us,en,,541,,2006-07-17,Tor Teen,,,,,Owned,2025-01-15,2024-08-01,"",1.5,,false,false,2025-01-15T13:57:07Z,,,,true,No,{}`;
const parser = getHardcoverCsvParser(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(csvData)); controller.close(); }, });
const books = []; const reader = stream.pipeThrough(parser).getReader();
while (true) { const { done, value } = await reader.read(); if (done) break; books.push(value); }
expect(books).toHaveLength(1);
// Test book with various filled fields expect(books[0]).toMatchObject({ asin: "", author: "Brandon Sanderson", binding: "", compilation: false, contentWarnings: "", countryCode: "us", dateAdded: new Date("2025-01-15T00:00:00.000Z"), dateFinished: null, dateStarted: new Date("2024-08-01T00:00:00.000Z"), durationInSeconds: 0, genres: "", hardcoverBookId: "369692", hardcoverEditionId: "30432878", isbn10: "0765377136", isbn13: "9780765377135", languageCode: "en", lists: "Owned", media: "Book", moods: "", owned: true, pages: 541, privacy: "Public", privateNotes: "", publishDate: new Date("2006-07-17T00:00:00.000Z"), publisher: "Tor Teen", rating: 3, review: "", reviewContainsSpoilers: false, reviewDate: new Date("2025-01-15T13:57:07.000Z"), reviewMediaUrl: "", reviewSlate: "{}", reviewUrl: "", series: "The Mistborn Saga: The Original Trilogy (#1.0)", sponsoredReview: false, status: "buzz.bookhive.defs#wantToRead", tags: "", title: "Mistborn: The Final Empire", }); }); });});