├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── book.rs ├── cmd.rs ├── library.rs └── main.rs /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.26" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" 17 | 18 | [[package]] 19 | name = "arrayref" 20 | version = "0.3.6" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 23 | 24 | [[package]] 25 | name = "arrayvec" 26 | version = "0.5.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 29 | 30 | [[package]] 31 | name = "atty" 32 | version = "0.2.14" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 35 | dependencies = [ 36 | "hermit-abi", 37 | "libc", 38 | "winapi", 39 | ] 40 | 41 | [[package]] 42 | name = "autocfg" 43 | version = "0.1.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.10.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 52 | dependencies = [ 53 | "byteorder", 54 | ] 55 | 56 | [[package]] 57 | name = "base64" 58 | version = "0.11.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 61 | 62 | [[package]] 63 | name = "bitflags" 64 | version = "1.2.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 67 | 68 | [[package]] 69 | name = "blake2b_simd" 70 | version = "0.5.10" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 73 | dependencies = [ 74 | "arrayref", 75 | "arrayvec", 76 | "constant_time_eq", 77 | ] 78 | 79 | [[package]] 80 | name = "blake3" 81 | version = "0.1.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "100179f909a27ed067ce4fa6db58f7fa0f67070d7a04d38f040886174b85ef6f" 84 | dependencies = [ 85 | "arrayref", 86 | "arrayvec", 87 | "cc", 88 | "cfg-if", 89 | "constant_time_eq", 90 | "crypto-mac", 91 | "digest", 92 | ] 93 | 94 | [[package]] 95 | name = "bumpalo" 96 | version = "3.1.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5fb8038c1ddc0a5f73787b130f4cc75151e96ed33e417fde765eb5a81e3532f4" 99 | 100 | [[package]] 101 | name = "byteorder" 102 | version = "1.3.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 105 | 106 | [[package]] 107 | name = "cc" 108 | version = "1.0.50" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 111 | 112 | [[package]] 113 | name = "cfg-if" 114 | version = "0.1.10" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 117 | 118 | [[package]] 119 | name = "chunked_transfer" 120 | version = "1.0.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "f98beb6554de08a14bd7b5c6014963c79d6a25a1c66b1d4ecb9e733ccba51d6c" 123 | 124 | [[package]] 125 | name = "clap" 126 | version = "2.33.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 129 | dependencies = [ 130 | "ansi_term", 131 | "atty", 132 | "bitflags", 133 | "strsim", 134 | "textwrap", 135 | "unicode-width", 136 | "vec_map", 137 | ] 138 | 139 | [[package]] 140 | name = "constant_time_eq" 141 | version = "0.1.5" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 144 | 145 | [[package]] 146 | name = "cookie" 147 | version = "0.12.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" 150 | dependencies = [ 151 | "time", 152 | "url 1.7.2", 153 | ] 154 | 155 | [[package]] 156 | name = "crossbeam-utils" 157 | version = "0.7.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" 160 | dependencies = [ 161 | "autocfg", 162 | "cfg-if", 163 | "lazy_static", 164 | ] 165 | 166 | [[package]] 167 | name = "crypto-mac" 168 | version = "0.7.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" 171 | dependencies = [ 172 | "generic-array", 173 | "subtle", 174 | ] 175 | 176 | [[package]] 177 | name = "digest" 178 | version = "0.8.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 181 | dependencies = [ 182 | "generic-array", 183 | ] 184 | 185 | [[package]] 186 | name = "dirs" 187 | version = "2.0.2" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 190 | dependencies = [ 191 | "cfg-if", 192 | "dirs-sys", 193 | ] 194 | 195 | [[package]] 196 | name = "dirs-sys" 197 | version = "0.3.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" 200 | dependencies = [ 201 | "cfg-if", 202 | "libc", 203 | "redox_users", 204 | "winapi", 205 | ] 206 | 207 | [[package]] 208 | name = "fuzzy-matcher" 209 | version = "0.3.3" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "c20c3dd0480475a1e6da2f0d4dad7052d81386f56be9e23622e027c9240839b6" 212 | dependencies = [ 213 | "thread_local", 214 | ] 215 | 216 | [[package]] 217 | name = "generic-array" 218 | version = "0.12.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" 221 | dependencies = [ 222 | "typenum", 223 | ] 224 | 225 | [[package]] 226 | name = "getrandom" 227 | version = "0.1.14" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 230 | dependencies = [ 231 | "cfg-if", 232 | "libc", 233 | "wasi", 234 | ] 235 | 236 | [[package]] 237 | name = "heck" 238 | version = "0.3.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 241 | dependencies = [ 242 | "unicode-segmentation", 243 | ] 244 | 245 | [[package]] 246 | name = "hermit-abi" 247 | version = "0.1.6" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" 250 | dependencies = [ 251 | "libc", 252 | ] 253 | 254 | [[package]] 255 | name = "hex" 256 | version = "0.4.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" 259 | 260 | [[package]] 261 | name = "idna" 262 | version = "0.1.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" 265 | dependencies = [ 266 | "matches", 267 | "unicode-bidi", 268 | "unicode-normalization", 269 | ] 270 | 271 | [[package]] 272 | name = "idna" 273 | version = "0.2.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 276 | dependencies = [ 277 | "matches", 278 | "unicode-bidi", 279 | "unicode-normalization", 280 | ] 281 | 282 | [[package]] 283 | name = "itoa" 284 | version = "0.4.5" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 287 | 288 | [[package]] 289 | name = "js-sys" 290 | version = "0.3.35" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" 293 | dependencies = [ 294 | "wasm-bindgen", 295 | ] 296 | 297 | [[package]] 298 | name = "lazy_static" 299 | version = "1.4.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 302 | 303 | [[package]] 304 | name = "libc" 305 | version = "0.2.66" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 308 | 309 | [[package]] 310 | name = "librarian" 311 | version = "0.1.0" 312 | dependencies = [ 313 | "anyhow", 314 | "blake3", 315 | "dirs", 316 | "fuzzy-matcher", 317 | "hex", 318 | "open", 319 | "scrawl", 320 | "serde", 321 | "serde_json", 322 | "structopt", 323 | "ureq", 324 | ] 325 | 326 | [[package]] 327 | name = "log" 328 | version = "0.4.8" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 331 | dependencies = [ 332 | "cfg-if", 333 | ] 334 | 335 | [[package]] 336 | name = "matches" 337 | version = "0.1.8" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 340 | 341 | [[package]] 342 | name = "memchr" 343 | version = "2.3.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" 346 | 347 | [[package]] 348 | name = "nom" 349 | version = "4.2.3" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 352 | dependencies = [ 353 | "memchr", 354 | "version_check", 355 | ] 356 | 357 | [[package]] 358 | name = "open" 359 | version = "1.3.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "3dfa632621d66502e1e9298c038d903090fc810a33cc1e6a02958fa0be65e3fb" 362 | dependencies = [ 363 | "winapi", 364 | ] 365 | 366 | [[package]] 367 | name = "percent-encoding" 368 | version = "1.0.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" 371 | 372 | [[package]] 373 | name = "percent-encoding" 374 | version = "2.1.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 377 | 378 | [[package]] 379 | name = "proc-macro-error" 380 | version = "0.4.8" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a" 383 | dependencies = [ 384 | "proc-macro-error-attr", 385 | "proc-macro2", 386 | "quote", 387 | "rustversion", 388 | "syn", 389 | ] 390 | 391 | [[package]] 392 | name = "proc-macro-error-attr" 393 | version = "0.4.8" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a" 396 | dependencies = [ 397 | "proc-macro2", 398 | "quote", 399 | "rustversion", 400 | "syn", 401 | "syn-mid", 402 | ] 403 | 404 | [[package]] 405 | name = "proc-macro2" 406 | version = "1.0.8" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" 409 | dependencies = [ 410 | "unicode-xid", 411 | ] 412 | 413 | [[package]] 414 | name = "qstring" 415 | version = "0.7.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" 418 | dependencies = [ 419 | "percent-encoding 2.1.0", 420 | ] 421 | 422 | [[package]] 423 | name = "quote" 424 | version = "1.0.2" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 427 | dependencies = [ 428 | "proc-macro2", 429 | ] 430 | 431 | [[package]] 432 | name = "redox_syscall" 433 | version = "0.1.56" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 436 | 437 | [[package]] 438 | name = "redox_users" 439 | version = "0.3.4" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" 442 | dependencies = [ 443 | "getrandom", 444 | "redox_syscall", 445 | "rust-argon2", 446 | ] 447 | 448 | [[package]] 449 | name = "ring" 450 | version = "0.16.11" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "741ba1704ae21999c00942f9f5944f801e977f54302af346b596287599ad1862" 453 | dependencies = [ 454 | "cc", 455 | "lazy_static", 456 | "libc", 457 | "spin", 458 | "untrusted", 459 | "web-sys", 460 | "winapi", 461 | ] 462 | 463 | [[package]] 464 | name = "rust-argon2" 465 | version = "0.7.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" 468 | dependencies = [ 469 | "base64 0.11.0", 470 | "blake2b_simd", 471 | "constant_time_eq", 472 | "crossbeam-utils", 473 | ] 474 | 475 | [[package]] 476 | name = "rustls" 477 | version = "0.16.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" 480 | dependencies = [ 481 | "base64 0.10.1", 482 | "log", 483 | "ring", 484 | "sct", 485 | "webpki", 486 | ] 487 | 488 | [[package]] 489 | name = "rustversion" 490 | version = "1.0.2" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" 493 | dependencies = [ 494 | "proc-macro2", 495 | "quote", 496 | "syn", 497 | ] 498 | 499 | [[package]] 500 | name = "ryu" 501 | version = "1.0.2" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 504 | 505 | [[package]] 506 | name = "scrawl" 507 | version = "1.1.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "8fbfda0f5cf9c3c8a8058bc9c48b8c1bcdaec976086ad389ae0df6e98b302dd2" 510 | 511 | [[package]] 512 | name = "sct" 513 | version = "0.6.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" 516 | dependencies = [ 517 | "ring", 518 | "untrusted", 519 | ] 520 | 521 | [[package]] 522 | name = "serde" 523 | version = "1.0.104" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 526 | dependencies = [ 527 | "serde_derive", 528 | ] 529 | 530 | [[package]] 531 | name = "serde_derive" 532 | version = "1.0.104" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 535 | dependencies = [ 536 | "proc-macro2", 537 | "quote", 538 | "syn", 539 | ] 540 | 541 | [[package]] 542 | name = "serde_json" 543 | version = "1.0.46" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "21b01d7f0288608a01dca632cf1df859df6fd6ffa885300fc275ce2ba6221953" 546 | dependencies = [ 547 | "itoa", 548 | "ryu", 549 | "serde", 550 | ] 551 | 552 | [[package]] 553 | name = "smallvec" 554 | version = "1.2.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 557 | 558 | [[package]] 559 | name = "sourcefile" 560 | version = "0.1.4" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" 563 | 564 | [[package]] 565 | name = "spin" 566 | version = "0.5.2" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 569 | 570 | [[package]] 571 | name = "strsim" 572 | version = "0.8.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 575 | 576 | [[package]] 577 | name = "structopt" 578 | version = "0.3.9" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98" 581 | dependencies = [ 582 | "clap", 583 | "lazy_static", 584 | "structopt-derive", 585 | ] 586 | 587 | [[package]] 588 | name = "structopt-derive" 589 | version = "0.4.2" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64" 592 | dependencies = [ 593 | "heck", 594 | "proc-macro-error", 595 | "proc-macro2", 596 | "quote", 597 | "syn", 598 | ] 599 | 600 | [[package]] 601 | name = "subtle" 602 | version = "1.0.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" 605 | 606 | [[package]] 607 | name = "syn" 608 | version = "1.0.14" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" 611 | dependencies = [ 612 | "proc-macro2", 613 | "quote", 614 | "unicode-xid", 615 | ] 616 | 617 | [[package]] 618 | name = "syn-mid" 619 | version = "0.5.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 622 | dependencies = [ 623 | "proc-macro2", 624 | "quote", 625 | "syn", 626 | ] 627 | 628 | [[package]] 629 | name = "textwrap" 630 | version = "0.11.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 633 | dependencies = [ 634 | "unicode-width", 635 | ] 636 | 637 | [[package]] 638 | name = "thread_local" 639 | version = "1.0.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 642 | dependencies = [ 643 | "lazy_static", 644 | ] 645 | 646 | [[package]] 647 | name = "time" 648 | version = "0.1.42" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 651 | dependencies = [ 652 | "libc", 653 | "redox_syscall", 654 | "winapi", 655 | ] 656 | 657 | [[package]] 658 | name = "typenum" 659 | version = "1.11.2" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" 662 | 663 | [[package]] 664 | name = "unicode-bidi" 665 | version = "0.3.4" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 668 | dependencies = [ 669 | "matches", 670 | ] 671 | 672 | [[package]] 673 | name = "unicode-normalization" 674 | version = "0.1.12" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 677 | dependencies = [ 678 | "smallvec", 679 | ] 680 | 681 | [[package]] 682 | name = "unicode-segmentation" 683 | version = "1.6.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 686 | 687 | [[package]] 688 | name = "unicode-width" 689 | version = "0.1.7" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 692 | 693 | [[package]] 694 | name = "unicode-xid" 695 | version = "0.2.0" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 698 | 699 | [[package]] 700 | name = "untrusted" 701 | version = "0.7.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" 704 | 705 | [[package]] 706 | name = "ureq" 707 | version = "0.11.4" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "801125e6d1ba6864cf3a5a92cfb2f0b0a3ee73e40602a0cd206ad2f3c040aa96" 710 | dependencies = [ 711 | "base64 0.11.0", 712 | "chunked_transfer", 713 | "cookie", 714 | "lazy_static", 715 | "qstring", 716 | "rustls", 717 | "url 2.1.1", 718 | "webpki", 719 | "webpki-roots", 720 | ] 721 | 722 | [[package]] 723 | name = "url" 724 | version = "1.7.2" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" 727 | dependencies = [ 728 | "idna 0.1.5", 729 | "matches", 730 | "percent-encoding 1.0.1", 731 | ] 732 | 733 | [[package]] 734 | name = "url" 735 | version = "2.1.1" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 738 | dependencies = [ 739 | "idna 0.2.0", 740 | "matches", 741 | "percent-encoding 2.1.0", 742 | ] 743 | 744 | [[package]] 745 | name = "vec_map" 746 | version = "0.8.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 749 | 750 | [[package]] 751 | name = "version_check" 752 | version = "0.1.5" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 755 | 756 | [[package]] 757 | name = "wasi" 758 | version = "0.9.0+wasi-snapshot-preview1" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 761 | 762 | [[package]] 763 | name = "wasm-bindgen" 764 | version = "0.2.58" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" 767 | dependencies = [ 768 | "cfg-if", 769 | "wasm-bindgen-macro", 770 | ] 771 | 772 | [[package]] 773 | name = "wasm-bindgen-backend" 774 | version = "0.2.58" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" 777 | dependencies = [ 778 | "bumpalo", 779 | "lazy_static", 780 | "log", 781 | "proc-macro2", 782 | "quote", 783 | "syn", 784 | "wasm-bindgen-shared", 785 | ] 786 | 787 | [[package]] 788 | name = "wasm-bindgen-macro" 789 | version = "0.2.58" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" 792 | dependencies = [ 793 | "quote", 794 | "wasm-bindgen-macro-support", 795 | ] 796 | 797 | [[package]] 798 | name = "wasm-bindgen-macro-support" 799 | version = "0.2.58" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" 802 | dependencies = [ 803 | "proc-macro2", 804 | "quote", 805 | "syn", 806 | "wasm-bindgen-backend", 807 | "wasm-bindgen-shared", 808 | ] 809 | 810 | [[package]] 811 | name = "wasm-bindgen-shared" 812 | version = "0.2.58" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" 815 | 816 | [[package]] 817 | name = "wasm-bindgen-webidl" 818 | version = "0.2.58" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" 821 | dependencies = [ 822 | "anyhow", 823 | "heck", 824 | "log", 825 | "proc-macro2", 826 | "quote", 827 | "syn", 828 | "wasm-bindgen-backend", 829 | "weedle", 830 | ] 831 | 832 | [[package]] 833 | name = "web-sys" 834 | version = "0.3.35" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" 837 | dependencies = [ 838 | "anyhow", 839 | "js-sys", 840 | "sourcefile", 841 | "wasm-bindgen", 842 | "wasm-bindgen-webidl", 843 | ] 844 | 845 | [[package]] 846 | name = "webpki" 847 | version = "0.21.2" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef" 850 | dependencies = [ 851 | "ring", 852 | "untrusted", 853 | ] 854 | 855 | [[package]] 856 | name = "webpki-roots" 857 | version = "0.18.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4" 860 | dependencies = [ 861 | "webpki", 862 | ] 863 | 864 | [[package]] 865 | name = "weedle" 866 | version = "0.10.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" 869 | dependencies = [ 870 | "nom", 871 | ] 872 | 873 | [[package]] 874 | name = "winapi" 875 | version = "0.3.8" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 878 | dependencies = [ 879 | "winapi-i686-pc-windows-gnu", 880 | "winapi-x86_64-pc-windows-gnu", 881 | ] 882 | 883 | [[package]] 884 | name = "winapi-i686-pc-windows-gnu" 885 | version = "0.4.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 888 | 889 | [[package]] 890 | name = "winapi-x86_64-pc-windows-gnu" 891 | version = "0.4.0" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 894 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "librarian" 3 | version = "0.1.0" 4 | authors = ["Christian Poveda "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [[bin]] 10 | name = "lbr" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | blake3 = "0.1.4" 15 | structopt = "0.3" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | hex = "0.4.0" 19 | open = "1.3.3" 20 | dirs = "2.0.2" 21 | anyhow = "1.0" 22 | ureq = "0.11.4" 23 | fuzzy-matcher = "0.3.3" 24 | scrawl = "1.1.0" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Librarian 2 | 3 | **This repo is now archived**. The spiritual successor to this project can be found here: https://github.com/pvdrz/domain. 4 | 5 | ## Description 6 | 7 | This is a tiny command-line application to manage your digital library. 8 | 9 | ## Why? 10 | 11 | I never found a satisfying way of storing and searching my digital library. 12 | Using the documents' metadata sounded like a good idea, but then I realized that 13 | editing the metadata of several file formats would require having more 14 | than one application to do it. Then I wrote this small app to handle it instead. 15 | 16 | ## How? 17 | 18 | - Librarian keeps an index with all your documents' metadata at 19 | `~/.library/index.json`. 20 | 21 | - When you store a document using `lbr add`, the file is copied to the 22 | `~/.library` folder and the metadata is added to the index file. Each 23 | document is indexed using the hash of the file. Additionally you can provide 24 | the ISBN of the document if it has one and Librarian will try to recover its 25 | metadata from Open Library. 26 | 27 | - Then you can search in your library using `lbr find`. 28 | 29 | - Once you found the document, you can open it using `lbr open` with the 30 | document's hash. This is equivalent to using `open` or `xdg-open`. 31 | 32 | - If you need to change the information of a document, use `lbr edit` to open 33 | the information as a JSON using your default text editor. 34 | 35 | For more help, run `lbr help`. 36 | 37 | ## Installation 38 | 39 | Clone this repository and run `cargo install --path `. 40 | 41 | ## Stability 42 | 43 | The API of Librarian will be susceptible to change until it reaches version 44 | `1.0.0`. However, I won't be changing the format of the index unless it is in a 45 | backwards-compatible way. So if you want to play with it and give it a try, you 46 | can rest assured that the next update of Librarian won't mess up your index. 47 | 48 | ## Contributions 49 | 50 | Please do! I'm more than happy to receive suggestions, questions, issues, PRs 51 | and so on. 52 | -------------------------------------------------------------------------------- /src/book.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | use std::collections::BTreeSet; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct Book { 8 | pub title: String, 9 | pub authors: BTreeSet, 10 | pub extension: String, 11 | pub keywords: BTreeSet, 12 | } 13 | 14 | impl Book { 15 | pub fn set_info_from_api(&mut self, isbn: &str) -> Result<()> { 16 | let isbn = format!( 17 | "ISBN:{}", 18 | isbn.chars() 19 | .filter(|&c| c.is_numeric() || c == 'X') 20 | .collect::() 21 | ); 22 | 23 | let resp = ureq::get("https://openlibrary.org/api/books") 24 | .query("bibkeys", &isbn) 25 | .query("jscmd", "data") 26 | .query("format", "json") 27 | .call() 28 | .into_reader(); 29 | 30 | let resp = serde_json::from_reader::<_, serde_json::Value>(resp) 31 | .context("Could not deserialize document information from the API")? 32 | .get(&isbn) 33 | .ok_or_else(|| anyhow!("Document with {} not found at Open Library", &isbn))? 34 | .clone(); 35 | 36 | self.title = resp 37 | .get("title") 38 | .map(|value| { 39 | value 40 | .as_str() 41 | .expect("Malformed response from API, title is not a string") 42 | .to_owned() 43 | }) 44 | .unwrap_or_else(|| String::new()); 45 | 46 | self.authors = resp 47 | .get("authors") 48 | .map(|value| { 49 | value 50 | .as_array() 51 | .expect("Malformed response from API, authors are not an array") 52 | .into_iter() 53 | .map(|j| { 54 | j.get("name") 55 | .expect("Malformed response from API: author does not have a name") 56 | .as_str() 57 | .expect("Malformed response from API: author's name is not a string") 58 | .to_owned() 59 | }) 60 | .collect() 61 | }) 62 | .unwrap_or_else(|| BTreeSet::new()); 63 | 64 | Ok(()) 65 | } 66 | 67 | pub fn edit(&mut self) -> Result { 68 | let text = 69 | serde_json::to_string_pretty(&self).context("cannot serialize document to JSON")?; 70 | let text = scrawl::with(&text)?; 71 | *self = serde_json::from_str(&text).context("cannot deserialize document from JSON")?; 72 | Ok(text) 73 | } 74 | } 75 | #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] 76 | pub struct BookHash([u8; 32]); 77 | 78 | impl From<[u8; 32]> for BookHash { 79 | fn from(bytes: [u8; 32]) -> Self { 80 | BookHash(bytes) 81 | } 82 | } 83 | 84 | impl From for BookHash { 85 | fn from(hash: blake3::Hash) -> Self { 86 | BookHash(hash.into()) 87 | } 88 | } 89 | 90 | impl From for [u8; 32] { 91 | fn from(hash: BookHash) -> Self { 92 | hash.0 93 | } 94 | } 95 | 96 | impl Serialize for BookHash { 97 | fn serialize(&self, serializer: S) -> Result 98 | where 99 | S: Serializer, 100 | { 101 | let s = hex::encode(&self.0); 102 | serializer.serialize_str(&s) 103 | } 104 | } 105 | 106 | impl<'de> Deserialize<'de> for BookHash { 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: Deserializer<'de>, 110 | { 111 | use serde::de::Error; 112 | let s: String = Deserialize::deserialize(deserializer)?; 113 | let mut bytes = [0; 32]; 114 | hex::decode_to_slice(s, &mut bytes).map_err(D::Error::custom)?; 115 | Ok(BookHash(bytes)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/cmd.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt, Debug)] 4 | #[structopt( 5 | about = "\"Each one of these souls is finite and precious. And I'm close... Close to saving them all.\"" 6 | )] 7 | pub enum Command { 8 | #[structopt(about = "Adds a new document into the library")] 9 | Add { 10 | #[structopt(help = "Path to the document to be stored")] 11 | file: String, 12 | #[structopt( 13 | short, 14 | long, 15 | help = "Get document information from Open Library using the ISBN" 16 | )] 17 | isbn: Option, 18 | }, 19 | #[structopt(about = "Finds a document in the library")] 20 | Find { 21 | #[structopt(help = "Pattern to search in the document information")] 22 | pattern: String, 23 | #[structopt(short, long, help = "Opens the first match of the search")] 24 | open: bool, 25 | }, 26 | #[structopt(about = "List all the documents in the library")] 27 | List, 28 | #[structopt(about = "Edits the info of a specific document using the default editor")] 29 | Edit { 30 | #[structopt(about = "Hash of the document to be updated")] 31 | hash: String, 32 | }, 33 | #[structopt(about = "Opens a document")] 34 | Open { 35 | #[structopt(help = "Hash of the document to be opened")] 36 | hash: String, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /src/library.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, ensure, Context, Result}; 2 | use fuzzy_matcher::skim::SkimMatcherV2; 3 | use fuzzy_matcher::FuzzyMatcher; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use std::collections::{BTreeMap, BTreeSet}; 7 | use std::fs::{copy, read, File}; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use crate::book::{Book, BookHash}; 11 | use crate::cmd::Command; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct Library { 15 | books: BTreeMap, 16 | root: PathBuf, 17 | } 18 | 19 | impl Library { 20 | pub fn get_hash(&self, hash_str: &str) -> Result { 21 | let str_len = hash_str.len(); 22 | 23 | if str_len < 64 { 24 | use std::ops::Bound::Included; 25 | let mut bot_hash = [0; 32]; 26 | let mut top_hash = [255; 32]; 27 | 28 | let mut hash_len = (str_len) / 2; 29 | if str_len % 2 == 0 { 30 | hex::decode_to_slice(&hash_str, &mut top_hash[..hash_len]) 31 | } else { 32 | hash_len = (1 + str_len) / 2; 33 | let b = &mut top_hash[hash_len - 1]; 34 | *b = b.wrapping_sub(15); 35 | hex::decode_to_slice(&(hash_str.to_owned() + "f"), &mut top_hash[..hash_len]) 36 | } 37 | .context("Invalid hash")?; 38 | 39 | for i in 0..hash_len { 40 | bot_hash[i] += top_hash[i]; 41 | } 42 | 43 | let mut range = self.books.range(( 44 | Included(BookHash::from(bot_hash)), 45 | Included(BookHash::from(top_hash)), 46 | )); 47 | 48 | if let Some((&hash, _)) = range.next() { 49 | ensure!( 50 | range.next().is_none(), 51 | "Hash collision, please use a longer prefix" 52 | ); 53 | Ok(hash) 54 | } else { 55 | bail!("Hash not found") 56 | } 57 | } else if str_len == 64 { 58 | let mut hash = [0; 32]; 59 | hex::decode_to_slice(&hash_str, &mut hash).context("Invalid hash")?; 60 | let hash = BookHash::from(hash); 61 | ensure!(self.books.contains_key(&hash), "Hash not found"); 62 | Ok(hash) 63 | } else { 64 | bail!("Hash is longer than expected") 65 | } 66 | } 67 | 68 | pub fn with_root(root: PathBuf) -> Self { 69 | Library { 70 | books: BTreeMap::new(), 71 | root, 72 | } 73 | } 74 | 75 | pub fn from_file(path: &Path) -> Result { 76 | serde_json::from_reader(File::open(path).context("Could not open index file")?) 77 | .context("Could not deserialize index contents") 78 | } 79 | 80 | pub fn persist(&self, path: &Path) -> Result<()> { 81 | serde_json::to_writer( 82 | File::create(path).context("Could not create index file")?, 83 | self, 84 | ) 85 | .context("Could not serialize index as JSON") 86 | } 87 | 88 | pub fn run_command(&mut self, cmd: Command) -> Result<()> { 89 | match cmd { 90 | Command::Add { file, isbn } => self.add(file, isbn), 91 | Command::Find { pattern, open } => self.find(pattern, open), 92 | Command::Open { hash } => self.open(hash), 93 | Command::Edit { hash } => self.edit(hash), 94 | Command::List => self.list(), 95 | } 96 | } 97 | 98 | fn add(&mut self, file: String, isbn: Option) -> Result<()> { 99 | let file = PathBuf::from(file); 100 | 101 | let hash: BookHash = blake3::hash(&read(&file).context("Could not read file")?).into(); 102 | 103 | if self.books.contains_key(&hash) { 104 | bail!( 105 | "Document with hash {} already exists", 106 | serde_json::to_string(&hash).unwrap() 107 | ); 108 | } 109 | 110 | let extension = file 111 | .extension() 112 | .ok_or_else(|| anyhow!("File {:?} has no extension", file))? 113 | .to_str() 114 | .ok_or_else(|| anyhow!("Extension is not valid unicode"))? 115 | .to_lowercase(); 116 | 117 | let path = self.path(hash, &extension); 118 | 119 | let mut book = Book { 120 | title: String::new(), 121 | authors: BTreeSet::new(), 122 | keywords: BTreeSet::new(), 123 | extension, 124 | }; 125 | 126 | if let Some(isbn) = isbn { 127 | book.set_info_from_api(&isbn)?; 128 | } 129 | 130 | let book_json = book.edit()?; 131 | 132 | assert!(self.books.insert(hash, book).is_none(),); 133 | 134 | copy(file, path).context("Could not copy file to library")?; 135 | 136 | println!( 137 | "Added document: {}\n with hash: {}", 138 | book_json, 139 | serde_json::to_string(&hash).expect("Bug: Could not serialize document hash") 140 | ); 141 | 142 | Ok(()) 143 | } 144 | 145 | fn list(&self) -> Result<()> { 146 | show_json(&self.books); 147 | Ok(()) 148 | } 149 | 150 | fn find(&self, pattern: String, open: bool) -> Result<()> { 151 | let matcher = SkimMatcherV2::default(); 152 | let mut scores = BTreeMap::new(); 153 | let mut books: Vec<_> = self 154 | .books 155 | .iter() 156 | .filter_map(|(hash, book)| { 157 | let mut score = matcher.fuzzy_match(&book.title, &pattern).unwrap_or(0); 158 | for author in &book.authors { 159 | score += matcher.fuzzy_match(author, &pattern).unwrap_or(0); 160 | } 161 | for keyword in &book.keywords { 162 | score += matcher.fuzzy_match(keyword, &pattern).unwrap_or(0); 163 | } 164 | if score == 0 { 165 | return None; 166 | } 167 | scores.insert(hash, -(score as isize)); 168 | Some((hash, book)) 169 | }) 170 | .collect(); 171 | 172 | books.sort_by_key(|(hash, _)| scores[hash]); 173 | 174 | if open { 175 | if let Some(&(&hash, book)) = books.get(0) { 176 | println!("Opening book {}", serde_json::to_string(&hash).unwrap()); 177 | open::that(self.path(hash, &book.extension)).context("Could not open document")?; 178 | } else { 179 | bail!("Search did not return any results"); 180 | } 181 | } else { 182 | show_json(&books); 183 | } 184 | 185 | Ok(()) 186 | } 187 | 188 | fn open(&self, hash_str: String) -> Result<()> { 189 | let hash = self.get_hash(&hash_str)?; 190 | let book = self 191 | .books 192 | .get(&hash) 193 | .ok_or_else(|| anyhow!("Document with hash {} not found", hash_str))?; 194 | open::that(self.path(hash, &book.extension)).context("Could not open document")?; 195 | Ok(()) 196 | } 197 | 198 | fn edit(&mut self, hash_str: String) -> Result<()> { 199 | let hash = self.get_hash(&hash_str)?; 200 | 201 | let book = self 202 | .books 203 | .get_mut(&hash) 204 | .ok_or_else(|| anyhow!("Document with hash {} not found", hash_str))?; 205 | 206 | let book_json = book.edit()?; 207 | 208 | println!("Updated document {}: {}", hash_str, book_json); 209 | 210 | Ok(()) 211 | } 212 | 213 | fn path(&self, hash: BookHash, extension: &str) -> PathBuf { 214 | let mut path = hex::encode(&<[u8; 32]>::from(hash)); 215 | path += "."; 216 | path += extension; 217 | self.root.join(path) 218 | } 219 | } 220 | 221 | fn show_json(s: &S) { 222 | println!( 223 | "{}", 224 | serde_json::to_string_pretty(&s) 225 | .expect("Bug: Could not serialize list of documents as JSON") 226 | ); 227 | } 228 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod book; 2 | mod cmd; 3 | mod library; 4 | 5 | use anyhow::{anyhow, Context, Result}; 6 | use structopt::StructOpt; 7 | 8 | use library::Library; 9 | 10 | fn main() -> Result<()> { 11 | let library_path = dirs::home_dir() 12 | .ok_or_else(|| anyhow!("Home directory not found"))? 13 | .join(".library"); 14 | 15 | let index_path = library_path.join("index.json"); 16 | 17 | let mut lib = if index_path.exists() { 18 | Library::from_file(&index_path)? 19 | } else { 20 | if !library_path.exists() { 21 | std::fs::create_dir(&library_path).with_context(|| { 22 | format!("Could not create library directory at {:?}", library_path) 23 | })?; 24 | } 25 | Library::with_root(library_path) 26 | }; 27 | 28 | let cmd = cmd::Command::from_args(); 29 | lib.run_command(cmd)?; 30 | 31 | lib.persist(&index_path)?; 32 | Ok(()) 33 | } 34 | --------------------------------------------------------------------------------