2023-11-20 01:37:55 +09:00
import fs from "node:fs/promises" ;
import { createWriteStream } from "node:fs" ;
import stream from "node:stream/promises" ;
import { Zip , ZipPassThrough } from "fflate" ;
2023-11-19 02:25:32 +09:00
import { Database } from "./database" ;
2023-12-02 23:22:33 +09:00
import { type TPlatform , site } from "./platform" ;
2023-11-19 02:25:32 +09:00
export type Book = {
id : number ;
2023-12-02 23:22:33 +09:00
platform : TPlatform ;
2023-11-19 02:25:32 +09:00
readerUrl : string ;
2023-11-20 22:56:09 +09:00
title : string ;
authors : Array < string > ;
2023-11-19 02:25:32 +09:00
} ;
export function createLibrary ( db : Database ) {
return {
2023-11-20 22:56:09 +09:00
async add ( readerUrlOrBook : string | Book ) {
if ( typeof readerUrlOrBook === "string" ) {
2023-12-02 23:22:33 +09:00
const platform = site ( readerUrlOrBook ) ;
2023-11-20 22:56:09 +09:00
await db . run (
2023-12-03 05:54:45 +09:00
` \
insert into books (
platform_id ,
reader_url )
values ( ( select id from platforms where name = ? ) , ? )
on conflict ( reader_url )
do nothing
` ,
2023-11-20 22:56:09 +09:00
platform ,
readerUrlOrBook ,
) ;
return ;
}
2023-11-19 02:25:32 +09:00
await db . run (
2023-11-20 22:56:09 +09:00
` \
insert into books (
platform_id ,
reader_url ,
title ,
authors )
values ( ( select id from platforms where name = ? ) , ? , ? , ? )
on conflict ( reader_url )
do update set title = excluded . title , authors = excluded . authors
` ,
2023-12-02 23:22:33 +09:00
readerUrlOrBook . platform ,
2023-11-20 22:56:09 +09:00
readerUrlOrBook . readerUrl ,
readerUrlOrBook . title ,
JSON . stringify ( readerUrlOrBook . authors ) ,
2023-11-19 02:25:32 +09:00
) ;
2023-11-19 20:51:29 +09:00
} ,
async delete ( id : number ) {
await db . run ( ` delete from books where id = ? ` , id ) ;
2023-11-19 02:25:32 +09:00
} ,
2023-12-03 05:54:45 +09:00
async get ( readerUrlOrBookId : string | number ) : Promise < Book | undefined > {
2023-11-20 22:56:09 +09:00
const row = await db . get (
2023-12-03 05:54:45 +09:00
` select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id where books.reader_url = ? or books.id = ? ` ,
readerUrlOrBookId ,
Number ( readerUrlOrBookId ) ,
2023-11-19 22:04:19 +09:00
) ;
2023-11-20 22:56:09 +09:00
const book : Book | undefined = row && {
. . . row ,
authors : JSON.parse ( row . authors ) ,
} ;
2023-11-19 22:04:19 +09:00
return book ;
} ,
2023-11-19 02:25:32 +09:00
async getBooks ( ) : Promise < Array < Book > > {
2023-11-20 22:56:09 +09:00
const rows = await db . all (
` select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id ` ,
2023-11-19 02:25:32 +09:00
) ;
2023-11-20 22:56:09 +09:00
const books : Array < Book > = rows . map ( ( row ) = > ( {
. . . row ,
authors : JSON.parse ( row . authors ) ,
} ) ) ;
2023-11-19 02:25:32 +09:00
return books ;
} ,
2023-12-02 15:56:24 +09:00
async archive (
path : string ,
book : Book ,
opts : {
outDir : string ;
outAuthorsLimit : number ;
} ,
) {
2023-11-23 10:24:08 +09:00
const bookDir = await fs . stat ( path ) ;
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
if ( ! bookDir . isDirectory ( ) ) {
throw new Error ( ` Not found: ${ path } ` ) ;
}
2023-12-03 05:54:45 +09:00
if ( ! book . title ) {
book . title = String ( book . id ) ;
}
if ( book . authors . length > 0 ) {
2023-12-24 23:15:06 +09:00
book . title = ` [ ${ book . authors
. slice ( 0 , opts . outAuthorsLimit )
. join ( ", " ) } ] $ { book . title } ` .replace(/[/]/g, "%2F");
2023-12-03 05:54:45 +09:00
}
2023-12-24 23:09:32 +09:00
await fs . mkdir ( opts . outDir , {
recursive : true ,
} ) ;
2023-12-03 12:44:31 +09:00
const files = await fs . readdir ( path ) ;
if ( files . every ( ( f ) = > f . match ( /[.](zip|cbz)$/ ) ) ) {
const digits = String ( files . length ) . length ;
function pad ( n : string ) {
return n . padStart ( digits , "0" ) ;
}
for ( const [ n , f ] of Object . entries ( files ) ) {
2023-12-24 23:09:32 +09:00
await fs . copyFile (
2023-12-03 12:44:31 +09:00
` ${ path } / ${ f } ` ,
2023-12-24 23:15:06 +09:00
` ${ opts . outDir } / ${ book . title } ${
2023-12-03 12:44:31 +09:00
files . length > 1 ? ` - ${ pad ( n ) } ` : ""
} . $ { f . split ( "." ) . at ( - 1 ) } ` ,
) ;
}
2023-12-24 23:09:32 +09:00
await fs . rm ( path , { recursive : true } ) ;
2023-12-03 12:44:31 +09:00
return ;
}
2023-12-24 23:15:06 +09:00
const out = createWriteStream ( ` ${ opts . outDir } / ${ book . title } .cbz ` ) ;
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
const zip = new Zip ( function cb ( err , data , final ) {
if ( err ) {
out . destroy ( err ) ;
return ;
2023-11-20 01:37:55 +09:00
}
2023-11-23 10:24:08 +09:00
out [ final ? "end" : "write" ] ( data ) ;
} ) ;
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
for ( const file of files ) {
const data = new ZipPassThrough ( file ) ;
zip . add ( data ) ;
const buffer = await fs . readFile ( ` ${ path } / ${ file } ` ) ;
data . push ( buffer , true ) ;
2023-11-20 01:37:55 +09:00
}
2023-11-23 10:24:08 +09:00
zip . end ( ) ;
await stream . finished ( out ) ;
await fs . rm ( path , { recursive : true } ) ;
2023-11-20 01:37:55 +09:00
} ,
2023-11-19 02:25:32 +09:00
} ;
}