Compare commits
844 Commits
Author | SHA1 | Date |
---|---|---|
Ozzie Isaacs | d3118c0aa9 | 4 days ago |
mapi68 | 04370944b9 | 6 days ago |
Ozzie Isaacs | ab11919c0b | 4 weeks ago |
Ozzie Isaacs | 58c269881f | 4 weeks ago |
Ozzie Isaacs | 6760d6971c | 4 weeks ago |
Kreeblah | ad05534ed2 | 4 weeks ago |
Ozzie Isaacs | ee451fb236 | 4 weeks ago |
Ozzie Isaacs | ab13fcf60c | 4 weeks ago |
Ozzie Isaacs | 6f60ec7b99 | 4 weeks ago |
Ozzie Isaacs | 894fd9d30a | 4 weeks ago |
Ozzie Isaacs | 2b1efdb50e | 4 weeks ago |
Ozzie Isaacs | fc9a9cb9ac | 4 weeks ago |
Ozzie Isaacs | e99be72ff7 | 4 weeks ago |
Ozzie Isaacs | f1ceff2b52 | 4 weeks ago |
Ozzie Isaacs | 7e85894b3a | 4 weeks ago |
Ozzie Isaacs | 5c49c8cdd7 | 4 weeks ago |
Ozzie Isaacs | c8c3b3cba3 | 4 weeks ago |
Ozzie Isaacs | 4911843146 | 4 weeks ago |
Ozzie Isaacs | 2c37546598 | 4 weeks ago |
Ozzie Isaacs | 25a875b628 | 4 weeks ago |
Ozzie Isaacs | 506f0a33cf | 4 weeks ago |
Ozzie Isaacs | 921caf6716 | 4 weeks ago |
Ozzie Isaacs | 60ed1904f5 | 4 weeks ago |
Ozzie Isaacs | cb62d36e44 | 1 month ago |
Ozzie Isaacs | 737d758362 | 1 month ago |
Ozzie Isaacs | 8e27912ff5 | 1 month ago |
Ozzie Isaacs | 3a603cec22 | 1 month ago |
eggy | b1d7badef4 | 1 month ago |
Ozzie Isaacs | e591211b57 | 1 month ago |
Ozzie Isaacs | a305c35de4 | 1 month ago |
growfrow | 51d306b11d | 2 months ago |
mapi68 | abb418fe86 | 2 months ago |
Ozzie Isaacs | 0925f34557 | 2 months ago |
Ozzie Isaacs | 15952a764c | 2 months ago |
Ozzie Isaacs | fcc95bd895 | 3 months ago |
Ghighi Eftimie | 964e7de920 | 3 months ago |
Ozzie Isaacs | 14b578dd3a | 3 months ago |
Ozzie Isaacs | becb84a73d | 3 months ago |
Ozzie Isaacs | c901ccbb01 | 3 months ago |
Ozzie Isaacs | f987fb0aba | 3 months ago |
Ozzie Isaacs | c30460d76b | 3 months ago |
Ozzie Isaacs | 97380b4b3f | 3 months ago |
Ozzie Isaacs | 4fbd064b85 | 3 months ago |
Ozzie Isaacs | abbd9a5888 | 3 months ago |
Ozzie Isaacs | e860b4e097 | 3 months ago |
Ozzie Isaacs | 23a8a4657d | 3 months ago |
Ozzie Isaacs | b38a1b2298 | 3 months ago |
Ozzie Isaacs | 0ebfba8d05 | 3 months ago |
Ozzie Isaacs | 990ad8d72d | 3 months ago |
Ozzie Isaacs | c3fc125501 | 3 months ago |
Ozzie Isaacs | 3c4ed0de1a | 3 months ago |
Ozzie Isaacs | 117c92233d | 3 months ago |
Ozzie Isaacs | 2ba14acf4f | 3 months ago |
Ozzie Isaacs | 80a2d07009 | 3 months ago |
Ozzie Isaacs | ff9e1ed7c8 | 3 months ago |
Ozzie Isaacs | 8e5bee5352 | 4 months ago |
Ozzie Isaacs | d659430116 | 4 months ago |
Ozzie Isaacs | 859dac462b | 4 months ago |
Ozzie Isaacs | 2bea4dbd06 | 4 months ago |
Ozzie Isaacs | 0180b4b6b5 | 4 months ago |
Ozzie Isaacs | 2bfb02c448 | 4 months ago |
Ozzie Isaacs | 4864254e37 | 4 months ago |
Ozzie Isaacs | 09dce28a0e | 4 months ago |
Ozzie Isaacs | e55d09d8bb | 4 months ago |
Ozzie Isaacs | 92c162b2fd | 4 months ago |
Ozzie Isaacs | 57fb5001e2 | 4 months ago |
Ozzie Isaacs | 64e5314148 | 4 months ago |
Ozzie Isaacs | 873602a5c9 | 4 months ago |
Ozzie Isaacs | 09e966e18a | 4 months ago |
Ozzie Isaacs | f7718cae0c | 4 months ago |
Ozzie Isaacs | 90e728516c | 4 months ago |
Ozzie Isaacs | 7c04b68c88 | 4 months ago |
Ozzie Isaacs | 8549689a0f | 4 months ago |
Ozzie Isaacs | d8f5c17518 | 4 months ago |
mapi68 | 05367d2df5 | 4 months ago |
Webysther Sperandio | eb6fbfc90c | 5 months ago |
Ozzie Isaacs | c2267b6902 | 5 months ago |
Ozzie Isaacs | 0e5520a261 | 5 months ago |
Ozzie Isaacs | 6f5e9f167e | 5 months ago |
Ozzie Isaacs | ce83fb6816 | 5 months ago |
Ozzie Isaacs | fbfb7adef6 | 5 months ago |
Ozzie Isaacs | cc52ad5d27 | 5 months ago |
Ozzie Isaacs | 706b9c4013 | 5 months ago |
Ozzie Isaacs | 6972c1b841 | 5 months ago |
Ozzie Isaacs | b9c329535d | 5 months ago |
Ozzie Isaacs | 8fdf7a94ab | 5 months ago |
Ozzie Isaacs | 31a344b410 | 5 months ago |
Ozzie Isaacs | 3814fbf08f | 5 months ago |
Ozzie Isaacs | ffc13a5565 | 5 months ago |
Ozzie Isaacs | 74c61d9685 | 5 months ago |
Ozzie Isaacs | b8031cd53f | 5 months ago |
Ozzie Isaacs | 898e76fc37 | 5 months ago |
Ozzie Isaacs | af71a1a2ed | 5 months ago |
Ozzie Isaacs | e0327db08f | 5 months ago |
Ozzie Isaacs | bf2ac97c47 | 5 months ago |
Ozzie Isaacs | 902fa254b0 | 5 months ago |
Ozzie Isaacs | f0cc93abd3 | 5 months ago |
Ozzie Isaacs | 977f07364b | 5 months ago |
Ozzie Isaacs | 00acd745f4 | 5 months ago |
Ozzie Isaacs | d272f43424 | 5 months ago |
Whatever Cloud | 7a8d8375d0 | 5 months ago |
Johannes H | 3aa75ef4a7 | 5 months ago |
Ozzie Isaacs | 25fb8d934f | 6 months ago |
GONCALVES Nelson (T0025615) | f08c8faaff | 6 months ago |
Michiel Cornelissen | bc0ebdb78d | 6 months ago |
Ozzie Isaacs | 2a4b3cb7af | 6 months ago |
Ozzie Isaacs | 4401cf66d1 | 6 months ago |
Ozzie Isaacs | d353c9b6d3 | 6 months ago |
Ozzie Isaacs | 0aba96c032 | 6 months ago |
Ozzie Isaacs | c60b7e9192 | 6 months ago |
Ozzie Isaacs | 23033255b8 | 6 months ago |
Ozzie Isaacs | 31c8909dea | 6 months ago |
Ozzie Isaacs | 9ef89dbcc3 | 6 months ago |
Ozzie Isaacs | 1086296d1d | 6 months ago |
Ozzie Isaacs | d341faf204 | 6 months ago |
Ozzie Isaacs | 2334e8f9c9 | 6 months ago |
Ozzie Isaacs | 90ad570578 | 6 months ago |
Ozzie Isaacs | fd90d6e375 | 6 months ago |
Ozzie Isaacs | 7fbbb85f47 | 6 months ago |
Ozzie Isaacs | 52c7557878 | 6 months ago |
Ozzie Isaacs | 794cd354ca | 6 months ago |
Ghighi Eftimie | 389e3f09f5 | 6 months ago |
Ghighi Eftimie | 285979b68d | 6 months ago |
Ozzie Isaacs | 3a012c900e | 6 months ago |
Ozzie Isaacs | ec45de3212 | 6 months ago |
Ozzie Isaacs | f644a2a136 | 6 months ago |
Russell | 01108aac42 | 6 months ago |
Russell Troxel | 400c745692 | 6 months ago |
ye | 9841a4d068 | 6 months ago |
Ozzie Isaacs | 7fd1d10fca | 7 months ago |
Ozzie Isaacs | 4f6bbfa8b8 | 7 months ago |
Ozzie Isaacs | cf6810db87 | 7 months ago |
Ozzie Isaacs | 5afff2231e | 7 months ago |
Ozzie Isaacs | d611582b78 | 7 months ago |
Ozzie Isaacs | 3bbd8ee27e | 7 months ago |
Ozzie Isaacs | f78e0ff938 | 7 months ago |
Ozzie Isaacs | bd71391bfb | 7 months ago |
Ozzie Isaacs | 20b2936cc1 | 7 months ago |
Ozzie Isaacs | 19825a635a | 7 months ago |
Ozzie Isaacs | 0d611d35de | 7 months ago |
Ozzie Isaacs | effd026fe2 | 7 months ago |
Ozzie Isaacs | d68e57c4fc | 7 months ago |
Ozzie Isaacs | 184ce23351 | 7 months ago |
Ozzie Isaacs | 2fbc3da451 | 7 months ago |
Ozzie Isaacs | fad6550ff1 | 7 months ago |
Ozzie Isaacs | b7aaa0f24d | 7 months ago |
Ozzie Isaacs | 5040bb762c | 7 months ago |
Ozzie Isaacs | 55deca1ec8 | 7 months ago |
Ozzie Isaacs | 40a16f4717 | 7 months ago |
Ozzie Isaacs | d26e60724a | 7 months ago |
Ozzie Isaacs | d877fa1c68 | 7 months ago |
Ozzie Isaacs | d55bafdfa9 | 7 months ago |
Ozzie Isaacs | a2a431802a | 7 months ago |
Ozzie Isaacs | b2e4907165 | 7 months ago |
Ozzie Isaacs | 6c2e40f544 | 7 months ago |
Ozzie Isaacs | 5e3d0ec2ad | 7 months ago |
Ozzie Isaacs | c550d6c90d | 7 months ago |
bacpd | 3b1d0b4013 | 8 months ago |
Ozzie Isaacs | 3d07efbb4f | 8 months ago |
mapi68 | c0ae5bb381 | 8 months ago |
Ozzie Isaacs | 6e755a26f9 | 8 months ago |
Ozzie Isaacs | c45188beb2 | 8 months ago |
Ozzie Isaacs | 0736c53d7b | 8 months ago |
Ozzie Isaacs | f0f8011d24 | 8 months ago |
Ozzie Isaacs | 65f3ecb924 | 8 months ago |
Ozzie Isaacs | 87b3999ec8 | 8 months ago |
Ozzie Isaacs | e32312b54a | 8 months ago |
Ozzie Isaacs | d7ea569e5d | 8 months ago |
Ozzie Isaacs | 96958e7266 | 8 months ago |
Ozzie Isaacs | 2c339ed10c | 8 months ago |
Ozzie Isaacs | dc2c30f508 | 8 months ago |
Ozzie Isaacs | 0c43d80163 | 8 months ago |
Ozzie Isaacs | df71a86f94 | 8 months ago |
Ozzie Isaacs | 7ed56b4397 | 8 months ago |
Ozzie Isaacs | 5ceb2b6d83 | 8 months ago |
Ozzie Isaacs | 8abea1ddd0 | 8 months ago |
Ozzie Isaacs | 11816d3405 | 8 months ago |
Ozzie Isaacs | 198bff928f | 8 months ago |
lawsssscat | cac200ba61 | 9 months ago |
David K | 8cc36ab081 | 9 months ago |
databoy2k | b3d1558df8 | 9 months ago |
byword77 | a045b6f467 | 9 months ago |
Ozzie Isaacs | 7a961c9011 | 9 months ago |
Ozzie Isaacs | 444ac181f8 | 9 months ago |
Ozzie Isaacs | 4bbcec21e4 | 9 months ago |
Ozzie Isaacs | 5509d4598b | 9 months ago |
Ozzie Isaacs | fab35e69ec | 9 months ago |
Ozzie Isaacs | 4f0f5b1495 | 9 months ago |
Ozzie Isaacs | cfa309f0d1 | 9 months ago |
Ozzie Isaacs | 885d914f18 | 9 months ago |
Ozzie Isaacs | b580f418f7 | 9 months ago |
Ozzie Isaacs | 6a14e2cf68 | 10 months ago |
Ozzie Isaacs | 8535bb5821 | 10 months ago |
Ozzie Isaacs | b2a26a421c | 10 months ago |
Ozzie Isaacs | 7aea7fc0bb | 10 months ago |
Ozzie Isaacs | 52172044e6 | 10 months ago |
Ozzie Isaacs | 9b99427c84 | 10 months ago |
Ozzie Isaacs | 0499e578cd | 10 months ago |
Ozzie Isaacs | b3a85ffcbb | 10 months ago |
PhracturedBlue | 074e611705 | 10 months ago |
Ozzie Isaacs | a1899bf582 | 10 months ago |
Ozzie Isaacs | f7ff3e7cba | 10 months ago |
Ozzie Isaacs | 3a08b91ffa | 10 months ago |
Ozzie Isaacs | d253804a50 | 10 months ago |
Ozzie Isaacs | ba0e5399d6 | 10 months ago |
Ozzie Isaacs | 7818c4a7b0 | 10 months ago |
Ozzie Isaacs | 3f6a12898b | 10 months ago |
Ozzie Isaacs | caf69669cb | 10 months ago |
Ozzie Isaacs | de59181be7 | 10 months ago |
Ozzie Isaacs | 966c9236b9 | 10 months ago |
Ozzie Isaacs | 34c6010ad0 | 10 months ago |
Ozzie Isaacs | 60e904967b | 10 months ago |
Ozzie Isaacs | 3efcbcc679 | 10 months ago |
Ozzie Isaacs | 7bb4bc934c | 10 months ago |
Ozzie Isaacs | dcb8a0f77b | 10 months ago |
Ozzie Isaacs | 2f12b2e315 | 10 months ago |
Ozzie Isaacs | 279f0569e4 | 10 months ago |
Ozzie Isaacs | 6723369d65 | 10 months ago |
Ozzie Isaacs | 4b93ac034f | 10 months ago |
Ozzie Isaacs | fda62dde1d | 10 months ago |
Ozzie Isaacs | df74fdb4d1 | 10 months ago |
Ozzie Isaacs | cce538d5a7 | 11 months ago |
Ozzie Isaacs | e8b0051b31 | 11 months ago |
Ozzie Isaacs | fe55958ecc | 11 months ago |
Ozzie Isaacs | 7b321d63c1 | 11 months ago |
Ozzie Isaacs | 986eaf9f02 | 11 months ago |
Ozzie Isaacs | caf8ed77d7 | 11 months ago |
Ghighi Eftimie | ee5cfa1f36 | 11 months ago |
Horus68 | 5eef476135 | 11 months ago |
Horus68 | b5e4a88357 | 11 months ago |
Horus68 | 256f4bb428 | 11 months ago |
Horus68 | a4d45512ee | 11 months ago |
Horus68 | 074687c330 | 11 months ago |
archont | 2f7b175dda | 11 months ago |
Ozzie Isaacs | a256bd5260 | 11 months ago |
Ozzie Isaacs | fdd1410b06 | 11 months ago |
Ozzie Isaacs | 3f5583017f | 11 months ago |
Ozzie Isaacs | 63b7d70f33 | 12 months ago |
Ozzie Isaacs | 500758050c | 12 months ago |
Ozzie Isaacs | 4b4c0daab0 | 12 months ago |
Ozzie Isaacs | 709a4e51ba | 12 months ago |
Ozzie Isaacs | eff0750d77 | 12 months ago |
Ozzie Isaacs | e63a04093c | 12 months ago |
Ozzie Isaacs | 07d97d18d0 | 12 months ago |
Ozzie Isaacs | d8f30983d5 | 12 months ago |
Ozzie Isaacs | 062efc4e78 | 12 months ago |
boosh | 4e6c9c2703 | 1 year ago |
Ozzie Isaacs | 4dc5885723 | 1 year ago |
quarz12 | 39638d3c9c | 1 year ago |
Ozzie Isaacs | 3ef34c8f15 | 1 year ago |
Ozzie Isaacs | 932abbf090 | 1 year ago |
Ozzie Isaacs | 860443079d | 1 year ago |
Ozzie Isaacs | bd4b7ffaba | 1 year ago |
Ozzie Isaacs | 33e35eeb52 | 1 year ago |
Ozzie Isaacs | ed09814460 | 1 year ago |
Ozzie Isaacs | e52eb74121 | 1 year ago |
Daniel | dc7fbce4f7 | 1 year ago |
Ozzie Isaacs | dad0fd5a1c | 1 year ago |
Ozzie Isaacs | 8a87c152b4 | 1 year ago |
Ozzie Isaacs | 16baa306c5 | 1 year ago |
Daniel | 2eb334fb3d | 1 year ago |
Ozzie Isaacs | cb7356a04d | 1 year ago |
Ozzie Isaacs | 63a561bf9b | 1 year ago |
Ozzie Isaacs | cc733454b2 | 1 year ago |
whilenot | 940544577a | 1 year ago |
xlivevil | 9e0fc320cb | 1 year ago |
xlivevil | bf3ca20fb2 | 1 year ago |
Ozzie Isaacs | c7e1736ade | 1 year ago |
Ozzie Isaacs | bc6a50550e | 1 year ago |
Ozzie Isaacs | fe4dc1bb8f | 1 year ago |
Ozzie Isaacs | f2369609e8 | 1 year ago |
Ozzie Isaacs | de4d6ec7df | 1 year ago |
mapi68 | 7754f4aa5d | 1 year ago |
Ozzie Isaacs | 524751ea51 | 1 year ago |
Ozzie Isaacs | 8111d0dd51 | 1 year ago |
Ozzie Isaacs | fad5929253 | 1 year ago |
Ozzie Isaacs | 9f28144779 | 1 year ago |
Ozzie Isaacs | 42fd6973a0 | 1 year ago |
driz | b2e20ff50c | 1 year ago |
Ozzie Isaacs | 6075b3dd1d | 1 year ago |
driz | 37871ea8cb | 1 year ago |
Ozzie Isaacs | e2785c3985 | 1 year ago |
Ozzie Isaacs | dba83a2900 | 1 year ago |
Ozzie Isaacs | 33c19b20f4 | 1 year ago |
Ozzie Isaacs | d2f39d3dce | 1 year ago |
Ozzie Isaacs | 1c8bc78b48 | 1 year ago |
Ozzie Isaacs | 6c6841f8b0 | 1 year ago |
Ozzie Isaacs | 592216588c | 1 year ago |
Wladimir Kirianov | f4db0f04d2 | 1 year ago |
Wladimir Kirianov | b16e3a6e2c | 1 year ago |
Ozzie Isaacs | 13c0d30a8f | 1 year ago |
Ozzie Isaacs | b9c942befc | 1 year ago |
Ozzie Isaacs | a68a0dd037 | 1 year ago |
Thomas de Ruiter | a952c36ab7 | 1 year ago |
Thomas de Ruiter | 5f0c7737fe | 1 year ago |
Ozzie Isaacs | 38484624e9 | 1 year ago |
Ozzie Isaacs | 1451a67912 | 1 year ago |
Ozzie Isaacs | a72f0a160b | 1 year ago |
Ozzie Isaacs | 253386b0a5 | 1 year ago |
Ozzie Isaacs | 6c8ffb3e7e | 1 year ago |
Ozzie Isaacs | 7d26e6fc85 | 1 year ago |
Ozzie Isaacs | 085a6b88a3 | 1 year ago |
Ozzie Isaacs | bde36e3cd4 | 1 year ago |
Ozzie Isaacs | 9646b6e2dd | 1 year ago |
Ozzie Isaacs | d35e781d41 | 1 year ago |
Ozzie Isaacs | 321db4d712 | 1 year ago |
Ozzie Isaacs | 2b9f920454 | 1 year ago |
Ozzie Isaacs | 45acd3febe | 1 year ago |
Ozzie Isaacs | cbd7ca2f3e | 1 year ago |
Ozzie Isaacs | ba7fee3918 | 1 year ago |
Ozzie Isaacs | 1210ccb43f | 1 year ago |
Jerry Vonau | 04f1f6493b | 1 year ago |
Ozzie Isaacs | 46d2d217ee | 1 year ago |
Ozzie Isaacs | e3fffa8a8f | 1 year ago |
Ozzie Isaacs | dfb49bfca9 | 1 year ago |
Ozzie Isaacs | 224777f5e3 | 1 year ago |
Ozzie Isaacs | 7ade4615a4 | 1 year ago |
Ozzie Isaacs | cbd679eb24 | 1 year ago |
Ozzie Isaacs | b277ed3359 | 1 year ago |
Ozzie Isaacs | fa95b07a95 | 1 year ago |
Ozzie Isaacs | 87bc8c6d96 | 1 year ago |
Ozzie Isaacs | db2bc6a2c2 | 1 year ago |
Ozzie Isaacs | cf850c6ed5 | 1 year ago |
Ozzie Isaacs | a6b54e398b | 1 year ago |
Ozzie Isaacs | 28eeb9eec3 | 1 year ago |
Ozzie Isaacs | 7d76f2ae33 | 1 year ago |
Ozzie Isaacs | 49e4f540c9 | 1 year ago |
Ozzie Isaacs | 64e9b13311 | 1 year ago |
Ozzie Isaacs | 3cf778b591 | 1 year ago |
Ozzie Isaacs | 942bcff5c4 | 1 year ago |
Ozzie Isaacs | 5c5db34a52 | 1 year ago |
Ozzie Isaacs | ae850172a3 | 1 year ago |
Ozzie Isaacs | 7ff4747f63 | 1 year ago |
Ozzie Isaacs | 76b0411c33 | 1 year ago |
Ozzie Isaacs | a414db0243 | 1 year ago |
Ozzie Isaacs | 162ac73bee | 1 year ago |
Ozzie Isaacs | fc31132f4e | 1 year ago |
Ozzie Isaacs | 856dce8add | 1 year ago |
Ozzie Isaacs | 3debe4aa4b | 1 year ago |
Ozzie Isaacs | b28a2cc58c | 1 year ago |
Ozzie Isaacs | 5fd0e4c046 | 1 year ago |
Ozzie Isaacs | 595f01e7a3 | 1 year ago |
Ozzie Isaacs | c79aa75f00 | 1 year ago |
Ozzie Isaacs | 0177a8bcca | 1 year ago |
Ozzie Isaacs | 38c601bb10 | 1 year ago |
Ozzie Isaacs | e7a6fe0bec | 1 year ago |
Ozzie Isaacs | 6119eb3681 | 1 year ago |
Ozzie Isaacs | 6b2ca9537d | 1 year ago |
Ozzie Isaacs | 3cb9a9b04a | 1 year ago |
Ozzie Isaacs | 3d8256b6a6 | 1 year ago |
Ozzie Isaacs | 660d1fb1ff | 1 year ago |
Ozzie Isaacs | fa3fe47059 | 1 year ago |
Ozzie Isaacs | 89bc72958e | 1 year ago |
Ozzie Isaacs | 73ea18b8ce | 1 year ago |
Ozzie Isaacs | 7ca07f06ce | 1 year ago |
Ozzie Isaacs | 8ee34bf428 | 1 year ago |
Ozzie Isaacs | 66d5b5a697 | 1 year ago |
Ozzie Isaacs | ce48e06c45 | 1 year ago |
Ozzie Isaacs | f4ecfe4aca | 1 year ago |
Ozzie Isaacs | dda20eb912 | 1 year ago |
GarcaMan | c4326c9495 | 1 year ago |
Ozzie Isaacs | 63a3edd429 | 1 year ago |
Ozzie Isaacs | 3b45234beb | 1 year ago |
Ozzie Isaacs | 8d0a699078 | 1 year ago |
Ozzie Isaacs | 5b5146a793 | 1 year ago |
Ozzie Isaacs | 7a4e6fbdfb | 1 year ago |
Ozzie Isaacs | 14d14637cd | 1 year ago |
Ozzie Isaacs | fb42f6bfff | 1 year ago |
Ozzie Isaacs | 4b7a0f3662 | 1 year ago |
Ozzie Isaacs | 275675b48a | 1 year ago |
Ozzie Isaacs | 907606295d | 1 year ago |
Ozzie Isaacs | 794c6ba254 | 1 year ago |
Ozzie Isaacs | ac13f6042a | 1 year ago |
Ozzie Isaacs | f8fbc807f1 | 1 year ago |
Ozzie Isaacs | 98da7dd5b0 | 1 year ago |
Ozzie Isaacs | 1c3b69c710 | 1 year ago |
mapi68 | 1dd638a786 | 1 year ago |
_Fervor_ | 6da7d05c6c | 1 year ago |
_Fervor_ | 3f72c3fffe | 1 year ago |
Ozzie Isaacs | cf9a7d538f | 1 year ago |
Ozzie Isaacs | ea9e8d4384 | 1 year ago |
Ozzie Isaacs | b9769a0975 | 1 year ago |
Ozzie Isaacs | 3a262661b5 | 1 year ago |
Ozzie Isaacs | d2056ceb51 | 1 year ago |
Ozzie Isaacs | e71a3452e1 | 1 year ago |
Ozzie Isaacs | 189da65fac | 1 year ago |
Ozzie Isaacs | 0e6b7f96d3 | 1 year ago |
Ozzie Isaacs | 1babb566fb | 1 year ago |
Ozzie Isaacs | c4e4acfc26 | 1 year ago |
Ozzie Isaacs | 6afb429185 | 1 year ago |
Ozzie Isaacs | f241b260d7 | 1 year ago |
Ozzie Isaacs | 260a694834 | 1 year ago |
Ozzie Isaacs | 508e2b4d0a | 1 year ago |
Ozzie Isaacs | 9701a97a57 | 1 year ago |
Ozzie Isaacs | 4913f06e0d | 1 year ago |
Petipopotam | d545ea9e6f | 1 year ago |
Petipopotam | 1ad8dc102a | 1 year ago |
Ozzie Isaacs | 36cb454d1c | 1 year ago |
Ozzie Isaacs | 1899cda8d1 | 1 year ago |
Ozzie Isaacs | 8dd4d0be1b | 1 year ago |
Ozzie Isaacs | d48d6880af | 1 year ago |
Ozzie Isaacs | 94a6931d48 | 1 year ago |
Ozzie Isaacs | c21a870b8e | 1 year ago |
Ozzie Isaacs | 791bc9621a | 1 year ago |
Ozzie Isaacs | 2d6fe483ba | 1 year ago |
Ozzie Isaacs | ad43f07dab | 1 year ago |
Ozzie Isaacs | 77637d81dd | 1 year ago |
Ozzie Isaacs | a2bf6dfb7b | 1 year ago |
Ozzie Isaacs | 1cd05d614c | 1 year ago |
Ozzie Isaacs | d75f681247 | 1 year ago |
Ozzie Isaacs | 2be2920833 | 1 year ago |
Ozzie Isaacs | d6184619f5 | 1 year ago |
Ozzie Isaacs | 43ee85fbb5 | 1 year ago |
Ozzie Isaacs | 8022b1bb36 | 1 year ago |
Ozzie Isaacs | 9e75c65af8 | 1 year ago |
Ozzie Isaacs | 7881950e66 | 1 year ago |
Ozzie Isaacs | 031658ae94 | 1 year ago |
Arief Hidayat | 48c2c7b543 | 1 year ago |
Petipopotam | beb619c2c2 | 1 year ago |
Petipopotam | ed22209e6c | 1 year ago |
blitzmann | 364c48edd8 | 1 year ago |
Ozzie Isaacs | e178efb58c | 1 year ago |
Vegard Fladby | 4105c64320 | 1 year ago |
Josh O'Brien | b3335f6733 | 1 year ago |
Benedikt McMullin | fba95956de | 1 year ago |
Ozzie Isaacs | ce0b3d8d10 | 1 year ago |
Ozzie Isaacs | 9545aa2a0b | 1 year ago |
jvoisin | 4629eec774 | 1 year ago |
Jeroen Kroese | 4977381b1c | 1 year ago |
Ozzie Isaacs | 6c1631acba | 1 year ago |
Ozzie Isaacs | 1489228649 | 1 year ago |
Ozzie Isaacs | 74efa52f26 | 1 year ago |
Ozzie Isaacs | 1ca1281346 | 1 year ago |
jvoisin | 631496775e | 1 year ago |
jvoisin | c5e539bbcd | 1 year ago |
jvoisin | 02ec853e3b | 1 year ago |
Ozzie Isaacs | d0411fd9c7 | 1 year ago |
Ozzie Isaacs | 567cb2e097 | 1 year ago |
Ozzie Isaacs | a635e136be | 1 year ago |
Ozzie Isaacs | 5ffb3e917f | 1 year ago |
Ozzie Isaacs | 5dc3385ae5 | 1 year ago |
Ozzie Isaacs | 66e0a81d23 | 1 year ago |
Ozzie Isaacs | 1f6eb2def6 | 1 year ago |
Ozzie Isaacs | 7d3af5bbd0 | 1 year ago |
Ozzie Isaacs | 043a612d1a | 1 year ago |
Ozzie Isaacs | 928e24fd1a | 1 year ago |
Ozzie Isaacs | 3361c41c6d | 1 year ago |
Ozzie Isaacs | 85a6616606 | 1 year ago |
Ozzie Isaacs | c15b603fef | 1 year ago |
Ozzie Isaacs | b12e47d0e5 | 1 year ago |
Ozzie Isaacs | 389263f5e7 | 1 year ago |
Ozzie Isaacs | 307b4526f6 | 1 year ago |
jvoisin | 7d023ce741 | 1 year ago |
Julien Voisin | 2ddbaa2150 | 1 year ago |
jvoisin | 29fef4a314 | 1 year ago |
JonathanHerrewijnen | 9450084d6e | 2 years ago |
Vijay Pillai | b52c7aac53 | 2 years ago |
Feige-cn | e8c461b14f | 2 years ago |
Ghighi Eftimie | 9409b9db9c | 2 years ago |
Ghighi Eftimie | a992aafc13 | 2 years ago |
Ghighi Eftimie | b663f1ce83 | 2 years ago |
Olivier | b45d69ef2d | 2 years ago |
Olivier | a80735d7d3 | 2 years ago |
Olivier | adfbd447ed | 2 years ago |
xlivevil | 73567db4fb | 2 years ago |
Ozzieisaacs | 3d59a78c9f | 2 years ago |
Ozzieisaacs | 8ba23ac3ee | 2 years ago |
Ghighi Eftimie | 397cd987cb | 2 years ago |
Ozzie Isaacs | 7eef44f73c | 2 years ago |
Ozzie Isaacs | e22ecda137 | 2 years ago |
ElQuimm | a003cd9758 | 2 years ago |
Ozzie Isaacs | 44f6655dd2 | 2 years ago |
Ozzie Isaacs | bd52f08a30 | 2 years ago |
Ozzie Isaacs | edc9703716 | 2 years ago |
Ozzie Isaacs | 56d697122c | 2 years ago |
Ozzie Isaacs | d39a43e838 | 2 years ago |
ElQuimm | 9df3a2558d | 2 years ago |
xlivevil | 7339c804a3 | 2 years ago |
xlivevil | 4d61c5535e | 2 years ago |
xlivevil | 09e1ec3d08 | 2 years ago |
Ozzie Isaacs | 8421a017f4 | 2 years ago |
Ozzie Isaacs | 27eb514ca4 | 2 years ago |
Ozzie Isaacs | b4d9e400d9 | 2 years ago |
Ozzie Isaacs | 67bc23ee0c | 2 years ago |
Ozzie Isaacs | b898b37e29 | 2 years ago |
Ozzie Isaacs | 10dcf39d50 | 2 years ago |
Ozzie Isaacs | e676e1685b | 2 years ago |
Ozzie Isaacs | 59a5ccd05c | 2 years ago |
Ozzie Isaacs | 04908e22fe | 2 years ago |
Ozzie Isaacs | 0f67e57be4 | 2 years ago |
Ozzie Isaacs | 071d19b8b3 | 2 years ago |
halink0803 | 1ffa190938 | 2 years ago |
Ozzie Isaacs | c10708ed07 | 2 years ago |
Ozzie Isaacs | b4851e1d70 | 2 years ago |
Ozzie Isaacs | 26be5ee237 | 2 years ago |
Ozzieisaacs | 241aa77d41 | 2 years ago |
Ozzieisaacs | ca0ee5d391 | 2 years ago |
Ozzieisaacs | 110d283a50 | 2 years ago |
Giulio De Pasquale | f6a9030c33 | 2 years ago |
Giulio De Pasquale | 452093db47 | 2 years ago |
Ozzieisaacs | 9fa56a2323 | 2 years ago |
Ozzieisaacs | 3a133901e4 | 2 years ago |
Ozzieisaacs | 7750ebde0f | 2 years ago |
Ozzieisaacs | 2472e03a69 | 2 years ago |
Ozzieisaacs | 6598c4d259 | 2 years ago |
Ozzie Isaacs | a9b20ca136 | 2 years ago |
Ozzie Isaacs | bf0375d51d | 2 years ago |
Ozzie Isaacs | 89d226e36b | 2 years ago |
Ozzie Isaacs | ec8844c7d4 | 2 years ago |
Ozzie Isaacs | e5c8a7ce50 | 2 years ago |
Ozzie Isaacs | dc3cafd23d | 2 years ago |
Ozzie Isaacs | 9de474e665 | 2 years ago |
Martin Brodbeck | cd143b7ef4 | 2 years ago |
Martin Brodbeck | 8a5112502d | 2 years ago |
Ozzie Isaacs | b5d5660d04 | 2 years ago |
Ozzie Isaacs | fc9c641e55 | 2 years ago |
Ozzie Isaacs | 68e21e1098 | 2 years ago |
Ozzie Isaacs | 828be29a80 | 2 years ago |
viljasenville | 46e5305f23 | 2 years ago |
Ozzie Isaacs | a3f7dc2a5a | 2 years ago |
Thore Schillmann | 9bcbe523d7 | 2 years ago |
Ozzie Isaacs | ae3e3559b8 | 2 years ago |
Ozzie Isaacs | a72f16fd3a | 2 years ago |
Ozzie Isaacs | c2545315e1 | 2 years ago |
Ozzie Isaacs | 61a0c72f8e | 2 years ago |
Ozzie Isaacs | 1e44cb3b6c | 2 years ago |
Ozzie Isaacs | 462aa47ed6 | 2 years ago |
Thore Schillmann | e176d63ca6 | 2 years ago |
Thore Schillmann | 80b0e88650 | 2 years ago |
Thore Schillmann | 0b4731913e | 2 years ago |
Thore Schillmann | fc7ce8da2d | 2 years ago |
Thore Schillmann | c89bc12c9b | 2 years ago |
Thore Schillmann | 4913673e8f | 2 years ago |
Thore Schillmann | fc004f4f0c | 2 years ago |
Ozzie Isaacs | 7344ef353c | 2 years ago |
Ozzie Isaacs | 3bde8a5d95 | 2 years ago |
Thore Schillmann | c5c3874243 | 2 years ago |
Kian-Meng Ang | c4104ddaf4 | 2 years ago |
Thore Schillmann | 0d34f41a48 | 2 years ago |
xlivevil | b47c1d2431 | 2 years ago |
Thore Schillmann | a77aef83c6 | 2 years ago |
Thore Schillmann | e39c6130c3 | 2 years ago |
subdiox | 12071f3e64 | 2 years ago |
subdiox | 92b6dbf26f | 2 years ago |
subdiox | 98b554a3a0 | 2 years ago |
Thore Schillmann | 03359599ed | 2 years ago |
Thore Schillmann | 3c4330ba51 | 2 years ago |
Thore Schillmann | 8c781ad4a4 | 2 years ago |
Thore Schillmann | 5e9ec706c5 | 2 years ago |
Ozzie Isaacs | 07c67b09db | 2 years ago |
Ozzie Isaacs | b1c70d5b4a | 2 years ago |
Ozzieisaacs | c5fc30a1be | 2 years ago |
Ozzie Isaacs | 29fd4ae4a2 | 2 years ago |
Ozzieisaacs | 4ef8c35fb7 | 2 years ago |
Ozzieisaacs | 04326af2da | 2 years ago |
Ozzieisaacs | d6a31e5db8 | 2 years ago |
Ozzie Isaacs | 73d48e4ac1 | 2 years ago |
Ozzie Isaacs | b206b7a5d8 | 2 years ago |
Ozzie Isaacs | 02e1be09df | 2 years ago |
Ozzie Isaacs | f85b587d0a | 2 years ago |
Ozzie Isaacs | 89d522e389 | 2 years ago |
Thore Schillmann | 2816a75c3e | 2 years ago |
Ozzie Isaacs | 78b45f716a | 2 years ago |
Ozzie Isaacs | 91df265d40 | 2 years ago |
Illia Maier | 7e7f54cfa7 | 2 years ago |
Illia Maier | 80bc14c0cf | 2 years ago |
Illia Maier | 7685818b16 | 2 years ago |
Ozzie Isaacs | f44d42f834 | 2 years ago |
GarcaMan | bf12542df5 | 2 years ago |
Ozzie Isaacs | 25f2af3f03 | 2 years ago |
Ozzie Isaacs | 909797dc49 | 2 years ago |
Ozzie Isaacs | 07d4e60655 | 2 years ago |
Ozzie Isaacs | d90cfce97f | 2 years ago |
Ozzie Isaacs | 543fe12862 | 2 years ago |
Ozzie Isaacs | 4f66d6b3b1 | 2 years ago |
Illia Maier | c36138b144 | 2 years ago |
Thore Schillmann | 7f6e88ce5e | 2 years ago |
Ozzie Isaacs | aa442d8c51 | 2 years ago |
Aisha Tammy | a3cd217cea | 2 years ago |
Thore Schillmann | 0f3f918153 | 2 years ago |
Ozzieisaacs | 790080f2a0 | 2 years ago |
Ozzieisaacs | 4ea80e9810 | 2 years ago |
Ozzieisaacs | 034d57134d | 2 years ago |
Ozzieisaacs | f6101fd462 | 2 years ago |
Ozzieisaacs | 1fa7de397a | 2 years ago |
leexia | 3b7cd38d5e | 2 years ago |
ImanSharaf | 78fb7a9756 | 2 years ago |
Evan Peterson | 7ae9f89bbf | 2 years ago |
Ozzie Isaacs | 8a6a8dcbe8 | 2 years ago |
Ozzie Isaacs | fbac3e38ac | 2 years ago |
Ozzie Isaacs | c1f1952b04 | 2 years ago |
Ozzie Isaacs | 5e4cf839bc | 2 years ago |
Ozzie Isaacs | 056ecf0d90 | 2 years ago |
Chris Thurber | 0c2f67bc7b | 2 years ago |
Ozzie Isaacs | 1bcb714fac | 2 years ago |
Ozzie Isaacs | 8cb3fe32a5 | 2 years ago |
Ozzieisaacs | ae5053e072 | 2 years ago |
Ozzie Isaacs | cde51e743a | 2 years ago |
Ozzie Isaacs | 3233b357f8 | 2 years ago |
Ozzie Isaacs | 49655e9f2d | 2 years ago |
Ozzie Isaacs | 7b45324149 | 2 years ago |
Ozzie Isaacs | 858d099509 | 2 years ago |
Ozzie Isaacs | 12f3a13d1d | 2 years ago |
Ozzieisaacs | 813d303ea7 | 2 years ago |
Ozzieisaacs | c1ca18f7dc | 2 years ago |
Ozzie Isaacs | e8e4d87d39 | 2 years ago |
Ozzie Isaacs | 5d5a94c9e5 | 2 years ago |
Ozzie Isaacs | 258b4a6767 | 2 years ago |
Ozzie Isaacs | ef4b5e2881 | 2 years ago |
Ozzie Isaacs | a968ddaef2 | 2 years ago |
Ozzie Isaacs | aaa749933d | 2 years ago |
Ozzie Isaacs | 2e007a160e | 2 years ago |
Ozzie Isaacs | e7464f2694 | 2 years ago |
Ozzie Isaacs | 47414ada69 | 2 years ago |
Ozzie Isaacs | 9410b47144 | 2 years ago |
Ozzie Isaacs | db03fb3edd | 2 years ago |
Ozzie Isaacs | 2b03cae017 | 2 years ago |
Ozzie Isaacs | 21ebdc0130 | 2 years ago |
Ozzie Isaacs | 6e8445fed5 | 2 years ago |
Ozzie Isaacs | d83c731030 | 2 years ago |
Ozzie Isaacs | ae9a970782 | 2 years ago |
Ozzie Isaacs | 1e723dff3a | 2 years ago |
Ozzie Isaacs | 8421a17a82 | 2 years ago |
Ozzie Isaacs | bc96ff9a39 | 2 years ago |
Ozzie Isaacs | bf049d8240 | 2 years ago |
Ozzie Isaacs | 6e783cd7ee | 2 years ago |
Ozzie Isaacs | 069dc2766f | 2 years ago |
Ozzie Isaacs | 2f5b9e41ac | 2 years ago |
Ozzie Isaacs | 9a8093db31 | 2 years ago |
Ozzie Isaacs | 5c342d4e7c | 2 years ago |
Ozzie Isaacs | 3c98cd1b9a | 2 years ago |
Ozzie Isaacs | 2303fc0814 | 2 years ago |
Ozzie Isaacs | a8680a45ca | 2 years ago |
Ozzie Isaacs | d75d95f401 | 2 years ago |
Ozzie Isaacs | fbb6de7195 | 2 years ago |
xlivevil | 3cbbf6fa86 | 2 years ago |
Ozzieisaacs | c92d65aad3 | 2 years ago |
Ozzieisaacs | c61e5d6ac0 | 2 years ago |
Ozzieisaacs | 130af069aa | 2 years ago |
Ozzieisaacs | 09b381101b | 2 years ago |
Ozzieisaacs | 35bb899879 | 2 years ago |
Ozzie Isaacs | 6184e2b7bc | 2 years ago |
Ozzie Isaacs | 2f3e5eadeb | 2 years ago |
Ozzie Isaacs | fe5d684d2c | 2 years ago |
Ozzie Isaacs | df53a5d8c9 | 2 years ago |
Ozzie Isaacs | 83b99fcb1a | 2 years ago |
Ozzie Isaacs | 028e6855a7 | 2 years ago |
Wulf Rajek | adf6728f14 | 2 years ago |
Ozzie Isaacs | 652d0fd86f | 2 years ago |
Ozzie Isaacs | 1136383b9a | 2 years ago |
Ozzie Isaacs | d770e5392e | 2 years ago |
Ozzie Isaacs | 3d2e7e847e | 2 years ago |
Ozzie Isaacs | a63af5882e | 2 years ago |
Wulf Rajek | 2d0af0ab49 | 2 years ago |
Ozzie Isaacs | d912c1c476 | 2 years ago |
Ozzie Isaacs | 42b0226f1a | 2 years ago |
Ozzie Isaacs | 8adae6ed0c | 2 years ago |
Ozzie Isaacs | fee76741a0 | 2 years ago |
Ozzie Isaacs | f36d3a76be | 2 years ago |
Ozzie Isaacs | afaf496fbe | 2 years ago |
Ozzie Isaacs | c06754975e | 2 years ago |
Ozzie Isaacs | 834edadc28 | 2 years ago |
Ozzie Isaacs | 7861f8a89a | 2 years ago |
Ozzie Isaacs | 73d359af05 | 2 years ago |
Ozzie Isaacs | 036cd7be48 | 2 years ago |
Ozzie Isaacs | baffe1f537 | 2 years ago |
Ozzie Isaacs | 2f949ce1dd | 2 years ago |
Ozzie Isaacs | 32a3c45ee0 | 2 years ago |
Ozzie Isaacs | 14a6e7c42c | 2 years ago |
Ozzie Isaacs | 2a5e9a97bb | 2 years ago |
Nicolas Ferrari | 504e58abdb | 2 years ago |
Ozzie Isaacs | a6a8f7eb43 | 2 years ago |
Ozzie Isaacs | 5070cc4c23 | 2 years ago |
Ozzie Isaacs | 0d49b56883 | 2 years ago |
Ozzie Isaacs | f5b79930ad | 2 years ago |
Ozzie Isaacs | c0d0660986 | 2 years ago |
Ozzie Isaacs | ec53570118 | 2 years ago |
Ozzie Isaacs | 8cb5989c97 | 2 years ago |
Ozzie Isaacs | 39459603d4 | 2 years ago |
Ozzie Isaacs | f34fc002da | 2 years ago |
Ozzie Isaacs | 06e8845641 | 2 years ago |
Ozzie Isaacs | 034ab73ccc | 2 years ago |
Ozzie Isaacs | 57cd8160a0 | 2 years ago |
Ozzie Isaacs | 399ddc5d6f | 2 years ago |
Ozzie Isaacs | d9a83e0638 | 2 years ago |
Ozzie Isaacs | 8f3bb2e338 | 2 years ago |
Ozzie Isaacs | 4545f4a20d | 2 years ago |
Ozzie Isaacs | 296f76b5fb | 2 years ago |
Ozzie Isaacs | 3b5e5f9b90 | 2 years ago |
Ozzie Isaacs | 8e2536c53b | 2 years ago |
Ozzie Isaacs | 4379669cf8 | 2 years ago |
Ozzie Isaacs | 2b31b6a306 | 2 years ago |
Ozzie Isaacs | 3a0dacc6a6 | 2 years ago |
Ozzie Isaacs | 547ea93dc9 | 2 years ago |
Ozzie Isaacs | d80297e1a8 | 2 years ago |
Ozzie Isaacs | 49692b4a45 | 2 years ago |
xlivevil | b54a170a00 | 2 years ago |
Ozzie Isaacs | 34478079d8 | 2 years ago |
Ozzie Isaacs | 753319c8b6 | 2 years ago |
Ozzie Isaacs | c53817859a | 2 years ago |
Ozzie Isaacs | 153a443fca | 2 years ago |
Bharat KNV | 9efd644360 | 2 years ago |
Ozzie Isaacs | 598618e428 | 2 years ago |
Ozzie Isaacs | 965352c8d9 | 2 years ago |
xlivevil | 97cf20764b | 2 years ago |
xlivevil | 695ce83681 | 2 years ago |
xlivevil | 86b779f39b | 2 years ago |
Ozzie Isaacs | 8007e450b3 | 2 years ago |
Ozzie Isaacs | 0aac961cde | 2 years ago |
Ozzie Isaacs | ef7c6731bc | 2 years ago |
Ozzie Isaacs | e9b674f46e | 2 years ago |
Ozzie Isaacs | 8f665ebd58 | 2 years ago |
Ozzie Isaacs | 7bb3cac7fb | 2 years ago |
Ozzie Isaacs | 9c5970bbfc | 2 years ago |
Ozzie Isaacs | ba23ada1fe | 2 years ago |
Ozzie Isaacs | 86b621e768 | 2 years ago |
Ozzie Isaacs | 5f70406b30 | 2 years ago |
Ozzie Isaacs | 6b026513cb | 2 years ago |
Ozzie Isaacs | 0436f0f9b2 | 2 years ago |
Ozzie Isaacs | 295888c654 | 2 years ago |
Ozzie Isaacs | 7317084a4e | 2 years ago |
Ozzie Isaacs | 0981337cdf | 2 years ago |
Ozzie Isaacs | 461dd05e2f | 2 years ago |
Ozzie Isaacs | 764389ea2a | 2 years ago |
Ozzie Isaacs | 4a0dde0371 | 2 years ago |
Ozzie Isaacs | e22b3da601 | 2 years ago |
Ozzie Isaacs | 7c623941de | 2 years ago |
Ozzie Isaacs | 3bb41aca6d | 2 years ago |
Ozzie Isaacs | 0c3c0c0664 | 2 years ago |
Ozzie Isaacs | 411c13977f | 2 years ago |
Ozzie Isaacs | 41f89af959 | 2 years ago |
Ozzie Isaacs | 895f68033f | 2 years ago |
Ozzie Isaacs | 2d49589e4b | 2 years ago |
Ozzie Isaacs | 89877835b3 | 2 years ago |
Ozzie Isaacs | 0ce41aef56 | 2 years ago |
Ozzie Isaacs | 5b3015619d | 2 years ago |
Ozzie Isaacs | 6ca08a7cc1 | 2 years ago |
Ozzie Isaacs | 7254ce6c81 | 2 years ago |
Ozzie Isaacs | cfa6b405da | 2 years ago |
Ozzie Isaacs | 26a8ac1425 | 2 years ago |
Ozzie Isaacs | 1ce45f3253 | 2 years ago |
Ozzie Isaacs | a03c95329c | 2 years ago |
Ozzie Isaacs | e0bf829def | 2 years ago |
Ozzie Isaacs | 0bc15636f2 | 2 years ago |
Ozzie Isaacs | 61bfeae936 | 2 years ago |
Ozzie Isaacs | 95e0255aa1 | 2 years ago |
Ozzie Isaacs | 23e47ba4e6 | 2 years ago |
Ozzie Isaacs | 4dcc44803c | 2 years ago |
Ozzie Isaacs | 111ab121b1 | 2 years ago |
Ozzie Isaacs | 1e04b51148 | 2 years ago |
Ozzie Isaacs | 3ae1b97d72 | 2 years ago |
Ozzie Isaacs | 3123a914a4 | 2 years ago |
Ozzie Isaacs | f6b46bb170 | 2 years ago |
Ozzie Isaacs | bb7f4cf74e | 2 years ago |
Ozzie Isaacs | 39ac37861f | 2 years ago |
mmonkey | 62ff6f7e8a | 2 years ago |
mmonkey | 032fced9c7 | 2 years ago |
Ozzie Isaacs | ae9c5da777 | 2 years ago |
Ozzie Isaacs | 42f8209a4a | 2 years ago |
Ozzie Isaacs | e757be6953 | 2 years ago |
Ozzie Isaacs | 4f3c396450 | 2 years ago |
mmonkey | 50bb74d748 | 2 years ago |
mmonkey | 3416323767 | 2 years ago |
mmonkey | 18ce310b30 | 2 years ago |
Ozzie Isaacs | 6339d25af0 | 2 years ago |
quarz12 | 477b202c38 | 2 years ago |
quarz12 | 326d6e7b9d | 2 years ago |
Ozzie Isaacs | baf32f9045 | 2 years ago |
Ozzie Isaacs | 3c4cd22d9e | 2 years ago |
Ozzie Isaacs | d9d6fb33ba | 2 years ago |
Ozzie Isaacs | 17b4643b7c | 2 years ago |
Daniel | 239f389c5c | 2 years ago |
Daniel | 8362c82d54 | 2 years ago |
Daniel | 62e7aca0fb | 2 years ago |
Ozzie Isaacs | 6a37c7ca9d | 2 years ago |
Ozzie Isaacs | 128db26301 | 2 years ago |
Ozzie Isaacs | d8f5bdea6d | 2 years ago |
Ozzie Isaacs | 127bf98aac | 2 years ago |
Ozzie Isaacs | ede273a8f9 | 2 years ago |
Ozzie Isaacs | bc7a305285 | 2 years ago |
Ozzie Isaacs | d759df0df6 | 2 years ago |
collerek | 20b5a9a2c0 | 2 years ago |
Evan Peterson | 4eaa9413f9 | 2 years ago |
Ozzieisaacs | 7eb875f388 | 2 years ago |
Ozzie Isaacs | 1a6579312f | 2 years ago |
Ozzie Isaacs | 2e815147fb | 3 years ago |
Bharat KNV | 0693cb1ddb | 3 years ago |
collerek | bea14d1784 | 3 years ago |
collerek | 51bf35c2e4 | 3 years ago |
collerek | d64589914f | 3 years ago |
collerek | 362fdc5716 | 3 years ago |
collerek | d55626d445 | 3 years ago |
Ozzie Isaacs | 42bf40d7bb | 3 years ago |
collerek | 920acaca99 | 3 years ago |
Ozzie Isaacs | 6e15280fac | 3 years ago |
Ozzie Isaacs | f78d2245aa | 3 years ago |
Ozzie Isaacs | d217676350 | 3 years ago |
Ozzie Isaacs | cd5711e651 | 3 years ago |
GarcaMan | e2eab808c0 | 3 years ago |
GarcaMan | 3ac08a8c0d | 3 years ago |
Denis Rodríguez | 3f56f0dca7 | 3 years ago |
GarcaMan | 7fc04b353b | 3 years ago |
GarcaMan | a8689ae26b | 3 years ago |
Ozzieisaacs | 1e9d88fa98 | 3 years ago |
Ozzieisaacs | 6deb527769 | 3 years ago |
Ozzieisaacs | 8e5bb02a28 | 3 years ago |
Ozzieisaacs | 4fd4cf4355 | 3 years ago |
Ozzieisaacs | baba205bce | 3 years ago |
Ozzie Isaacs | 3cfffa1487 | 3 years ago |
Ozzie Isaacs | 3a1a32f053 | 3 years ago |
Ozzie Isaacs | f6a2b8a9ef | 3 years ago |
Ozzie Isaacs | 516e76de4f | 3 years ago |
Ozzie Isaacs | 4c7b5999f7 | 3 years ago |
Ozzie Isaacs | bb20979c71 | 3 years ago |
Ozzie Isaacs | 917909cfdb | 3 years ago |
Ozzie Isaacs | b4262b1317 | 3 years ago |
mmonkey | cd3791f5f4 | 3 years ago |
mmonkey | 9e7f69e38a | 3 years ago |
mmonkey | 46205a1f83 | 3 years ago |
mmonkey | 26071d4e7a | 3 years ago |
mmonkey | 0bd544704d | 3 years ago |
mmonkey | be28a91315 | 3 years ago |
mmonkey | 524ed07a6c | 3 years ago |
mmonkey | 8bee2b9552 | 3 years ago |
mmonkey | d648785471 | 3 years ago |
mmonkey | 9a08bcd2bc | 3 years ago |
mmonkey | 04a5db5c1d | 3 years ago |
Ozzie Isaacs | 10731696df | 3 years ago |
Ozzie Isaacs | 1e7a2c400b | 3 years ago |
Ozzie Isaacs | 30e897af48 | 3 years ago |
Ozzie Isaacs | dd30ac4fbd | 3 years ago |
mmonkey | 2c8d055ca4 | 3 years ago |
mmonkey | 8cc06683df | 3 years ago |
mmonkey | 774799f316 | 3 years ago |
mmonkey | d53daaa387 | 3 years ago |
mmonkey | 35c60eaee5 | 3 years ago |
mmonkey | af24d4edbe | 4 years ago |
mmonkey | eef21759cd | 4 years ago |
mmonkey | 242a2767a1 | 4 years ago |
mmonkey | 626051e489 | 4 years ago |
mmonkey | 541fc7e14e | 4 years ago |
mmonkey | e48bdf9d5a | 4 years ago |
mmonkey | 21fce9a5b5 | 4 years ago |
mmonkey | 774b9ae12d | 4 years ago |
@ -1,4 +1,5 @@
|
||||
constants.py ident export-subst
|
||||
/test export-ignore
|
||||
/library export-ignore
|
||||
cps/static/css/libs/* linguist-vendored
|
||||
cps/static/js/libs/* linguist-vendored
|
||||
|
@ -0,0 +1 @@
|
||||
custom: ["https://PayPal.Me/calibreweb",]
|
@ -1,101 +1,125 @@
|
||||
# About
|
||||
# Short Notice from the maintainer
|
||||
|
||||
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
||||
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
|
||||
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
|
||||
I have turned off all notifications from Github/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
||||
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
|
||||
|
||||
[![GitHub License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
||||
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)]()
|
||||
[![GitHub all releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
|
||||
# Calibre-Web
|
||||
|
||||
Calibre-Web is a web app that offers a clean and intuitive interface for browsing, reading, and downloading eBooks using a valid [Calibre](https://calibre-ebook.com) database.
|
||||
|
||||
[![License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
||||
![Commit Activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)
|
||||
[![All Releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
|
||||
[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
||||
[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
||||
[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB)
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong> (click to expand)</summary>
|
||||
|
||||
1. [About](#calibre-web)
|
||||
2. [Features](#features)
|
||||
3. [Installation](#installation)
|
||||
- [Installation via pip (recommended)](#installation-via-pip-recommended)
|
||||
- [Quick start](#quick-start)
|
||||
- [Requirements](#requirements)
|
||||
4. [Docker Images](#docker-images)
|
||||
5. [Contributor Recognition](#contributor-recognition)
|
||||
6. [Contact](#contact)
|
||||
7. [Contributing to Calibre-Web](#contributing-to-calibre-web)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
||||
|
||||
![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png)
|
||||
|
||||
## Features
|
||||
|
||||
- Bootstrap 3 HTML5 interface
|
||||
- full graphical setup
|
||||
- User management with fine-grained per-user permissions
|
||||
- Modern and responsive Bootstrap 3 HTML5 interface
|
||||
- Full graphical setup
|
||||
- Comprehensive user management with fine-grained per-user permissions
|
||||
- Admin interface
|
||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||
- OPDS feed for eBook reader apps
|
||||
- Filter and search by titles, authors, tags, series and language
|
||||
- Create a custom book collection (shelves)
|
||||
- Support for editing eBook metadata and deleting eBooks from Calibre library
|
||||
- Support for converting eBooks through Calibre binaries
|
||||
- Restrict eBook download to logged-in users
|
||||
- Support for public user registration
|
||||
- Send eBooks to Kindle devices with the click of a button
|
||||
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
||||
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
|
||||
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
||||
- Support for Calibre Custom Columns
|
||||
- Ability to hide content based on categories and Custom Column content per user
|
||||
- Multilingual user interface supporting 20+ languages ([supported languages](https://github.com/janeczku/calibre-web/wiki/Translation-Status))
|
||||
- OPDS feed for eBook reader apps
|
||||
- Advanced search and filtering options
|
||||
- Custom book collection (shelves) creation
|
||||
- eBook metadata editing and deletion support
|
||||
- Metadata download from various sources (extensible via plugins)
|
||||
- eBook conversion through Calibre binaries
|
||||
- eBook download restriction to logged-in users
|
||||
- Public user registration support
|
||||
- Send eBooks to E-Readers with a single click
|
||||
- Sync Kobo devices with your Calibre library
|
||||
- In-browser eBook reading support for multiple formats
|
||||
- Upload new books in various formats, including audio formats
|
||||
- Calibre Custom Columns support
|
||||
- Content hiding based on categories and Custom Column content per user
|
||||
- Self-update capability
|
||||
- "Magic Link" login to make it easy to log on eReaders
|
||||
- Login via LDAP, google/github oauth and via proxy authentication
|
||||
- "Magic Link" login for easy access on eReaders
|
||||
- LDAP, Google/GitHub OAuth, and proxy authentication support
|
||||
|
||||
## Installation
|
||||
|
||||
#### Installation via pip (recommended)
|
||||
1. Install calibre web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
|
||||
2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
|
||||
3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
|
||||
|
||||
#### Manual installation
|
||||
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
|
||||
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
|
||||
1. Create a virtual environment for Calibre-Web to avoid conflicts with existing Python dependencies
|
||||
2. Install Calibre-Web via pip: `pip install calibreweb` (or `pip3` depending on your OS/distro)
|
||||
3. Install optional features via pip as needed, see [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
|
||||
4. Start Calibre-Web by typing `cps`
|
||||
|
||||
Issues with Ubuntu:
|
||||
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
|
||||
*Note: Raspberry Pi OS users may encounter issues during installation. If so, please update pip (`./venv/bin/python3 -m pip install --upgrade pip`) and/or install cargo (`sudo apt install cargo`) before retrying the installation.*
|
||||
|
||||
## Quick start
|
||||
Refer to the Wiki for additional installation examples: [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-in-Linux-Mint-19-or-20), [Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
|
||||
|
||||
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
||||
Go to Login page
|
||||
## Quick Start
|
||||
|
||||
#### Default admin login:
|
||||
*Username:* admin\
|
||||
*Password:* admin123
|
||||
1. Open your browser and navigate to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
2. Log in with the default admin credentials
|
||||
3. If you don't have a Calibre database, you can use [this database](https://github.com/janeczku/calibre-web/raw/master/library/metadata.db) (move it out of the Calibre-Web folder to prevent overwriting during updates)
|
||||
4. Set `Location of Calibre database` to the path of the folder containing your Calibre library (metadata.db) and click "Save"
|
||||
5. Optionally, use Google Drive to host your Calibre library by following the [Google Drive integration guide](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration)
|
||||
6. Configure your Calibre-Web instance via the admin page, referring to the [Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) guides
|
||||
|
||||
#### Default Admin Login:
|
||||
- **Username:** admin
|
||||
- **Password:** admin123
|
||||
|
||||
## Requirements
|
||||
|
||||
python 3.5+
|
||||
- Python 3.5+
|
||||
- [Imagemagick](https://imagemagick.org/script/download.php) for cover extraction from EPUBs (Windows users may need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction)
|
||||
- Optional: [Calibre desktop program](https://calibre-ebook.com/download) for on-the-fly conversion and metadata editing (set "calibre's converter tool" path on the setup page)
|
||||
- Optional: [Kepubify tool](https://github.com/pgaskin/kepubify/releases/latest) for Kobo device support (place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows)
|
||||
|
||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
||||
## Docker Images
|
||||
|
||||
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
|
||||
Pre-built Docker images are available in the following Docker Hub repositories (maintained by the LinuxServer team):
|
||||
|
||||
[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `\opt\kepubify` Windows: `C:\Program Files\kepubify`.
|
||||
#### **LinuxServer - x64, aarch64**
|
||||
- [Docker Hub](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||
- [GitHub](https://github.com/linuxserver/docker-calibre-web)
|
||||
- [GitHub - Optional Calibre layer](https://github.com/linuxserver/docker-mods/tree/universal-calibre)
|
||||
|
||||
## Docker Images
|
||||
Include the environment variable `DOCKER_MODS=linuxserver/mods:universal-calibre` in your Docker run/compose file to add the Calibre `ebook-convert` binary (x64 only). Omit this variable for a lightweight image.
|
||||
|
||||
Both the Calibre-Web and Calibre-Mod images are automatically rebuilt on new releases and updates.
|
||||
|
||||
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
||||
- Set "path to convertertool" to `/usr/bin/ebook-convert`
|
||||
- Set "path to unrar" to `/usr/bin/unrar`
|
||||
|
||||
#### **LinuxServer - x64, armhf, aarch64**
|
||||
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||
+ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
|
||||
+ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre)
|
||||
## Contributor Recognition
|
||||
|
||||
This image has the option to pull in an extra docker manifest layer to include the Calibre `ebook-convert` binary. Just include the environmental variable `DOCKER_MODS=linuxserver/calibre-web:calibre` in your docker run/docker compose file. **(x64 only)**
|
||||
|
||||
If you do not need this functionality then this can be omitted, keeping the image as lightweight as possible.
|
||||
|
||||
Both the Calibre-Web and Calibre-Mod images are rebuilt automatically on new releases of Calibre-Web and Calibre respectively, and on updates to any included base image packages on a weekly basis if required.
|
||||
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
||||
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
||||
We would like to thank all the [contributors](https://github.com/janeczku/calibre-web/graphs/contributors) and maintainers of Calibre-Web for their valuable input and dedication to the project. Your contributions are greatly appreciated.
|
||||
|
||||
# Contact
|
||||
## Contact
|
||||
|
||||
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||
Join us on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||
|
||||
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
||||
For more information, How To's, and FAQs, please visit the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
||||
|
||||
# Contributing to Calibre-Web
|
||||
## Contributing to Calibre-Web
|
||||
|
||||
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
||||
Check out our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
||||
|
@ -1,3 +1,4 @@
|
||||
[python: **.py]
|
||||
|
||||
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
|
||||
[jinja2: **/templates/**.*ml]
|
||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
@ -0,0 +1,40 @@
|
||||
from babel import negotiate_locale
|
||||
from flask_babel import Babel, Locale
|
||||
from babel.core import UnknownLocaleError
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
from . import logger
|
||||
|
||||
log = logger.create()
|
||||
|
||||
babel = Babel()
|
||||
|
||||
def get_locale():
|
||||
# if a user is logged in, use the locale from the user settings
|
||||
if current_user is not None and hasattr(current_user, "locale"):
|
||||
# if the account is the guest account bypass the config lang settings
|
||||
if current_user.name != 'Guest':
|
||||
return current_user.locale
|
||||
|
||||
preferred = list()
|
||||
if request.accept_languages:
|
||||
for x in request.accept_languages.values():
|
||||
try:
|
||||
preferred.append(str(Locale.parse(x.replace('-', '_'))))
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
log.debug('Could not parse locale "%s": %s', x, e)
|
||||
|
||||
return negotiate_locale(preferred or ['en'], get_available_translations())
|
||||
|
||||
|
||||
def get_user_locale_language(user_language):
|
||||
return Locale.parse(user_language).get_language_name(get_locale())
|
||||
|
||||
|
||||
def get_available_locale():
|
||||
return [Locale('en')] + babel.list_translations()
|
||||
|
||||
|
||||
def get_available_translations():
|
||||
return set(str(item) for item in get_available_locale())
|
@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018-2019 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from . import logger
|
||||
from lxml.etree import ParserError
|
||||
|
||||
try:
|
||||
# at least bleach 6.0 is needed -> incomplatible change from list arguments to set arguments
|
||||
from bleach import clean_text as clean_html
|
||||
BLEACH = True
|
||||
except ImportError:
|
||||
try:
|
||||
BLEACH = False
|
||||
from nh3 import clean as clean_html
|
||||
except ImportError:
|
||||
try:
|
||||
BLEACH = False
|
||||
from lxml.html.clean import clean_html
|
||||
except ImportError:
|
||||
clean_html = None
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def clean_string(unsafe_text, book_id=0):
|
||||
try:
|
||||
if BLEACH:
|
||||
safe_text = clean_html(unsafe_text, tags=set(), attributes=set())
|
||||
else:
|
||||
safe_text = clean_html(unsafe_text)
|
||||
except ParserError as e:
|
||||
log.error("Comments of book {} are corrupted: {}".format(book_id, e))
|
||||
safe_text = ""
|
||||
except TypeError as e:
|
||||
log.error("Comments can't be parsed, maybe 'lxml' is too new, try installing 'bleach': {}".format(e))
|
||||
safe_text = ""
|
||||
return safe_text
|
@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2022 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
use_IM = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
use_IM = False
|
||||
|
||||
|
||||
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
|
||||
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
|
||||
|
||||
|
||||
def cover_processing(tmp_file_name, img, extension):
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
if use_IM:
|
||||
with Image(blob=img) as imgc:
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace('rgb')
|
||||
imgc.save(filename=tmp_cover_name)
|
||||
return tmp_cover_name
|
||||
else:
|
||||
return None
|
||||
if img:
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
else:
|
||||
return None
|
@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2024 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from uuid import uuid4
|
||||
import os
|
||||
|
||||
from .file_helper import get_temp_dir
|
||||
from .subproc_wrapper import process_open
|
||||
from . import logger, config
|
||||
from .constants import SUPPORTED_CALIBRE_BINARIES
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def do_calibre_export(book_id, book_format):
|
||||
try:
|
||||
quotes = [3, 5, 7, 9]
|
||||
tmp_dir = get_temp_dir()
|
||||
calibredb_binarypath = get_calibre_binarypath("calibredb")
|
||||
temp_file_name = str(uuid4())
|
||||
my_env = os.environ.copy()
|
||||
if config.config_calibre_split:
|
||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
library_path = config.config_calibre_split_dir
|
||||
else:
|
||||
library_path = config.config_calibre_dir
|
||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||
str(book_id)]
|
||||
p = process_open(opf_command, quotes, my_env)
|
||||
_, err = p.communicate()
|
||||
if err:
|
||||
log.error('Metadata embedder encountered an error: %s', err)
|
||||
return tmp_dir, temp_file_name
|
||||
except OSError as ex:
|
||||
# ToDo real error handling
|
||||
log.error_or_exception(ex)
|
||||
return None, None
|
||||
|
||||
|
||||
def get_calibre_binarypath(binary):
|
||||
binariesdir = config.config_binariesdir
|
||||
if binariesdir:
|
||||
try:
|
||||
return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary])
|
||||
except KeyError as ex:
|
||||
log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary])
|
||||
pass
|
||||
return ""
|
@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2018 lemmsh, Kennyl, Kyosfonica, matthazinski
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
|
||||
from . import isoLanguages
|
||||
|
||||
default_ns = {
|
||||
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
||||
'pkg': 'http://www.idpf.org/2007/opf',
|
||||
}
|
||||
|
||||
OPF_NAMESPACE = "http://www.idpf.org/2007/opf"
|
||||
PURL_NAMESPACE = "http://purl.org/dc/elements/1.1/"
|
||||
|
||||
OPF = "{%s}" % OPF_NAMESPACE
|
||||
PURL = "{%s}" % PURL_NAMESPACE
|
||||
|
||||
etree.register_namespace("opf", OPF_NAMESPACE)
|
||||
etree.register_namespace("dc", PURL_NAMESPACE)
|
||||
|
||||
OPF_NS = {None: OPF_NAMESPACE} # the default namespace (no prefix)
|
||||
NSMAP = {'dc': PURL_NAMESPACE, 'opf': OPF_NAMESPACE}
|
||||
|
||||
|
||||
def updateEpub(src, dest, filename, data, ):
|
||||
# create a temp copy of the archive without filename
|
||||
with zipfile.ZipFile(src, 'r') as zin:
|
||||
with zipfile.ZipFile(dest, 'w') as zout:
|
||||
zout.comment = zin.comment # preserve the comment
|
||||
for item in zin.infolist():
|
||||
if item.filename != filename:
|
||||
zout.writestr(item, zin.read(item.filename))
|
||||
|
||||
# now add filename with its new data
|
||||
with zipfile.ZipFile(dest, mode='a', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr(filename, data)
|
||||
|
||||
|
||||
def get_content_opf(file_path, ns=default_ns):
|
||||
epubZip = zipfile.ZipFile(file_path)
|
||||
txt = epubZip.read('META-INF/container.xml')
|
||||
tree = etree.fromstring(txt)
|
||||
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
||||
cf = epubZip.read(cf_name)
|
||||
|
||||
return etree.fromstring(cf), cf_name
|
||||
|
||||
|
||||
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
||||
# generate root package element
|
||||
package = etree.Element(OPF + "package", nsmap=OPF_NS)
|
||||
package.set("unique-identifier", "uuid_id")
|
||||
package.set("version", "2.0")
|
||||
|
||||
# generate metadata element and all sub elements of it
|
||||
metadata = etree.SubElement(package, "metadata", nsmap=NSMAP)
|
||||
identifier = etree.SubElement(metadata, PURL + "identifier", id="calibre_id", nsmap=NSMAP)
|
||||
identifier.set(OPF + "scheme", "calibre")
|
||||
identifier.text = str(book.id)
|
||||
identifier2 = etree.SubElement(metadata, PURL + "identifier", id="uuid_id", nsmap=NSMAP)
|
||||
identifier2.set(OPF + "scheme", "uuid")
|
||||
identifier2.text = book.uuid
|
||||
for i in book.identifiers:
|
||||
identifier = etree.SubElement(metadata, PURL + "identifier", nsmap=NSMAP)
|
||||
identifier.set(OPF + "scheme", i.format_type())
|
||||
identifier.text = str(i.val)
|
||||
title = etree.SubElement(metadata, PURL + "title", nsmap=NSMAP)
|
||||
title.text = book.title
|
||||
for author in book.authors:
|
||||
creator = etree.SubElement(metadata, PURL + "creator", nsmap=NSMAP)
|
||||
creator.text = str(author.name)
|
||||
creator.set(OPF + "file-as", book.author_sort) # ToDo Check
|
||||
creator.set(OPF + "role", "aut")
|
||||
contributor = etree.SubElement(metadata, PURL + "contributor", nsmap=NSMAP)
|
||||
contributor.text = "calibre (5.7.2) [https://calibre-ebook.com]"
|
||||
contributor.set(OPF + "file-as", "calibre") # ToDo Check
|
||||
contributor.set(OPF + "role", "bkp")
|
||||
|
||||
date = etree.SubElement(metadata, PURL + "date", nsmap=NSMAP)
|
||||
date.text = '{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(d=book.pubdate)
|
||||
if book.comments and book.comments[0].text:
|
||||
for b in book.comments:
|
||||
description = etree.SubElement(metadata, PURL + "description", nsmap=NSMAP)
|
||||
description.text = b.text
|
||||
for b in book.publishers:
|
||||
publisher = etree.SubElement(metadata, PURL + "publisher", nsmap=NSMAP)
|
||||
publisher.text = str(b.name)
|
||||
if not book.languages:
|
||||
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
|
||||
language.text = export_language
|
||||
else:
|
||||
for b in book.languages:
|
||||
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
|
||||
language.text = str(b.lang_code) if lang_type == 3 else isoLanguages.get(part3=b.lang_code).part1
|
||||
for b in book.tags:
|
||||
subject = etree.SubElement(metadata, PURL + "subject", nsmap=NSMAP)
|
||||
subject.text = str(b.name)
|
||||
etree.SubElement(metadata, "meta", name="calibre:author_link_map",
|
||||
content="{" + ", ".join(['"' + str(a.name) + '": ""' for a in book.authors]) + "}",
|
||||
nsmap=NSMAP)
|
||||
for b in book.series:
|
||||
etree.SubElement(metadata, "meta", name="calibre:series",
|
||||
content=str(str(b.name)),
|
||||
nsmap=NSMAP)
|
||||
if book.series:
|
||||
etree.SubElement(metadata, "meta", name="calibre:series_index",
|
||||
content=str(book.series_index),
|
||||
nsmap=NSMAP)
|
||||
if len(book.ratings) and book.ratings[0].rating > 0:
|
||||
etree.SubElement(metadata, "meta", name="calibre:rating",
|
||||
content=str(book.ratings[0].rating),
|
||||
nsmap=NSMAP)
|
||||
etree.SubElement(metadata, "meta", name="calibre:timestamp",
|
||||
content='{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(
|
||||
d=book.timestamp),
|
||||
nsmap=NSMAP)
|
||||
etree.SubElement(metadata, "meta", name="calibre:title_sort",
|
||||
content=book.sort,
|
||||
nsmap=NSMAP)
|
||||
sequence = 0
|
||||
for cc in custom_columns:
|
||||
value = None
|
||||
extra = None
|
||||
cc_entry = getattr(book, "custom_column_" + str(cc.id))
|
||||
if cc_entry.__len__():
|
||||
value = [c.value for c in cc_entry] if cc.is_multiple else cc_entry[0].value
|
||||
extra = cc_entry[0].extra if hasattr(cc_entry[0], "extra") else None
|
||||
etree.SubElement(metadata, "meta", name="calibre:user_metadata:#{}".format(cc.label),
|
||||
content=cc.to_json(value, extra, sequence),
|
||||
nsmap=NSMAP)
|
||||
sequence += 1
|
||||
|
||||
# generate guide element and all sub elements of it
|
||||
# Title is translated from default export language
|
||||
guide = etree.SubElement(package, "guide")
|
||||
etree.SubElement(guide, "reference", type="cover", title=translated_cover_name, href="cover.jpg")
|
||||
|
||||
return package
|
||||
|
||||
def replace_metadata(tree, package):
|
||||
rep_element = tree.xpath('/pkg:package/pkg:metadata', namespaces=default_ns)[0]
|
||||
new_element = package.xpath('//metadata', namespaces=default_ns)[0]
|
||||
tree.replace(rep_element, new_element)
|
||||
return etree.tostring(tree,
|
||||
xml_declaration=True,
|
||||
encoding='utf-8',
|
||||
pretty_print=True).decode('utf-8')
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2023 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from tempfile import gettempdir
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def get_temp_dir():
|
||||
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
return tmp_dir
|
||||
|
||||
|
||||
def del_temp_dir():
|
||||
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||
shutil.rmtree(tmp_dir)
|
@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2020 mmonkey
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from . import logger
|
||||
from .constants import CACHE_DIR
|
||||
from os import makedirs, remove
|
||||
from os.path import isdir, isfile, join
|
||||
from shutil import rmtree
|
||||
|
||||
|
||||
class FileSystem:
|
||||
_instance = None
|
||||
_cache_dir = CACHE_DIR
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(FileSystem, cls).__new__(cls)
|
||||
cls.log = logger.create()
|
||||
return cls._instance
|
||||
|
||||
def get_cache_dir(self, cache_type=None):
|
||||
if not isdir(self._cache_dir):
|
||||
try:
|
||||
makedirs(self._cache_dir)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to create path {self._cache_dir} (Permission denied).')
|
||||
raise
|
||||
|
||||
path = join(self._cache_dir, cache_type)
|
||||
if cache_type and not isdir(path):
|
||||
try:
|
||||
makedirs(path)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to create path {path} (Permission denied).')
|
||||
raise
|
||||
|
||||
return path if cache_type else self._cache_dir
|
||||
|
||||
def get_cache_file_dir(self, filename, cache_type=None):
|
||||
path = join(self.get_cache_dir(cache_type), filename[:2])
|
||||
if not isdir(path):
|
||||
try:
|
||||
makedirs(path)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to create path {path} (Permission denied).')
|
||||
raise
|
||||
|
||||
return path
|
||||
|
||||
def get_cache_file_path(self, filename, cache_type=None):
|
||||
return join(self.get_cache_file_dir(filename, cache_type), filename) if filename else None
|
||||
|
||||
def get_cache_file_exists(self, filename, cache_type=None):
|
||||
path = self.get_cache_file_path(filename, cache_type)
|
||||
return isfile(path)
|
||||
|
||||
def delete_cache_dir(self, cache_type=None):
|
||||
if not cache_type and isdir(self._cache_dir):
|
||||
try:
|
||||
rmtree(self._cache_dir)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to delete path {self._cache_dir} (Permission denied).')
|
||||
raise
|
||||
|
||||
path = join(self._cache_dir, cache_type)
|
||||
if cache_type and isdir(path):
|
||||
try:
|
||||
rmtree(path)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to delete path {path} (Permission denied).')
|
||||
raise
|
||||
|
||||
def delete_cache_file(self, filename, cache_type=None):
|
||||
path = self.get_cache_file_path(filename, cache_type)
|
||||
if isfile(path):
|
||||
try:
|
||||
remove(path)
|
||||
except OSError:
|
||||
self.log.info(f'Failed to delete path {path} (Permission denied).')
|
||||
raise
|
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2022 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gevent.pywsgi import WSGIHandler
|
||||
|
||||
class MyWSGIHandler(WSGIHandler):
|
||||
def get_environ(self):
|
||||
env = super().get_environ()
|
||||
path, __ = self.path.split('?', 1) if '?' in self.path else (self.path, '')
|
||||
env['RAW_URI'] = path
|
||||
return env
|
||||
|
||||
|
@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2012-2022 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from . import create_app, limiter
|
||||
from .jinjia import jinjia
|
||||
from .remotelogin import remotelogin
|
||||
from flask import request
|
||||
|
||||
|
||||
def request_username():
|
||||
return request.authorization.username
|
||||
|
||||
def main():
|
||||
app = create_app()
|
||||
|
||||
from .web import web
|
||||
from .opds import opds
|
||||
from .admin import admi
|
||||
from .gdrive import gdrive
|
||||
from .editbooks import editbook
|
||||
from .about import about
|
||||
from .search import search
|
||||
from .search_metadata import meta
|
||||
from .shelf import shelf
|
||||
from .tasks_status import tasks
|
||||
from .error_handler import init_errorhandler
|
||||
try:
|
||||
from .kobo import kobo, get_kobo_activated
|
||||
from .kobo_auth import kobo_auth
|
||||
from flask_limiter.util import get_remote_address
|
||||
kobo_available = get_kobo_activated()
|
||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||
kobo_available = False
|
||||
|
||||
try:
|
||||
from .oauth_bb import oauth
|
||||
oauth_available = True
|
||||
except ImportError:
|
||||
oauth_available = False
|
||||
|
||||
from . import web_server
|
||||
init_errorhandler()
|
||||
|
||||
app.register_blueprint(search)
|
||||
app.register_blueprint(tasks)
|
||||
app.register_blueprint(web)
|
||||
app.register_blueprint(opds)
|
||||
limiter.limit("3/minute",key_func=request_username)(opds)
|
||||
app.register_blueprint(jinjia)
|
||||
app.register_blueprint(about)
|
||||
app.register_blueprint(shelf)
|
||||
app.register_blueprint(admi)
|
||||
app.register_blueprint(remotelogin)
|
||||
app.register_blueprint(meta)
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
success = web_server.start()
|
||||
sys.exit(0 if success else 1)
|
@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2022 quarz12
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import concurrent.futures
|
||||
import requests
|
||||
from bs4 import BeautifulSoup as BS # requirement
|
||||
from typing import List, Optional
|
||||
|
||||
try:
|
||||
import cchardet #optional for better speed
|
||||
except ImportError:
|
||||
pass
|
||||
from cps import logger
|
||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||
import cps.logger as logger
|
||||
|
||||
#from time import time
|
||||
from operator import itemgetter
|
||||
log = logger.create()
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
class Amazon(Metadata):
|
||||
__name__ = "Amazon"
|
||||
__id__ = "amazon"
|
||||
headers = {'upgrade-insecure-requests': '1',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||
'sec-gpc': '1',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-encoding': 'gzip, deflate, br',
|
||||
'accept-language': 'en-US,en;q=0.9'}
|
||||
session = requests.Session()
|
||||
session.headers=headers
|
||||
|
||||
def search(
|
||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||
) -> Optional[List[MetaRecord]]:
|
||||
#timer=time()
|
||||
def inner(link, index) -> [dict, int]:
|
||||
with self.session as session:
|
||||
try:
|
||||
r = session.get(f"https://www.amazon.com/{link}")
|
||||
r.raise_for_status()
|
||||
except Exception as ex:
|
||||
log.warning(ex)
|
||||
return None
|
||||
long_soup = BS(r.text, "lxml") #~4sec :/
|
||||
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
||||
if soup2 is None:
|
||||
return None
|
||||
try:
|
||||
match = MetaRecord(
|
||||
title = "",
|
||||
authors = "",
|
||||
source=MetaSourceInfo(
|
||||
id=self.__id__,
|
||||
description="Amazon Books",
|
||||
link="https://amazon.com/"
|
||||
),
|
||||
url = f"https://www.amazon.com{link}",
|
||||
#the more searches the slower, these are too hard to find in reasonable time or might not even exist
|
||||
publisher= "", # very unreliable
|
||||
publishedDate= "", # very unreliable
|
||||
id = None, # ?
|
||||
tags = [] # dont exist on amazon
|
||||
)
|
||||
|
||||
try:
|
||||
match.description = "\n".join(
|
||||
soup2.find("div", attrs={"data-feature-name": "bookDescription"}).stripped_strings)\
|
||||
.replace("\xa0"," ")[:-9].strip().strip("\n")
|
||||
except (AttributeError, TypeError):
|
||||
return None # if there is no description it is not a book and therefore should be ignored
|
||||
try:
|
||||
match.title = soup2.find("span", attrs={"id": "productTitle"}).text
|
||||
except (AttributeError, TypeError):
|
||||
match.title = ""
|
||||
try:
|
||||
match.authors = [next(
|
||||
filter(lambda i: i != " " and i != "\n" and not i.startswith("{"),
|
||||
x.findAll(string=True))).strip()
|
||||
for x in soup2.findAll("span", attrs={"class": "author"})]
|
||||
except (AttributeError, TypeError, StopIteration):
|
||||
match.authors = ""
|
||||
try:
|
||||
match.rating = int(
|
||||
soup2.find("span", class_="a-icon-alt").text.split(" ")[0].split(".")[
|
||||
0]) # first number in string
|
||||
except (AttributeError, ValueError):
|
||||
match.rating = 0
|
||||
try:
|
||||
match.cover = soup2.find("img", attrs={"class": "a-dynamic-image frontImage"})["src"]
|
||||
except (AttributeError, TypeError):
|
||||
match.cover = ""
|
||||
return match, index
|
||||
except Exception as e:
|
||||
log.error_or_exception(e)
|
||||
return None
|
||||
|
||||
val = list()
|
||||
if self.active:
|
||||
try:
|
||||
results = self.session.get(
|
||||
f"https://www.amazon.com/s?k={query.replace(' ', '+')}&i=digital-text&sprefix={query.replace(' ', '+')}"
|
||||
f"%2Cdigital-text&ref=nb_sb_noss",
|
||||
headers=self.headers)
|
||||
results.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
log.error_or_exception(e)
|
||||
return []
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return []
|
||||
soup = BS(results.text, 'html.parser')
|
||||
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
||||
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:5])}
|
||||
val = list(map(lambda x : x.result() ,concurrent.futures.as_completed(fut)))
|
||||
result = list(filter(lambda x: x, val))
|
||||
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
|
@ -0,0 +1,259 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2022 xlivevil
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import re
|
||||
from concurrent import futures
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
from html2text import HTML2Text
|
||||
from lxml import etree
|
||||
|
||||
from cps import logger
|
||||
from cps.services.Metadata import Metadata, MetaRecord, MetaSourceInfo
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def html2text(html: str) -> str:
|
||||
|
||||
h2t = HTML2Text()
|
||||
h2t.body_width = 0
|
||||
h2t.single_line_break = True
|
||||
h2t.emphasis_mark = "*"
|
||||
return h2t.handle(html)
|
||||
|
||||
|
||||
class Douban(Metadata):
|
||||
__name__ = "豆瓣"
|
||||
__id__ = "douban"
|
||||
DESCRIPTION = "豆瓣"
|
||||
META_URL = "https://book.douban.com/"
|
||||
SEARCH_JSON_URL = "https://www.douban.com/j/search"
|
||||
SEARCH_URL = "https://www.douban.com/search"
|
||||
|
||||
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
|
||||
AUTHORS_PATTERN = re.compile(r"作者|译者")
|
||||
PUBLISHER_PATTERN = re.compile(r"出版社")
|
||||
SUBTITLE_PATTERN = re.compile(r"副标题")
|
||||
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
|
||||
SERIES_PATTERN = re.compile(r"丛书")
|
||||
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
|
||||
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
|
||||
|
||||
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
|
||||
COVER_XPATH = "//a[@class='nbg']"
|
||||
INFO_XPATH = "//*[@id='info']//span[@class='pl']"
|
||||
TAGS_XPATH = "//a[contains(@class, 'tag')]"
|
||||
DESCRIPTION_XPATH = "//div[@id='link-report']//div[@class='intro']"
|
||||
RATING_XPATH = "//div[@class='rating_self clearfix']/strong"
|
||||
|
||||
session = requests.Session()
|
||||
session.headers = {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
||||
}
|
||||
|
||||
def search(self,
|
||||
query: str,
|
||||
generic_cover: str = "",
|
||||
locale: str = "en") -> List[MetaRecord]:
|
||||
val = []
|
||||
if self.active:
|
||||
log.debug(f"start searching {query} on douban")
|
||||
if title_tokens := list(
|
||||
self.get_title_tokens(query, strip_joiners=False)):
|
||||
query = "+".join(title_tokens)
|
||||
|
||||
book_id_list = self._get_book_id_list_from_html(query)
|
||||
|
||||
if not book_id_list:
|
||||
log.debug("No search results in Douban")
|
||||
return []
|
||||
|
||||
with futures.ThreadPoolExecutor(
|
||||
max_workers=5, thread_name_prefix='douban') as executor:
|
||||
|
||||
fut = [
|
||||
executor.submit(self._parse_single_book, book_id,
|
||||
generic_cover) for book_id in book_id_list
|
||||
]
|
||||
|
||||
val = [
|
||||
future.result() for future in futures.as_completed(fut)
|
||||
if future.result()
|
||||
]
|
||||
|
||||
return val
|
||||
|
||||
def _get_book_id_list_from_html(self, query: str) -> List[str]:
|
||||
try:
|
||||
r = self.session.get(self.SEARCH_URL,
|
||||
params={
|
||||
"cat": 1001,
|
||||
"q": query
|
||||
})
|
||||
r.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return []
|
||||
|
||||
html = etree.HTML(r.content.decode("utf8"))
|
||||
result_list = html.xpath(self.COVER_XPATH)
|
||||
|
||||
return [
|
||||
self.ID_PATTERN.search(item.get("onclick")).group("id")
|
||||
for item in result_list[:10]
|
||||
if self.ID_PATTERN.search(item.get("onclick"))
|
||||
]
|
||||
|
||||
def _get_book_id_list_from_json(self, query: str) -> List[str]:
|
||||
try:
|
||||
r = self.session.get(self.SEARCH_JSON_URL,
|
||||
params={
|
||||
"cat": 1001,
|
||||
"q": query
|
||||
})
|
||||
r.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return []
|
||||
|
||||
results = r.json()
|
||||
if results["total"] == 0:
|
||||
return []
|
||||
|
||||
return [
|
||||
self.ID_PATTERN.search(item).group("id")
|
||||
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
||||
]
|
||||
|
||||
def _parse_single_book(self,
|
||||
id: str,
|
||||
generic_cover: str = "") -> Optional[MetaRecord]:
|
||||
url = f"https://book.douban.com/subject/{id}/"
|
||||
log.debug(f"start parsing {url}")
|
||||
|
||||
try:
|
||||
r = self.session.get(url)
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
|
||||
match = MetaRecord(
|
||||
id=id,
|
||||
title="",
|
||||
authors=[],
|
||||
url=url,
|
||||
source=MetaSourceInfo(
|
||||
id=self.__id__,
|
||||
description=self.DESCRIPTION,
|
||||
link=self.META_URL,
|
||||
),
|
||||
)
|
||||
|
||||
decode_content = r.content.decode("utf8")
|
||||
html = etree.HTML(decode_content)
|
||||
|
||||
match.title = html.xpath(self.TITTLE_XPATH)[0].text
|
||||
match.cover = html.xpath(
|
||||
self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
||||
try:
|
||||
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
|
||||
except Exception:
|
||||
rating_num = 0
|
||||
match.rating = int(-1 * rating_num // 2 * -1) if rating_num else 0
|
||||
|
||||
tag_elements = html.xpath(self.TAGS_XPATH)
|
||||
if len(tag_elements):
|
||||
match.tags = [tag_element.text for tag_element in tag_elements]
|
||||
else:
|
||||
match.tags = self._get_tags(decode_content)
|
||||
|
||||
description_element = html.xpath(self.DESCRIPTION_XPATH)
|
||||
if len(description_element):
|
||||
match.description = html2text(
|
||||
etree.tostring(description_element[-1]).decode("utf8"))
|
||||
|
||||
info = html.xpath(self.INFO_XPATH)
|
||||
|
||||
for element in info:
|
||||
text = element.text
|
||||
if self.AUTHORS_PATTERN.search(text):
|
||||
next_element = element.getnext()
|
||||
while next_element is not None and next_element.tag != "br":
|
||||
match.authors.append(next_element.text)
|
||||
next_element = next_element.getnext()
|
||||
elif self.PUBLISHER_PATTERN.search(text):
|
||||
if publisher := element.tail.strip():
|
||||
match.publisher = publisher
|
||||
else:
|
||||
match.publisher = element.getnext().text
|
||||
elif self.SUBTITLE_PATTERN.search(text):
|
||||
match.title = f'{match.title}:{element.tail.strip()}'
|
||||
elif self.PUBLISHED_DATE_PATTERN.search(text):
|
||||
match.publishedDate = self._clean_date(element.tail.strip())
|
||||
elif self.SERIES_PATTERN.search(text):
|
||||
match.series = element.getnext().text
|
||||
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
|
||||
match.identifiers[i_type.group()] = element.tail.strip()
|
||||
|
||||
return match
|
||||
|
||||
def _clean_date(self, date: str) -> str:
|
||||
"""
|
||||
Clean up the date string to be in the format YYYY-MM-DD
|
||||
|
||||
Examples of possible patterns:
|
||||
'2014-7-16', '1988年4月', '1995-04', '2021-8', '2020-12-1', '1996年',
|
||||
'1972', '2004/11/01', '1959年3月北京第1版第1印'
|
||||
"""
|
||||
year = date[:4]
|
||||
moon = "01"
|
||||
day = "01"
|
||||
|
||||
if len(date) > 5:
|
||||
digit = []
|
||||
ls = []
|
||||
for i in range(5, len(date)):
|
||||
if date[i].isdigit():
|
||||
digit.append(date[i])
|
||||
elif digit:
|
||||
ls.append("".join(digit) if len(digit) ==
|
||||
2 else f"0{digit[0]}")
|
||||
digit = []
|
||||
if digit:
|
||||
ls.append("".join(digit) if len(digit) ==
|
||||
2 else f"0{digit[0]}")
|
||||
|
||||
moon = ls[0]
|
||||
if len(ls) > 1:
|
||||
day = ls[1]
|
||||
|
||||
return f"{year}-{moon}-{day}"
|
||||
|
||||
def _get_tags(self, text: str) -> List[str]:
|
||||
tags = []
|
||||
if criteria := self.CRITERIA_PATTERN.search(text):
|
||||
tags.extend(
|
||||
item.replace('7:', '') for item in criteria.group().split('|')
|
||||
if item.startswith('7:'))
|
||||
|
||||
return tags
|
@ -0,0 +1,357 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2021 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from dateutil import parser
|
||||
from html2text import HTML2Text
|
||||
from lxml.html import HtmlElement, fromstring, tostring
|
||||
from markdown2 import Markdown
|
||||
|
||||
from cps import logger
|
||||
from cps.isoLanguages import get_language_name
|
||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||
|
||||
log = logger.create()
|
||||
|
||||
SYMBOLS_TO_TRANSLATE = (
|
||||
"öÖüÜóÓőŐúÚéÉáÁűŰíÍąĄćĆęĘłŁńŃóÓśŚźŹżŻ",
|
||||
"oOuUoOoOuUeEaAuUiIaAcCeElLnNoOsSzZzZ",
|
||||
)
|
||||
SYMBOL_TRANSLATION_MAP = dict(
|
||||
[(ord(a), ord(b)) for (a, b) in zip(*SYMBOLS_TO_TRANSLATE)]
|
||||
)
|
||||
|
||||
|
||||
def get_int_or_float(value: str) -> Union[int, float]:
|
||||
number_as_float = float(value)
|
||||
number_as_int = int(number_as_float)
|
||||
return number_as_int if number_as_float == number_as_int else number_as_float
|
||||
|
||||
|
||||
def strip_accents(s: Optional[str]) -> Optional[str]:
|
||||
return s.translate(SYMBOL_TRANSLATION_MAP) if s is not None else s
|
||||
|
||||
|
||||
def sanitize_comments_html(html: str) -> str:
|
||||
text = html2text(html)
|
||||
md = Markdown()
|
||||
html = md.convert(text)
|
||||
return html
|
||||
|
||||
|
||||
def html2text(html: str) -> str:
|
||||
# replace <u> tags with <span> as <u> becomes emphasis in html2text
|
||||
if isinstance(html, bytes):
|
||||
html = html.decode("utf-8")
|
||||
html = re.sub(
|
||||
r"<\s*(?P<solidus>/?)\s*[uU]\b(?P<rest>[^>]*)>",
|
||||
r"<\g<solidus>span\g<rest>>",
|
||||
html,
|
||||
)
|
||||
h2t = HTML2Text()
|
||||
h2t.body_width = 0
|
||||
h2t.single_line_break = True
|
||||
h2t.emphasis_mark = "*"
|
||||
return h2t.handle(html)
|
||||
|
||||
|
||||
class LubimyCzytac(Metadata):
|
||||
__name__ = "LubimyCzytac.pl"
|
||||
__id__ = "lubimyczytac"
|
||||
|
||||
BASE_URL = "https://lubimyczytac.pl"
|
||||
|
||||
BOOK_SEARCH_RESULT_XPATH = (
|
||||
"*//div[@class='listSearch']//div[@class='authorAllBooks__single']"
|
||||
)
|
||||
SINGLE_BOOK_RESULT_XPATH = ".//div[contains(@class,'authorAllBooks__singleText')]"
|
||||
TITLE_PATH = "/div/a[contains(@class,'authorAllBooks__singleTextTitle')]"
|
||||
TITLE_TEXT_PATH = f"{TITLE_PATH}//text()"
|
||||
URL_PATH = f"{TITLE_PATH}/@href"
|
||||
AUTHORS_PATH = "/div/a[contains(@href,'autor')]//text()"
|
||||
|
||||
SIBLINGS = "/following-sibling::dd"
|
||||
|
||||
CONTAINER = "//section[@class='container book']"
|
||||
PUBLISHER = f"{CONTAINER}//dt[contains(text(),'Wydawnictwo:')]{SIBLINGS}/a/text()"
|
||||
LANGUAGES = f"{CONTAINER}//dt[contains(text(),'Język:')]{SIBLINGS}/text()"
|
||||
DESCRIPTION = f"{CONTAINER}//div[@class='collapse-content']"
|
||||
SERIES = f"{CONTAINER}//span/a[contains(@href,'/cykl/')]/text()"
|
||||
TRANSLATOR = f"{CONTAINER}//dt[contains(text(),'Tłumacz:')]{SIBLINGS}/a/text()"
|
||||
|
||||
DETAILS = "//div[@id='book-details']"
|
||||
PUBLISH_DATE = "//dt[contains(@title,'Data pierwszego wydania"
|
||||
FIRST_PUBLISH_DATE = f"{DETAILS}{PUBLISH_DATE} oryginalnego')]{SIBLINGS}[1]/text()"
|
||||
FIRST_PUBLISH_DATE_PL = f"{DETAILS}{PUBLISH_DATE} polskiego')]{SIBLINGS}[1]/text()"
|
||||
TAGS = "//a[contains(@href,'/ksiazki/t/')]/text()" # "//nav[@aria-label='breadcrumbs']//a[contains(@href,'/ksiazki/k/')]/span/text()"
|
||||
|
||||
|
||||
RATING = "//meta[@property='books:rating:value']/@content"
|
||||
COVER = "//meta[@property='og:image']/@content"
|
||||
ISBN = "//meta[@property='books:isbn']/@content"
|
||||
META_TITLE = "//meta[@property='og:description']/@content"
|
||||
|
||||
SUMMARY = "//script[@type='application/ld+json']//text()"
|
||||
|
||||
def search(
|
||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||
) -> Optional[List[MetaRecord]]:
|
||||
if self.active:
|
||||
try:
|
||||
result = requests.get(self._prepare_query(title=query))
|
||||
result.raise_for_status()
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
root = fromstring(result.text)
|
||||
lc_parser = LubimyCzytacParser(root=root, metadata=self)
|
||||
matches = lc_parser.parse_search_results()
|
||||
if matches:
|
||||
with ThreadPool(processes=10) as pool:
|
||||
final_matches = pool.starmap(
|
||||
lc_parser.parse_single_book,
|
||||
[(match, generic_cover, locale) for match in matches],
|
||||
)
|
||||
return final_matches
|
||||
return matches
|
||||
|
||||
def _prepare_query(self, title: str) -> str:
|
||||
query = ""
|
||||
characters_to_remove = "\?()\/"
|
||||
pattern = "[" + characters_to_remove + "]"
|
||||
title = re.sub(pattern, "", title)
|
||||
title = title.replace("_", " ")
|
||||
if '"' in title or ",," in title:
|
||||
title = title.split('"')[0].split(",,")[0]
|
||||
|
||||
if "/" in title:
|
||||
title_tokens = [
|
||||
token for token in title.lower().split(" ") if len(token) > 1
|
||||
]
|
||||
else:
|
||||
title_tokens = list(self.get_title_tokens(title, strip_joiners=False))
|
||||
if title_tokens:
|
||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||
query = query + "%20".join(tokens)
|
||||
if not query:
|
||||
return ""
|
||||
return f"{LubimyCzytac.BASE_URL}/szukaj/ksiazki?phrase={query}"
|
||||
|
||||
|
||||
class LubimyCzytacParser:
|
||||
PAGES_TEMPLATE = "<p id='strony'>Książka ma {0} stron(y).</p>"
|
||||
TRANSLATOR_TEMPLATE = "<p id='translator'>Tłumacz: {0}</p>"
|
||||
PUBLISH_DATE_TEMPLATE = "<p id='pierwsze_wydanie'>Data pierwszego wydania: {0}</p>"
|
||||
PUBLISH_DATE_PL_TEMPLATE = (
|
||||
"<p id='pierwsze_wydanie'>Data pierwszego wydania w Polsce: {0}</p>"
|
||||
)
|
||||
|
||||
def __init__(self, root: HtmlElement, metadata: Metadata) -> None:
|
||||
self.root = root
|
||||
self.metadata = metadata
|
||||
|
||||
def parse_search_results(self) -> List[MetaRecord]:
|
||||
matches = []
|
||||
results = self.root.xpath(LubimyCzytac.BOOK_SEARCH_RESULT_XPATH)
|
||||
for result in results:
|
||||
title = self._parse_xpath_node(
|
||||
root=result,
|
||||
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
|
||||
f"{LubimyCzytac.TITLE_TEXT_PATH}",
|
||||
)
|
||||
|
||||
book_url = self._parse_xpath_node(
|
||||
root=result,
|
||||
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
|
||||
f"{LubimyCzytac.URL_PATH}",
|
||||
)
|
||||
authors = self._parse_xpath_node(
|
||||
root=result,
|
||||
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
|
||||
f"{LubimyCzytac.AUTHORS_PATH}",
|
||||
take_first=False,
|
||||
)
|
||||
if not all([title, book_url, authors]):
|
||||
continue
|
||||
matches.append(
|
||||
MetaRecord(
|
||||
id=book_url.replace(f"/ksiazka/", "").split("/")[0],
|
||||
title=title,
|
||||
authors=[strip_accents(author) for author in authors],
|
||||
url=LubimyCzytac.BASE_URL + book_url,
|
||||
source=MetaSourceInfo(
|
||||
id=self.metadata.__id__,
|
||||
description=self.metadata.__name__,
|
||||
link=LubimyCzytac.BASE_URL,
|
||||
),
|
||||
)
|
||||
)
|
||||
return matches
|
||||
|
||||
def parse_single_book(
|
||||
self, match: MetaRecord, generic_cover: str, locale: str
|
||||
) -> MetaRecord:
|
||||
try:
|
||||
response = requests.get(match.url)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
self.root = fromstring(response.text)
|
||||
match.cover = self._parse_cover(generic_cover=generic_cover)
|
||||
match.description = self._parse_description()
|
||||
match.languages = self._parse_languages(locale=locale)
|
||||
match.publisher = self._parse_publisher()
|
||||
match.publishedDate = self._parse_from_summary(attribute_name="datePublished")
|
||||
match.rating = self._parse_rating()
|
||||
match.series, match.series_index = self._parse_series()
|
||||
match.tags = self._parse_tags()
|
||||
match.identifiers = {
|
||||
"isbn": self._parse_isbn(),
|
||||
"lubimyczytac": match.id,
|
||||
}
|
||||
return match
|
||||
|
||||
def _parse_xpath_node(
|
||||
self,
|
||||
xpath: str,
|
||||
root: HtmlElement = None,
|
||||
take_first: bool = True,
|
||||
strip_element: bool = True,
|
||||
) -> Optional[Union[str, List[str]]]:
|
||||
root = root if root is not None else self.root
|
||||
node = root.xpath(xpath)
|
||||
if not node:
|
||||
return None
|
||||
return (
|
||||
(node[0].strip() if strip_element else node[0])
|
||||
if take_first
|
||||
else [x.strip() for x in node]
|
||||
)
|
||||
|
||||
def _parse_cover(self, generic_cover) -> Optional[str]:
|
||||
return (
|
||||
self._parse_xpath_node(xpath=LubimyCzytac.COVER, take_first=True)
|
||||
or generic_cover
|
||||
)
|
||||
|
||||
def _parse_publisher(self) -> Optional[str]:
|
||||
return self._parse_xpath_node(xpath=LubimyCzytac.PUBLISHER, take_first=True)
|
||||
|
||||
def _parse_languages(self, locale: str) -> List[str]:
|
||||
languages = list()
|
||||
lang = self._parse_xpath_node(xpath=LubimyCzytac.LANGUAGES, take_first=True)
|
||||
if lang:
|
||||
if "polski" in lang:
|
||||
languages.append("pol")
|
||||
if "angielski" in lang:
|
||||
languages.append("eng")
|
||||
return [get_language_name(locale, language) for language in languages]
|
||||
|
||||
def _parse_series(self) -> Tuple[Optional[str], Optional[Union[float, int]]]:
|
||||
series_index = 0
|
||||
series = self._parse_xpath_node(xpath=LubimyCzytac.SERIES, take_first=True)
|
||||
if series:
|
||||
if "tom " in series:
|
||||
series_name, series_info = series.split(" (tom ", 1)
|
||||
series_info = series_info.replace(" ", "").replace(")", "")
|
||||
# Check if book is not a bundle, i.e. chapter 1-3
|
||||
if "-" in series_info:
|
||||
series_info = series_info.split("-", 1)[0]
|
||||
if series_info.replace(".", "").isdigit() is True:
|
||||
series_index = get_int_or_float(series_info)
|
||||
return series_name, series_index
|
||||
return None, None
|
||||
|
||||
def _parse_tags(self) -> List[str]:
|
||||
tags = self._parse_xpath_node(xpath=LubimyCzytac.TAGS, take_first=False)
|
||||
return [
|
||||
strip_accents(w.replace(", itd.", " itd."))
|
||||
for w in tags
|
||||
if isinstance(w, str)
|
||||
]
|
||||
|
||||
def _parse_from_summary(self, attribute_name: str) -> Optional[str]:
|
||||
value = None
|
||||
summary_text = self._parse_xpath_node(xpath=LubimyCzytac.SUMMARY)
|
||||
if summary_text:
|
||||
data = json.loads(summary_text)
|
||||
value = data.get(attribute_name)
|
||||
return value.strip() if value is not None else value
|
||||
|
||||
def _parse_rating(self) -> Optional[str]:
|
||||
rating = self._parse_xpath_node(xpath=LubimyCzytac.RATING)
|
||||
return round(float(rating.replace(",", ".")) / 2) if rating else rating
|
||||
|
||||
def _parse_date(self, xpath="first_publish") -> Optional[datetime.datetime]:
|
||||
options = {
|
||||
"first_publish": LubimyCzytac.FIRST_PUBLISH_DATE,
|
||||
"first_publish_pl": LubimyCzytac.FIRST_PUBLISH_DATE_PL,
|
||||
}
|
||||
date = self._parse_xpath_node(xpath=options.get(xpath))
|
||||
return parser.parse(date) if date else None
|
||||
|
||||
def _parse_isbn(self) -> Optional[str]:
|
||||
return self._parse_xpath_node(xpath=LubimyCzytac.ISBN)
|
||||
|
||||
def _parse_description(self) -> str:
|
||||
description = ""
|
||||
description_node = self._parse_xpath_node(
|
||||
xpath=LubimyCzytac.DESCRIPTION, strip_element=False
|
||||
)
|
||||
if description_node is not None:
|
||||
for source in self.root.xpath('//p[@class="source"]'):
|
||||
source.getparent().remove(source)
|
||||
description = tostring(description_node, method="html")
|
||||
description = sanitize_comments_html(description)
|
||||
|
||||
else:
|
||||
description_node = self._parse_xpath_node(xpath=LubimyCzytac.META_TITLE)
|
||||
if description_node is not None:
|
||||
description = description_node
|
||||
description = sanitize_comments_html(description)
|
||||
description = self._add_extra_info_to_description(description=description)
|
||||
return description
|
||||
|
||||
def _add_extra_info_to_description(self, description: str) -> str:
|
||||
pages = self._parse_from_summary(attribute_name="numberOfPages")
|
||||
if pages:
|
||||
description += LubimyCzytacParser.PAGES_TEMPLATE.format(pages)
|
||||
|
||||
first_publish_date = self._parse_date()
|
||||
if first_publish_date:
|
||||
description += LubimyCzytacParser.PUBLISH_DATE_TEMPLATE.format(
|
||||
first_publish_date.strftime("%d.%m.%Y")
|
||||
)
|
||||
|
||||
first_publish_date_pl = self._parse_date(xpath="first_publish_pl")
|
||||
if first_publish_date_pl:
|
||||
description += LubimyCzytacParser.PUBLISH_DATE_PL_TEMPLATE.format(
|
||||
first_publish_date_pl.strftime("%d.%m.%Y")
|
||||
)
|
||||
translator = self._parse_xpath_node(xpath=LubimyCzytac.TRANSLATOR)
|
||||
if translator:
|
||||
description += LubimyCzytacParser.TRANSLATOR_TEMPLATE.format(translator)
|
||||
|
||||
|
||||
return description
|
@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2020 mmonkey
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from . import config, constants
|
||||
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
||||
from .tasks.database import TaskReconnectDatabase
|
||||
from .tasks.tempFolder import TaskDeleteTempFolder
|
||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.metadata_backup import TaskBackupMetadata
|
||||
|
||||
def get_scheduled_tasks(reconnect=True):
|
||||
tasks = list()
|
||||
# Reconnect Calibre database (metadata.db) based on config.schedule_reconnect
|
||||
if reconnect:
|
||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||
|
||||
# Delete temp folder
|
||||
tasks.append([lambda: TaskDeleteTempFolder(), 'delete temp', True])
|
||||
|
||||
# Generate metadata.opf file for each changed book
|
||||
if config.schedule_metadata_backup:
|
||||
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
|
||||
|
||||
# Generate all missing book cover thumbnails
|
||||
if config.schedule_generate_book_covers:
|
||||
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
|
||||
tasks.append([lambda: TaskGenerateCoverThumbnails(), 'generate book covers', False])
|
||||
|
||||
# Generate all missing series thumbnails
|
||||
if config.schedule_generate_series_covers:
|
||||
tasks.append([lambda: TaskGenerateSeriesThumbnails(), 'generate book covers', False])
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def end_scheduled_tasks():
|
||||
worker = WorkerThread.get_instance()
|
||||
for __, __, __, task, __ in worker.tasks:
|
||||
if task.scheduled and task.is_cancellable:
|
||||
worker.end_task(task.id)
|
||||
|
||||
|
||||
def register_scheduled_tasks(reconnect=True):
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
if scheduler:
|
||||
# Remove all existing jobs
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
start = config.schedule_start_time
|
||||
duration = config.schedule_duration
|
||||
|
||||
# Register scheduled tasks
|
||||
timezone_info = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
|
||||
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start,
|
||||
timezone=timezone_info))
|
||||
end_time = calclulate_end_time(start, duration)
|
||||
scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute,
|
||||
timezone=timezone_info),
|
||||
name="end scheduled task")
|
||||
|
||||
# Kick-off tasks, if they should currently be running
|
||||
if should_task_be_running(start, duration):
|
||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(reconnect))
|
||||
|
||||
|
||||
def register_startup_tasks():
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
if scheduler:
|
||||
start = config.schedule_start_time
|
||||
duration = config.schedule_duration
|
||||
|
||||
# Run scheduled tasks immediately for development and testing
|
||||
# Ignore tasks that should currently be running, as these will be added when registering scheduled tasks
|
||||
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, duration):
|
||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
||||
else:
|
||||
scheduler.schedule_tasks_immediately(tasks=[[lambda: TaskDeleteTempFolder(), 'delete temp', True]])
|
||||
|
||||
|
||||
def should_task_be_running(start, duration):
|
||||
now = datetime.datetime.now()
|
||||
start_time = datetime.datetime.now().replace(hour=start, minute=0, second=0, microsecond=0)
|
||||
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||
return start_time < now < end_time
|
||||
|
||||
|
||||
def calclulate_end_time(start, duration):
|
||||
start_time = datetime.datetime.now().replace(hour=start, minute=0)
|
||||
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||
|
@ -0,0 +1,403 @@
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2022 OzzieIsaacs
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, request, redirect, url_for, flash
|
||||
from flask import session as flask_session
|
||||
from flask_login import current_user
|
||||
from flask_babel import format_date
|
||||
from flask_babel import gettext as _
|
||||
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
||||
from sqlalchemy.sql.functions import coalesce
|
||||
|
||||
from . import logger, db, calibre_db, config, ub
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .render_template import render_title_template
|
||||
from .pagination import Pagination
|
||||
|
||||
search = Blueprint('search', __name__)
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
@search.route("/search", methods=["GET"])
|
||||
@login_required_if_no_ano
|
||||
def simple_search():
|
||||
term = request.args.get("query")
|
||||
if term:
|
||||
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
|
||||
else:
|
||||
return render_title_template('search.html',
|
||||
searchterm="",
|
||||
result_count=0,
|
||||
title=_("Search"),
|
||||
page="search")
|
||||
|
||||
|
||||
@search.route("/advsearch", methods=['POST'])
|
||||
@login_required_if_no_ano
|
||||
def advanced_search():
|
||||
values = dict(request.form)
|
||||
params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf', 'exclude_shelf',
|
||||
'include_language', 'exclude_language', 'include_extension', 'exclude_extension']
|
||||
for param in params:
|
||||
values[param] = list(request.form.getlist(param))
|
||||
flask_session['query'] = json.dumps(values)
|
||||
return redirect(url_for('web.books_list', data="advsearch", sort_param='stored', query=""))
|
||||
|
||||
|
||||
@search.route("/advsearch", methods=['GET'])
|
||||
@login_required_if_no_ano
|
||||
def advanced_search_form():
|
||||
# Build custom columns names
|
||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||
return render_prepare_search_form(cc)
|
||||
|
||||
|
||||
def adv_search_custom_columns(cc, term, q):
|
||||
for c in cc:
|
||||
if c.datatype == "datetime":
|
||||
custom_start = term.get('custom_column_' + str(c.id) + '_start')
|
||||
custom_end = term.get('custom_column_' + str(c.id) + '_end')
|
||||
if custom_start:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
func.datetime(db.cc_classes[c.id].value) >= func.datetime(custom_start)))
|
||||
if custom_end:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
|
||||
else:
|
||||
custom_query = term.get('custom_column_' + str(c.id))
|
||||
if custom_query != '' and custom_query is not None:
|
||||
if c.datatype == 'bool':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == (custom_query == "True")))
|
||||
elif c.datatype == 'int' or c.datatype == 'float':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == custom_query))
|
||||
elif c.datatype == 'rating':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == int(float(custom_query) * 2)))
|
||||
else:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
|
||||
return q
|
||||
|
||||
|
||||
def adv_search_language(q, include_languages_inputs, exclude_languages_inputs):
|
||||
if current_user.filter_language() != "all":
|
||||
q = q.filter(db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()))
|
||||
else:
|
||||
for language in include_languages_inputs:
|
||||
q = q.filter(db.Books.languages.any(db.Languages.id == language))
|
||||
for language in exclude_languages_inputs:
|
||||
q = q.filter(not_(db.Books.series.any(db.Languages.id == language)))
|
||||
return q
|
||||
|
||||
|
||||
def adv_search_ratings(q, rating_high, rating_low):
|
||||
if rating_high:
|
||||
rating_high = int(rating_high) * 2
|
||||
q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high))
|
||||
if rating_low:
|
||||
rating_low = int(rating_low) * 2
|
||||
q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low))
|
||||
return q
|
||||
|
||||
|
||||
def adv_search_read_status(read_status):
|
||||
if not config.config_read_column:
|
||||
if read_status == "True":
|
||||
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
|
||||
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
|
||||
else:
|
||||
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
|
||||
else:
|
||||
try:
|
||||
if read_status == "True":
|
||||
db_filter = db.cc_classes[config.config_read_column].value == True
|
||||
else:
|
||||
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
|
||||
except (KeyError, AttributeError, IndexError):
|
||||
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
|
||||
flash(_("Custom Column No.%(column)d does not exist in calibre database",
|
||||
column=config.config_read_column),
|
||||
category="error")
|
||||
return true()
|
||||
return db_filter
|
||||
|
||||
|
||||
def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
|
||||
for extension in include_extension_inputs:
|
||||
q = q.filter(db.Books.data.any(db.Data.format == extension))
|
||||
for extension in exclude_extension_inputs:
|
||||
q = q.filter(not_(db.Books.data.any(db.Data.format == extension)))
|
||||
return q
|
||||
|
||||
|
||||
def adv_search_tag(q, include_tag_inputs, exclude_tag_inputs):
|
||||
for tag in include_tag_inputs:
|
||||
q = q.filter(db.Books.tags.any(db.Tags.id == tag))
|
||||
for tag in exclude_tag_inputs:
|
||||
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
|
||||
return q
|
||||
|
||||
|
||||
def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
|
||||
for serie in include_series_inputs:
|
||||
q = q.filter(db.Books.series.any(db.Series.id == serie))
|
||||
for serie in exclude_series_inputs:
|
||||
q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
|
||||
return q
|
||||
|
||||
def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
|
||||
q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\
|
||||
.filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
|
||||
if len(include_shelf_inputs) > 0:
|
||||
q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
|
||||
return q
|
||||
|
||||
def extend_search_term(searchterm,
|
||||
author_name,
|
||||
book_title,
|
||||
publisher,
|
||||
pub_start,
|
||||
pub_end,
|
||||
tags,
|
||||
rating_high,
|
||||
rating_low,
|
||||
read_status,
|
||||
):
|
||||
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
||||
if pub_start:
|
||||
try:
|
||||
searchterm.extend([_("Published after ") +
|
||||
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
|
||||
format='medium')])
|
||||
except ValueError:
|
||||
pub_start = ""
|
||||
if pub_end:
|
||||
try:
|
||||
searchterm.extend([_("Published before ") +
|
||||
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
|
||||
format='medium')])
|
||||
except ValueError:
|
||||
pub_end = ""
|
||||
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
|
||||
for key, db_element in elements.items():
|
||||
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
|
||||
searchterm.extend(tag.name for tag in tag_names)
|
||||
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['exclude_' + key])).all()
|
||||
searchterm.extend(tag.name for tag in tag_names)
|
||||
language_names = calibre_db.session.query(db.Languages). \
|
||||
filter(db.Languages.id.in_(tags['include_language'])).all()
|
||||
if language_names:
|
||||
language_names = calibre_db.speaking_language(language_names)
|
||||
searchterm.extend(language.name for language in language_names)
|
||||
language_names = calibre_db.session.query(db.Languages). \
|
||||
filter(db.Languages.id.in_(tags['exclude_language'])).all()
|
||||
if language_names:
|
||||
language_names = calibre_db.speaking_language(language_names)
|
||||
searchterm.extend(language.name for language in language_names)
|
||||
if rating_high:
|
||||
searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
|
||||
if rating_low:
|
||||
searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
|
||||
if read_status != "Any":
|
||||
searchterm.extend([_("Read Status = '%(status)s'", status=read_status)])
|
||||
searchterm.extend(ext for ext in tags['include_extension'])
|
||||
searchterm.extend(ext for ext in tags['exclude_extension'])
|
||||
# handle custom columns
|
||||
searchterm = " + ".join(filter(None, searchterm))
|
||||
return searchterm, pub_start, pub_end
|
||||
|
||||
|
||||
def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
sort = order[0] if order else [db.Books.sort]
|
||||
pagination = None
|
||||
|
||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
||||
.outerjoin(db.Series)\
|
||||
.filter(calibre_db.common_filters(True))
|
||||
|
||||
# parse multi selects to a complete dict
|
||||
tags = dict()
|
||||
elements = ['tag', 'serie', 'shelf', 'language', 'extension']
|
||||
for element in elements:
|
||||
tags['include_' + element] = term.get('include_' + element)
|
||||
tags['exclude_' + element] = term.get('exclude_' + element)
|
||||
|
||||
author_name = term.get("author_name")
|
||||
book_title = term.get("book_title")
|
||||
publisher = term.get("publisher")
|
||||
pub_start = term.get("publishstart")
|
||||
pub_end = term.get("publishend")
|
||||
rating_low = term.get("ratinghigh")
|
||||
rating_high = term.get("ratinglow")
|
||||
description = term.get("comment")
|
||||
read_status = term.get("read_status")
|
||||
if author_name:
|
||||
author_name = author_name.strip().lower().replace(',', '|')
|
||||
if book_title:
|
||||
book_title = book_title.strip().lower()
|
||||
if publisher:
|
||||
publisher = publisher.strip().lower()
|
||||
|
||||
search_term = []
|
||||
cc_present = False
|
||||
for c in cc:
|
||||
if c.datatype == "datetime":
|
||||
column_start = term.get('custom_column_' + str(c.id) + '_start')
|
||||
column_end = term.get('custom_column_' + str(c.id) + '_end')
|
||||
if column_start:
|
||||
search_term.extend(["{} >= {}".format(c.name,
|
||||
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
|
||||
format='medium')
|
||||
)])
|
||||
cc_present = True
|
||||
if column_end:
|
||||
search_term.extend(["{} <= {}".format(c.name,
|
||||
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
||||
format='medium')
|
||||
)])
|
||||
cc_present = True
|
||||
elif term.get('custom_column_' + str(c.id)):
|
||||
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||
cc_present = True
|
||||
|
||||
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
|
||||
or rating_high or description or cc_present or read_status != "Any":
|
||||
search_term, pub_start, pub_end = extend_search_term(search_term,
|
||||
author_name,
|
||||
book_title,
|
||||
publisher,
|
||||
pub_start,
|
||||
pub_end,
|
||||
tags,
|
||||
rating_high,
|
||||
rating_low,
|
||||
read_status)
|
||||
if author_name:
|
||||
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_name + "%")))
|
||||
if book_title:
|
||||
q = q.filter(func.lower(db.Books.title).ilike("%" + book_title + "%"))
|
||||
if pub_start:
|
||||
q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
|
||||
if pub_end:
|
||||
q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
|
||||
if read_status != "Any":
|
||||
q = q.filter(adv_search_read_status(read_status))
|
||||
if publisher:
|
||||
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
||||
q = adv_search_tag(q, tags['include_tag'], tags['exclude_tag'])
|
||||
q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie'])
|
||||
q = adv_search_shelf(q, tags['include_shelf'], tags['exclude_shelf'])
|
||||
q = adv_search_extension(q, tags['include_extension'], tags['exclude_extension'])
|
||||
q = adv_search_language(q, tags['include_language'], tags['exclude_language'])
|
||||
q = adv_search_ratings(q, rating_high, rating_low)
|
||||
|
||||
if description:
|
||||
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
|
||||
|
||||
# search custom columns
|
||||
try:
|
||||
q = adv_search_custom_columns(cc, term, q)
|
||||
except AttributeError as ex:
|
||||
log.debug_or_exception(ex)
|
||||
flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error")
|
||||
|
||||
q = q.order_by(*sort).all()
|
||||
flask_session['query'] = json.dumps(term)
|
||||
ub.store_combo_ids(q)
|
||||
result_count = len(q)
|
||||
if offset is not None and limit is not None:
|
||||
offset = int(offset)
|
||||
limit_all = offset + int(limit)
|
||||
pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
|
||||
else:
|
||||
offset = 0
|
||||
limit_all = result_count
|
||||
entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
|
||||
return render_title_template('search.html',
|
||||
adv_searchterm=search_term,
|
||||
pagination=pagination,
|
||||
entries=entries,
|
||||
result_count=result_count,
|
||||
title=_("Advanced Search"), page="advsearch",
|
||||
order=order[1])
|
||||
|
||||
|
||||
def render_prepare_search_form(cc):
|
||||
# prepare data for search-form
|
||||
tags = calibre_db.session.query(db.Tags)\
|
||||
.join(db.books_tags_link)\
|
||||
.join(db.Books)\
|
||||
.filter(calibre_db.common_filters()) \
|
||||
.group_by(text('books_tags_link.tag'))\
|
||||
.order_by(db.Tags.name).all()
|
||||
series = calibre_db.session.query(db.Series)\
|
||||
.join(db.books_series_link)\
|
||||
.join(db.Books)\
|
||||
.filter(calibre_db.common_filters()) \
|
||||
.group_by(text('books_series_link.series'))\
|
||||
.order_by(db.Series.name)\
|
||||
.filter(calibre_db.common_filters()).all()
|
||||
shelves = ub.session.query(ub.Shelf)\
|
||||
.filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\
|
||||
.order_by(ub.Shelf.name).all()
|
||||
extensions = calibre_db.session.query(db.Data)\
|
||||
.join(db.Books)\
|
||||
.filter(calibre_db.common_filters()) \
|
||||
.group_by(db.Data.format)\
|
||||
.order_by(db.Data.format).all()
|
||||
if current_user.filter_language() == "all":
|
||||
languages = calibre_db.speaking_language()
|
||||
else:
|
||||
languages = None
|
||||
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
|
||||
series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
|
||||
|
||||
|
||||
def render_search_results(term, offset=None, order=None, limit=None):
|
||||
if term:
|
||||
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
|
||||
entries, result_count, pagination = calibre_db.get_search_results(term,
|
||||
config,
|
||||
offset,
|
||||
order,
|
||||
limit,
|
||||
*join)
|
||||
else:
|
||||
entries = list()
|
||||
order = [None, None]
|
||||
pagination = result_count = None
|
||||
|
||||
return render_title_template('search.html',
|
||||
searchterm=term,
|
||||
pagination=pagination,
|
||||
query=term,
|
||||
adv_searchterm=term,
|
||||
entries=entries,
|
||||
result_count=result_count,
|
||||
title=_("Search"),
|
||||
page="search",
|
||||
order=order[1])
|
||||
|
||||
|
@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||
# Copyright (C) 2020 mmonkey
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import atexit
|
||||
|
||||
from .. import logger
|
||||
from .worker import WorkerThread
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
use_APScheduler = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
use_APScheduler = False
|
||||
log = logger.create()
|
||||
log.info('APScheduler not found. Unable to schedule tasks.')
|
||||
|
||||
|
||||
class BackgroundScheduler:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if not use_APScheduler:
|
||||
return False
|
||||
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BackgroundScheduler, cls).__new__(cls)
|
||||
cls.log = logger.create()
|
||||
cls.scheduler = BScheduler()
|
||||
cls.scheduler.start()
|
||||
|
||||
return cls._instance
|
||||
|
||||
def schedule(self, func, trigger, name=None):
|
||||
if use_APScheduler:
|
||||
return self.scheduler.add_job(func=func, trigger=trigger, name=name)
|
||||
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
|
||||
if use_APScheduler:
|
||||
def scheduled_task():
|
||||
worker_task = task()
|
||||
worker_task.scheduled = True
|
||||
WorkerThread.add(user, worker_task, hidden=hidden)
|
||||
return self.schedule(func=scheduled_task, trigger=trigger, name=name)
|
||||
|
||||
# Expects a list of lambda expressions for the tasks
|
||||
def schedule_tasks(self, tasks, user=None, trigger=None):
|
||||
if use_APScheduler:
|
||||
for task in tasks:
|
||||
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
|
||||
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
|
||||
if use_APScheduler:
|
||||
def immediate_task():
|
||||
WorkerThread.add(user, task(), hidden)
|
||||
return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
|
||||
|
||||
# Expects a list of lambda expressions for the tasks
|
||||
def schedule_tasks_immediately(self, tasks, user=None):
|
||||
if use_APScheduler:
|
||||
for task in tasks:
|
||||
self.schedule_task_immediately(task[0], user, name="immediately " + task[1], hidden=task[2])
|
||||
|
||||
# Remove all jobs
|
||||
def remove_all_jobs(self):
|
||||
self.scheduler.remove_all_jobs()
|
@ -0,0 +1,19 @@
|
||||
.lightTheme {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.darkTheme {
|
||||
background: #202124;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.sepiaTheme {
|
||||
background: #ece1ca;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.blackTheme {
|
||||
background: #000;
|
||||
color: #fff
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8
|
||||
9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
|
Before Width: | Height: | Size: 461 B |
@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>
|
Before Width: | Height: | Size: 458 B |
Before Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 326 B |
@ -1,16 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||
16"
|
||||
fill="rgba(255,255,255,1)">
|
||||
<path
|
||||
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
|
||||
</path>
|
||||
<path
|
||||
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
|
||||
</path>
|
||||
<circle
|
||||
cx="8" cy="5" r="1.188">
|
||||
</circle>
|
||||
</svg>
|
Before Width: | Height: | Size: 557 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>
|
Before Width: | Height: | Size: 255 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>
|
Before Width: | Height: | Size: 339 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>
|
Before Width: | Height: | Size: 256 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>
|
Before Width: | Height: | Size: 231 B |
@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>
|
Before Width: | Height: | Size: 521 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>
|
Before Width: | Height: | Size: 302 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4z"/></svg>
|
After Width: | Height: | Size: 171 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>
|
Before Width: | Height: | Size: 307 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>
|
Before Width: | Height: | Size: 509 B |
@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
|
Before Width: | Height: | Size: 505 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"/></svg>
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"/></svg>
|
Before Width: | Height: | Size: 196 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"/></svg>
|
Before Width: | Height: | Size: 705 B |
@ -1,2 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||
fill="rgba(255,255,255,1)"><path d="M4 16V2s0-1 1-1h6s1 0 1 1v14l-4-5z"/></svg>
|
Before Width: | Height: | Size: 142 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="m14 9h-6c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm-5.2-8h-3.8c-1.3 0-1.3 2 0 2h1.7zm-6.8 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.3 1.7-0.7 0-0.5-0.4-1-1-1zm3 8c-1 0-1.3 1-0.7 1.7 0.6 0.6 1.7 0.2 1.7-0.7 0-0.5-0.4-1-1-1zm0.3-4h-0.3c-1.4 0-1.4 2 0 2h2.3zm-3.3 0c-0.9 0-1.4 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.7 0-0.6-0.5-1-1-1zm12 8h-9c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zm-12 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.712 0-0.5-0.4-1-1-1z"/><path d="m7.37 4.838 3.93-3.911v2.138h3.629v3.546h-3.629v2.138l-3.93-3.911"/></svg>
|
After Width: | Height: | Size: 581 B |
@ -1,5 +0,0 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||
fill="rgba(255,255,255,1)"><path d="M14 3h-2v2h2v8H2V5h7V3h-.849L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM2 3h3.219l1.072 1H2z"></path><path d="M8.146 6.146a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2-2a.5.5 0 1 0-.707-.707L11 7.293V.5a.5.5 0 0 0-1 0v6.793L8.854 6.146a.5.5 0 0 0-.708 0z"></path></svg>
|
Before Width: | Height: | Size: 651 B |
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
|
||||
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
|
||||
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
|
||||
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
|
||||
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
|
||||
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
|
||||
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
|
||||
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 804 B |
@ -1 +0,0 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="rgba(255,255,255,1)"><path d="M8 11a1 1 0 01-.707-.293l-2.99-2.99c-.91-.942.471-2.324 1.414-1.414L8 8.586l2.283-2.283c.943-.91 2.324.472 1.414 1.414l-2.99 2.99A1 1 0 018 11z"/></svg>
|
Before Width: | Height: | Size: 251 B |