alexandria-book-collection-manager 0.6.9.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (293) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +68 -0
  4. data/.rubocop_todo.yml +200 -0
  5. data/CHANGELOG.md +23 -0
  6. data/COPYING +339 -0
  7. data/ChangeLog.0 +3598 -0
  8. data/Gemfile +9 -0
  9. data/INSTALL.rdoc +156 -0
  10. data/PACKAGING +36 -0
  11. data/README.md +88 -0
  12. data/RELEASE_CHECKLIST +18 -0
  13. data/Rakefile +264 -0
  14. data/TODO +24 -0
  15. data/alexandria-book-collection-manager.gemspec +35 -0
  16. data/alexandria.desktop.in +10 -0
  17. data/bin/alexandria +82 -0
  18. data/doc/AUTHORS +60 -0
  19. data/doc/BUGS +31 -0
  20. data/doc/FAQ +369 -0
  21. data/doc/HACKING +19 -0
  22. data/doc/NEWS +341 -0
  23. data/doc/alexandria.1 +120 -0
  24. data/doc/cuecat_support.rdoc +67 -0
  25. data/lib/alexandria.rb +85 -0
  26. data/lib/alexandria/about.rb +82 -0
  27. data/lib/alexandria/book_providers.rb +411 -0
  28. data/lib/alexandria/book_providers/adlibris.rb +235 -0
  29. data/lib/alexandria/book_providers/amazon_aws.rb +261 -0
  30. data/lib/alexandria/book_providers/amazon_ecs_util.rb +405 -0
  31. data/lib/alexandria/book_providers/barnes_and_noble.rb +229 -0
  32. data/lib/alexandria/book_providers/bol_it.rb +162 -0
  33. data/lib/alexandria/book_providers/deastore.rb +277 -0
  34. data/lib/alexandria/book_providers/douban.rb +135 -0
  35. data/lib/alexandria/book_providers/ibs_it.rb +149 -0
  36. data/lib/alexandria/book_providers/mcu.rb +177 -0
  37. data/lib/alexandria/book_providers/proxis.rb +205 -0
  38. data/lib/alexandria/book_providers/pseudomarc.rb +185 -0
  39. data/lib/alexandria/book_providers/renaud.rb +142 -0
  40. data/lib/alexandria/book_providers/siciliano.rb +271 -0
  41. data/lib/alexandria/book_providers/thalia.rb +197 -0
  42. data/lib/alexandria/book_providers/web.rb +59 -0
  43. data/lib/alexandria/book_providers/webster_it.rb +173 -0
  44. data/lib/alexandria/book_providers/worldcat.rb +251 -0
  45. data/lib/alexandria/book_providers/z3950.rb +422 -0
  46. data/lib/alexandria/config.rb +8 -0
  47. data/lib/alexandria/console.rb +31 -0
  48. data/lib/alexandria/execution_queue.rb +96 -0
  49. data/lib/alexandria/export_library.rb +536 -0
  50. data/lib/alexandria/import_library.rb +316 -0
  51. data/lib/alexandria/import_library_csv.rb +270 -0
  52. data/lib/alexandria/logging.rb +159 -0
  53. data/lib/alexandria/models/book.rb +72 -0
  54. data/lib/alexandria/models/library.rb +714 -0
  55. data/lib/alexandria/net.rb +53 -0
  56. data/lib/alexandria/preferences.rb +324 -0
  57. data/lib/alexandria/scanners.rb +42 -0
  58. data/lib/alexandria/scanners/cuecat.rb +118 -0
  59. data/lib/alexandria/scanners/keyboard.rb +57 -0
  60. data/lib/alexandria/smart_library.rb +525 -0
  61. data/lib/alexandria/ui.rb +53 -0
  62. data/lib/alexandria/ui/builder_base.rb +43 -0
  63. data/lib/alexandria/ui/callbacks.rb +389 -0
  64. data/lib/alexandria/ui/completion_models.rb +228 -0
  65. data/lib/alexandria/ui/dialogs/about_dialog.rb +59 -0
  66. data/lib/alexandria/ui/dialogs/acquire_dialog.rb +640 -0
  67. data/lib/alexandria/ui/dialogs/alert_dialog.rb +68 -0
  68. data/lib/alexandria/ui/dialogs/bad_isbns_dialog.rb +43 -0
  69. data/lib/alexandria/ui/dialogs/barcode_animation.rb +159 -0
  70. data/lib/alexandria/ui/dialogs/book_properties_dialog.rb +210 -0
  71. data/lib/alexandria/ui/dialogs/book_properties_dialog_base.rb +432 -0
  72. data/lib/alexandria/ui/dialogs/export_dialog.rb +172 -0
  73. data/lib/alexandria/ui/dialogs/import_dialog.rb +205 -0
  74. data/lib/alexandria/ui/dialogs/misc_dialogs.rb +85 -0
  75. data/lib/alexandria/ui/dialogs/new_book_dialog.rb +639 -0
  76. data/lib/alexandria/ui/dialogs/new_book_dialog_manual.rb +153 -0
  77. data/lib/alexandria/ui/dialogs/new_smart_library_dialog.rb +67 -0
  78. data/lib/alexandria/ui/dialogs/preferences_dialog.rb +587 -0
  79. data/lib/alexandria/ui/dialogs/smart_library_properties_dialog.rb +56 -0
  80. data/lib/alexandria/ui/dialogs/smart_library_properties_dialog_base.rb +432 -0
  81. data/lib/alexandria/ui/dndable.rb +81 -0
  82. data/lib/alexandria/ui/gtk_thread_help.rb +88 -0
  83. data/lib/alexandria/ui/icons.rb +100 -0
  84. data/lib/alexandria/ui/iconview.rb +91 -0
  85. data/lib/alexandria/ui/iconview_tooltips.rb +162 -0
  86. data/lib/alexandria/ui/init.rb +93 -0
  87. data/lib/alexandria/ui/libraries_combo.rb +75 -0
  88. data/lib/alexandria/ui/listview.rb +310 -0
  89. data/lib/alexandria/ui/main_app.rb +67 -0
  90. data/lib/alexandria/ui/multi_drag_treeview.rb +150 -0
  91. data/lib/alexandria/ui/sidepane.rb +194 -0
  92. data/lib/alexandria/ui/sound.rb +107 -0
  93. data/lib/alexandria/ui/ui_manager.rb +1308 -0
  94. data/lib/alexandria/undo_manager.rb +78 -0
  95. data/lib/alexandria/utils.rb +30 -0
  96. data/lib/alexandria/version.rb +24 -0
  97. data/lib/alexandria/web_themes.rb +75 -0
  98. data/misc/sounds/README +15 -0
  99. data/misc/sounds/bad_scan.csd +62 -0
  100. data/misc/sounds/good_scan.csd +61 -0
  101. data/misc/sounds/scanning.csd +46 -0
  102. data/po/ChangeLog +488 -0
  103. data/po/Makefile +44 -0
  104. data/po/README +29 -0
  105. data/po/commit-po +72 -0
  106. data/po/cs.po +1437 -0
  107. data/po/cy.po +1521 -0
  108. data/po/de.po +1400 -0
  109. data/po/el.po +1379 -0
  110. data/po/es.po +1376 -0
  111. data/po/fr.po +1420 -0
  112. data/po/ga.po +1359 -0
  113. data/po/gl.po +1397 -0
  114. data/po/it.po +1406 -0
  115. data/po/ja.po +1355 -0
  116. data/po/mk.po +1373 -0
  117. data/po/nb.po +1386 -0
  118. data/po/nl.po +1405 -0
  119. data/po/pl.po +1373 -0
  120. data/po/pt.po +1398 -0
  121. data/po/pt_BR.po +1409 -0
  122. data/po/ru.po +1372 -0
  123. data/po/sk.po +1380 -0
  124. data/po/sv.po +1402 -0
  125. data/po/uk.po +1423 -0
  126. data/po/zh_TW.po +1394 -0
  127. data/schemas/alexandria.schemas +300 -0
  128. data/share/alexandria/glade/acquire_dialog__builder.glade +201 -0
  129. data/share/alexandria/glade/book_properties_dialog__builder.glade +910 -0
  130. data/share/alexandria/glade/main_app__builder.glade +229 -0
  131. data/share/alexandria/glade/new_book_dialog__builder.glade +379 -0
  132. data/share/alexandria/glade/preferences_dialog__builder.glade +733 -0
  133. data/share/alexandria/icons/alexandria.png +0 -0
  134. data/share/alexandria/icons/alexandria_small.png +0 -0
  135. data/share/alexandria/icons/book.png +0 -0
  136. data/share/alexandria/icons/book_icon.png +0 -0
  137. data/share/alexandria/icons/book_small.png +0 -0
  138. data/share/alexandria/icons/cuecat.png +0 -0
  139. data/share/alexandria/icons/cuecat_inactive.png +0 -0
  140. data/share/alexandria/icons/favorite_tag.png +0 -0
  141. data/share/alexandria/icons/less.png +0 -0
  142. data/share/alexandria/icons/library.png +0 -0
  143. data/share/alexandria/icons/library_small.png +0 -0
  144. data/share/alexandria/icons/lookup.png +0 -0
  145. data/share/alexandria/icons/more.png +0 -0
  146. data/share/alexandria/icons/no_cover.png +0 -0
  147. data/share/alexandria/icons/smart_library.png +0 -0
  148. data/share/alexandria/icons/smart_library_small.png +0 -0
  149. data/share/alexandria/icons/star_set.png +0 -0
  150. data/share/alexandria/icons/star_unset.png +0 -0
  151. data/share/alexandria/icons/view_as_icons.png +0 -0
  152. data/share/alexandria/icons/view_as_list.png +0 -0
  153. data/share/alexandria/ui/menus.xml +91 -0
  154. data/share/alexandria/ui/popups.xml +91 -0
  155. data/share/alexandria/web-themes/clean/clean.css +85 -0
  156. data/share/alexandria/web-themes/clean/preview.jpg +0 -0
  157. data/share/alexandria/web-themes/list/list.css +105 -0
  158. data/share/alexandria/web-themes/list/preview.jpg +0 -0
  159. data/share/app-icon/16x16/alexandria.png +0 -0
  160. data/share/app-icon/16x16/alexandria.svg +263 -0
  161. data/share/app-icon/22x22/alexandria.png +0 -0
  162. data/share/app-icon/22x22/alexandria.svg +465 -0
  163. data/share/app-icon/24x24/alexandria.png +0 -0
  164. data/share/app-icon/32x32/alexandria.png +0 -0
  165. data/share/app-icon/32x32/alexandria.svg +813 -0
  166. data/share/app-icon/32x32/alexandria.xpm +241 -0
  167. data/share/app-icon/48x48/alexandria.png +0 -0
  168. data/share/app-icon/scalable/alexandria.svg +700 -0
  169. data/share/gnome/help/alexandria/C/about.xml +44 -0
  170. data/share/gnome/help/alexandria/C/adding-books.xml +339 -0
  171. data/share/gnome/help/alexandria/C/alexandria.xml +185 -0
  172. data/share/gnome/help/alexandria/C/bugs.xml +18 -0
  173. data/share/gnome/help/alexandria/C/editing-book-properties.xml +124 -0
  174. data/share/gnome/help/alexandria/C/exporting.xml +81 -0
  175. data/share/gnome/help/alexandria/C/figures/adding_books_acquire_from_scanner_process.png +0 -0
  176. data/share/gnome/help/alexandria/C/figures/adding_books_add_by_isbn.png +0 -0
  177. data/share/gnome/help/alexandria/C/figures/adding_books_isbn_import.png +0 -0
  178. data/share/gnome/help/alexandria/C/figures/adding_books_manual_details.png +0 -0
  179. data/share/gnome/help/alexandria/C/figures/adding_books_rename_library_after_import.png +0 -0
  180. data/share/gnome/help/alexandria/C/figures/adding_books_search_results.png +0 -0
  181. data/share/gnome/help/alexandria/C/figures/editing_book_properties_info.png +0 -0
  182. data/share/gnome/help/alexandria/C/figures/editing_book_properties_loaning.png +0 -0
  183. data/share/gnome/help/alexandria/C/figures/exporting_information_html.png +0 -0
  184. data/share/gnome/help/alexandria/C/figures/getting_started_first_launched.png +0 -0
  185. data/share/gnome/help/alexandria/C/figures/searching_filtering_views_list_view.png +0 -0
  186. data/share/gnome/help/alexandria/C/figures/searching_filtering_views_list_view_search.png +0 -0
  187. data/share/gnome/help/alexandria/C/figures/settings_providers_new_z3950.png +0 -0
  188. data/share/gnome/help/alexandria/C/figures/smart_libraries_new_smart_library.png +0 -0
  189. data/share/gnome/help/alexandria/C/figures/working_with_libraries_library_pane.png +0 -0
  190. data/share/gnome/help/alexandria/C/getting-started.xml +154 -0
  191. data/share/gnome/help/alexandria/C/gnu-fdl-1.2.xml +543 -0
  192. data/share/gnome/help/alexandria/C/introduction.xml +142 -0
  193. data/share/gnome/help/alexandria/C/searching.xml +90 -0
  194. data/share/gnome/help/alexandria/C/settings.xml +140 -0
  195. data/share/gnome/help/alexandria/C/smart-libraries.xml +160 -0
  196. data/share/gnome/help/alexandria/C/working-with-libraries.xml +76 -0
  197. data/share/gnome/help/alexandria/ChangeLog +99 -0
  198. data/share/gnome/help/alexandria/fr/alexandria.xml +2292 -0
  199. data/share/gnome/help/alexandria/fr/figures/alexandria_add_button.png +0 -0
  200. data/share/gnome/help/alexandria/fr/figures/alexandria_add_by_isbn_1.png +0 -0
  201. data/share/gnome/help/alexandria/fr/figures/alexandria_add_by_search_1.png +0 -0
  202. data/share/gnome/help/alexandria/fr/figures/alexandria_add_manually.png +0 -0
  203. data/share/gnome/help/alexandria/fr/figures/alexandria_add_z3950.png +0 -0
  204. data/share/gnome/help/alexandria/fr/figures/alexandria_close_button.png +0 -0
  205. data/share/gnome/help/alexandria/fr/figures/alexandria_edit_info.png +0 -0
  206. data/share/gnome/help/alexandria/fr/figures/alexandria_export_web_page.png +0 -0
  207. data/share/gnome/help/alexandria/fr/figures/alexandria_importing.png +0 -0
  208. data/share/gnome/help/alexandria/fr/figures/alexandria_library_pane.png +0 -0
  209. data/share/gnome/help/alexandria/fr/figures/alexandria_list_view.png +0 -0
  210. data/share/gnome/help/alexandria/fr/figures/alexandria_list_view_search.png +0 -0
  211. data/share/gnome/help/alexandria/fr/figures/alexandria_loaning.png +0 -0
  212. data/share/gnome/help/alexandria/fr/figures/alexandria_main_window.png +0 -0
  213. data/share/gnome/help/alexandria/fr/figures/alexandria_remove_button.png +0 -0
  214. data/share/gnome/help/alexandria/ja/about.xml +33 -0
  215. data/share/gnome/help/alexandria/ja/adding-books.xml +314 -0
  216. data/share/gnome/help/alexandria/ja/alexandria.xml +172 -0
  217. data/share/gnome/help/alexandria/ja/bugs.xml +11 -0
  218. data/share/gnome/help/alexandria/ja/editing-book-properties.xml +100 -0
  219. data/share/gnome/help/alexandria/ja/exporting.xml +98 -0
  220. data/share/gnome/help/alexandria/ja/figures/adding_books_acquire_from_scanner_process.png +0 -0
  221. data/share/gnome/help/alexandria/ja/figures/adding_books_add_by_isbn.png +0 -0
  222. data/share/gnome/help/alexandria/ja/figures/adding_books_isbn_import.png +0 -0
  223. data/share/gnome/help/alexandria/ja/figures/adding_books_manual_details.png +0 -0
  224. data/share/gnome/help/alexandria/ja/figures/adding_books_rename_library_after_import.png +0 -0
  225. data/share/gnome/help/alexandria/ja/figures/adding_books_search_results.png +0 -0
  226. data/share/gnome/help/alexandria/ja/figures/editing_book_properties_info.png +0 -0
  227. data/share/gnome/help/alexandria/ja/figures/editing_book_properties_loaning.png +0 -0
  228. data/share/gnome/help/alexandria/ja/figures/exporting_information_html.png +0 -0
  229. data/share/gnome/help/alexandria/ja/figures/getting_started_first_launched.png +0 -0
  230. data/share/gnome/help/alexandria/ja/figures/searching_filtering_views_list_view.png +0 -0
  231. data/share/gnome/help/alexandria/ja/figures/searching_filtering_views_list_view_search.png +0 -0
  232. data/share/gnome/help/alexandria/ja/figures/settings_providers_new_z3950.png +0 -0
  233. data/share/gnome/help/alexandria/ja/figures/smart_libraries_new_smart_library.png +0 -0
  234. data/share/gnome/help/alexandria/ja/figures/working_with_libraries_library_pane.png +0 -0
  235. data/share/gnome/help/alexandria/ja/getting-started.xml +144 -0
  236. data/share/gnome/help/alexandria/ja/gnu-fdl-1.2.xml +541 -0
  237. data/share/gnome/help/alexandria/ja/introduction.xml +134 -0
  238. data/share/gnome/help/alexandria/ja/searching.xml +104 -0
  239. data/share/gnome/help/alexandria/ja/settings.xml +129 -0
  240. data/share/gnome/help/alexandria/ja/smart-libraries.xml +140 -0
  241. data/share/gnome/help/alexandria/ja/working-with-libraries.xml +88 -0
  242. data/share/menu/alexandria +7 -0
  243. data/share/omf/alexandria/alexandria-C.omf.in +27 -0
  244. data/share/omf/alexandria/alexandria-fr.omf.in +30 -0
  245. data/share/sounds/alexandria/bad_scan.ogg +0 -0
  246. data/share/sounds/alexandria/bad_scan.wav +0 -0
  247. data/share/sounds/alexandria/good_scan.ogg +0 -0
  248. data/share/sounds/alexandria/good_scan.wav +0 -0
  249. data/share/sounds/alexandria/scanning.ogg +0 -0
  250. data/share/sounds/alexandria/scanning.wav +0 -0
  251. data/spec/alexandria/library_spec.rb +205 -0
  252. data/spec/alexandria/preferences_spec.rb +22 -0
  253. data/spec/alexandria/scanners/cuecat_spec.rb +68 -0
  254. data/spec/alexandria/smart_library_spec.rb +22 -0
  255. data/spec/alexandria/ui/dialogs_spec.rb +90 -0
  256. data/spec/alexandria/ui/iconview_spec.rb +27 -0
  257. data/spec/alexandria/ui/listview_spec.rb +28 -0
  258. data/spec/alexandria/ui/main_app_spec.rb +48 -0
  259. data/spec/alexandria/ui/sidepane_spec.rb +27 -0
  260. data/spec/alexandria/ui/ui_manager_spec.rb +26 -0
  261. data/spec/alexandria/ui/ui_utilities_spec.rb +60 -0
  262. data/spec/alexandria/utilities_spec.rb +50 -0
  263. data/spec/data/libraries/0.6.1-noisbn/My Library/0201398257.yaml +10 -0
  264. data/spec/data/libraries/0.6.1-noisbn/My Library/1565920007.yaml +10 -0
  265. data/spec/data/libraries/0.6.1/My Library/0192812173.yaml +9 -0
  266. data/spec/data/libraries/0.6.1/My Library/0201398257.cover +0 -0
  267. data/spec/data/libraries/0.6.1/My Library/0201398257.yaml +10 -0
  268. data/spec/data/libraries/0.6.1/My Library/1565920007.yaml +10 -0
  269. data/spec/data/libraries/0.6.2/My Library/9780140266146.cover +0 -0
  270. data/spec/data/libraries/0.6.2/My Library/9780140266146.yaml +16 -0
  271. data/spec/data/libraries/0.6.2/My Library/9780140278781.cover +0 -0
  272. data/spec/data/libraries/0.6.2/My Library/9780140278781.yaml +21 -0
  273. data/spec/data/libraries/0.6.2/My Library/9780571147168.cover +0 -0
  274. data/spec/data/libraries/0.6.2/My Library/9780571147168.yaml +20 -0
  275. data/spec/data/libraries/0.6.2/My Library/9780575079038.cover +0 -0
  276. data/spec/data/libraries/0.6.2/My Library/9780575079038.yaml +20 -0
  277. data/spec/data/libraries/0.6.2/My Library/9780755322800.cover +0 -0
  278. data/spec/data/libraries/0.6.2/My Library/9780755322800.yaml +20 -0
  279. data/spec/spec_helper.rb +46 -0
  280. data/tasks/rdoc.rake +6 -0
  281. data/tasks/setup.rb +30 -0
  282. data/tasks/spec.rake +29 -0
  283. data/tasks/test.rake +38 -0
  284. data/test/application_test.rb +39 -0
  285. data/test/book_test.rb +34 -0
  286. data/test/data/isbns.txt +3 -0
  287. data/test/isbn_test.rb +68 -0
  288. data/test/providers_test.rb +254 -0
  289. data/test/test_helper.rb +42 -0
  290. data/util/rake/fileinstall.rb +313 -0
  291. data/util/rake/gettextgenerate.rb +158 -0
  292. data/util/rake/omfgenerate.rb +79 -0
  293. metadata +452 -0
@@ -0,0 +1,159 @@
1
+ # Copyright (C) 2007 Cathal Mc Ginley
2
+ # Modifications Copyright (C) 2011 Matijs van Zuijlen
3
+ #
4
+ # Alexandria is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License as
6
+ # published by the Free Software Foundation; either version 2 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # Alexandria is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public
15
+ # License along with Alexandria; see the file COPYING. If not,
16
+ # write to the Free Software Foundation, Inc., 51 Franklin Street,
17
+ # Fifth Floor, Boston, MA 02110-1301 USA.
18
+
19
+ require 'logger'
20
+ require 'forwardable'
21
+
22
+ module Alexandria
23
+ # A Logger subclass which accepts a source for log messages
24
+ # in order to improve legibility of the logs.
25
+ # The source should usually be +self+, whether that be a Class, Module
26
+ # or Object. A LoggerWrapper can be used to simplify this procedure.
27
+ class Logger < ::Logger
28
+ def add(severity, message = nil, source = nil, progname = nil, &block)
29
+ if source.nil?
30
+ return super(severity, message, progname, &block)
31
+ end
32
+ category = self.class.category(source)
33
+ unless block_given?
34
+ return super(severity, progname, category)
35
+ end
36
+ category = "#{category} #{progname}" if progname
37
+ super(severity, message, category, &block)
38
+ end
39
+
40
+ def debug(source = nil, progname = nil, &block)
41
+ add(DEBUG, nil, source, progname, &block)
42
+ end
43
+
44
+ def info(source = nil, progname = nil, &block)
45
+ add(INFO, nil, source, progname, &block)
46
+ end
47
+
48
+ def warn(source = nil, progname = nil, &block)
49
+ add(WARN, nil, source, progname, &block)
50
+ end
51
+
52
+ def error(source = nil, progname = nil, &block)
53
+ add(ERROR, nil, source, progname, &block)
54
+ end
55
+
56
+ def fatal(source = nil, progname = nil, &block)
57
+ add(FATAL, nil, source, progname, &block)
58
+ end
59
+
60
+ def self.category(source)
61
+ if source.instance_of? Class
62
+ "[Cls #{source.name}]"
63
+ elsif source.instance_of? Module
64
+ "[Mod #{source.name}]"
65
+ else
66
+ "<Obj #{source.class.name}>"
67
+ end
68
+ end
69
+ end
70
+
71
+ # A wrapper around a Logger, which allows code to define the source
72
+ # once (in the wrapper's initialization) and then call the log methods
73
+ # whithout needing to specify the source each time.
74
+ class LogWrapper
75
+ extend Forwardable
76
+ def initialize(logger, source)
77
+ @logger = logger
78
+ @source = source
79
+ end
80
+
81
+ def_delegators :@logger, :debug?, :info?, :warn?, :error?, :fatal?
82
+
83
+ def <<(msg)
84
+ if msg.respond_to? :backtrace
85
+ msg.backtrace.each { |line|
86
+ @logger << " #{line} \n"
87
+ }
88
+ else
89
+ @logger << msg + "\n"
90
+ end
91
+ end
92
+
93
+ def debug(progname = nil, &block)
94
+ @logger.debug(@source, progname, &block)
95
+ end
96
+
97
+ def info(progname = nil, &block)
98
+ @logger.info(@source, progname, &block)
99
+ end
100
+
101
+ def warn(progname = nil, &block)
102
+ @logger.warn(@source, progname, &block)
103
+ end
104
+
105
+ def error(progname = nil, &block)
106
+ @logger.error(@source, progname, &block)
107
+ end
108
+
109
+ def fatal(progname = nil, &block)
110
+ @logger.fatal(@source, progname, &block)
111
+ end
112
+ end
113
+
114
+ # A mixin to include a +log+ instance method for objects or a
115
+ # static +log+ method for classes and modules. In either case, a
116
+ # +LogWrapper+ is returned which wraps the Alexandria log and
117
+ # specifies the appropriate source object, class or module.
118
+ module Logging
119
+ module ClassMethods
120
+ def log
121
+ @log_wrapper ||= LogWrapper.new(Alexandria.log, self)
122
+ end
123
+ end
124
+
125
+ def self.included(base)
126
+ base.extend(ClassMethods)
127
+ end
128
+
129
+ def log
130
+ @log_wrapper ||= LogWrapper.new(Alexandria.log, self)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ # Creates the Logger for Alexandria
137
+ def self.create_logger
138
+ logger = Alexandria::Logger.new(STDERR)
139
+
140
+ level = ENV['LOGLEVEL'] ? ENV['LOGLEVEL'].intern : nil
141
+ if [:FATAL, :ERROR, :WARN, :INFO, :DEBUG].include? level
142
+ logger.level = Logger.const_get(level)
143
+ else
144
+ logger.level = Logger::WARN # default level
145
+ logger.warn(self, "Unknown LOGLEVEL '#{level}'; using WARN") if level
146
+ end
147
+
148
+ logger
149
+ end
150
+
151
+ @@logger = create_logger
152
+
153
+ public
154
+
155
+ # Returns the Logger for Alexandria
156
+ def self.log
157
+ @@logger
158
+ end
159
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright (C) 2004-2006 Laurent Sansonetti
2
+ #
3
+ # Alexandria is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU General Public License as
5
+ # published by the Free Software Foundation; either version 2 of the
6
+ # License, or (at your option) any later version.
7
+ #
8
+ # Alexandria is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public
14
+ # License along with Alexandria; see the file COPYING. If not,
15
+ # write to the Free Software Foundation, Inc., 51 Franklin Street,
16
+ # Fifth Floor, Boston, MA 02110-1301 USA.
17
+
18
+ module Alexandria
19
+ class Book
20
+ attr_accessor :title, :authors, :isbn, :publisher, :publishing_year,
21
+ :edition, :rating, :notes, :loaned, :loaned_since,
22
+ :loaned_to, :saved_ident, :redd, :redd_when, :own, :want, :tags, :version, :library
23
+
24
+ DEFAULT_RATING = 0
25
+
26
+ def initialize(title, authors, isbn, publisher, publishing_year,
27
+ edition)
28
+
29
+ @title = title
30
+ @authors = authors
31
+ @isbn = isbn
32
+ @publisher = publisher
33
+ @edition = edition # actually used for binding! (i.e. paperback or hardback)
34
+ @notes = ''
35
+ @saved_ident = ident
36
+ @publishing_year = publishing_year
37
+ @redd = false
38
+ @own = true
39
+ @want = true
40
+ @tags = []
41
+ # Need to implement bulk save function to update this
42
+ @version = Alexandria::DATA_VERSION
43
+ end
44
+
45
+ def ident
46
+ if (!@isbn.nil?) and @isbn.empty?
47
+ @isbn = nil
48
+ end
49
+ @isbn or @title.hash.to_s
50
+ end
51
+
52
+ def loaned?
53
+ loaned or false
54
+ end
55
+
56
+ def redd?
57
+ redd or false
58
+ end
59
+
60
+ def want?
61
+ want or false
62
+ end
63
+
64
+ def own?
65
+ own or false
66
+ end
67
+
68
+ def ==(obj)
69
+ obj.is_a?(self.class) and ident == obj.ident
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,714 @@
1
+ # Copyright (C) 2004-2006 Laurent Sansonetti
2
+ # Copyright (C) 2014 Matijs van Zuijlen
3
+ #
4
+ # Alexandria is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License as
6
+ # published by the Free Software Foundation; either version 2 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # Alexandria is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public
15
+ # License along with Alexandria; see the file COPYING. If not,
16
+ # write to the Free Software Foundation, Inc., 51 Franklin Street,
17
+ # Fifth Floor, Boston, MA 02110-1301 USA.
18
+
19
+ require 'yaml'
20
+ require 'fileutils'
21
+ require 'rexml/document'
22
+ require 'tempfile'
23
+ require 'etc'
24
+ require 'open-uri'
25
+ require 'observer'
26
+ require 'singleton'
27
+
28
+ class Array
29
+ def sum
30
+ reduce(0) { |a, b| a + b }
31
+ end
32
+ end
33
+
34
+ module Alexandria
35
+ class Library < Array
36
+ include Logging
37
+
38
+ attr_reader :name
39
+ attr_accessor :ruined_books, :updating, :deleted_books
40
+ DIR = File.join(ENV['HOME'], '.alexandria')
41
+ EXT = { book: '.yaml', cover: '.cover' }
42
+
43
+ include GetText
44
+ extend GetText
45
+ bindtextdomain(Alexandria::TEXTDOMAIN, charset: 'UTF-8')
46
+
47
+ BOOK_ADDED, BOOK_UPDATED, BOOK_REMOVED = (0..3).to_a
48
+ include Observable
49
+
50
+ def path
51
+ File.join(DIR, @name)
52
+ end
53
+
54
+ def updating?
55
+ @updating
56
+ end
57
+ def self.generate_new_name(existing_libraries,
58
+ from_base = _('Untitled'))
59
+ i = 1
60
+ name = nil
61
+ all_libraries = existing_libraries + @@deleted_libraries
62
+ while true
63
+ name = i == 1 ? from_base : from_base + " #{i}"
64
+ break unless all_libraries.find { |x| x.name == name }
65
+ i += 1
66
+ end
67
+ name
68
+ end
69
+
70
+ FIX_BIGNUM_REGEX =
71
+ /loaned_since:\s*(\!ruby\/object\:Bignum\s*)?(\d+)\n/
72
+
73
+ def self.load(name)
74
+ test = [0, nil]
75
+ ruined_books = []
76
+ library = Library.new(name)
77
+ FileUtils.mkdir_p(library.path) unless File.exist?(library.path)
78
+ Dir.chdir(library.path) do
79
+ Dir['*' + EXT[:book]].each do |filename|
80
+
81
+ test[1] = filename if test[0] == 0
82
+
83
+ unless File.size? test[1]
84
+ log.warn { "Book file #{test[1]} was empty" }
85
+ md = /([\dxX]{10,13})#{EXT[:book]}/.match(filename)
86
+ if md
87
+ file_isbn = md[1]
88
+ ruined_books << [nil, file_isbn, library]
89
+ else
90
+ log.warn { "Filename #{filename} does not contain an ISBN" }
91
+ # TODO delete this file...
92
+ end
93
+ next
94
+ end
95
+ book = regularize_book_from_yaml(test[1])
96
+ old_isbn = book.isbn
97
+ old_pub_year = book.publishing_year
98
+ begin
99
+ begin
100
+ book.isbn = canonicalise_ean(book.isbn).to_s unless book.isbn.nil?
101
+ raise "Not a book: #{book.inspect}" unless book.is_a?(Book)
102
+ rescue InvalidISBNError
103
+ book.isbn = old_isbn
104
+ end
105
+
106
+ book.publishing_year = book.publishing_year.to_i unless book.publishing_year.nil?
107
+
108
+ # Or if isbn has changed
109
+ raise "#{test[1]} isbn is not okay" unless book.isbn == old_isbn
110
+
111
+ # Re-save book if Alexandria::DATA_VERSION changes
112
+ raise "#{test[1]} version is not okay" unless book.version == Alexandria::DATA_VERSION
113
+
114
+ # Or if publishing year has changed
115
+ raise "#{test[1]} pub year is not okay" unless book.publishing_year == old_pub_year
116
+
117
+ # ruined_books << [book, book.isbn, library]
118
+ book.library = library.name
119
+
120
+ ## TODO copy cover image file, if necessary
121
+ # due to #26909 cover files for books without ISBN are re-saved as "g#{ident}.cover"
122
+ if book.isbn.nil? || book.isbn.empty?
123
+ if File.exist? library.old_cover(book)
124
+ log.debug { "#{library.name}; book #{book.title} has no ISBN, fixing cover image" }
125
+ FileUtils::Verbose.mv(library.old_cover(book), library.cover(book))
126
+ end
127
+ end
128
+
129
+ library << book
130
+ rescue
131
+ book.version = Alexandria::DATA_VERSION
132
+ savedfilename = library.simple_save(book)
133
+ test[0] = test[0] + 1
134
+ test[1] = savedfilename
135
+
136
+ # retries the Dir.each block...
137
+ # but gives up after three tries
138
+ redo unless test[0] > 2
139
+
140
+ else
141
+ test = [0, nil]
142
+ end
143
+ end
144
+
145
+ # Since 0.4.0 the cover files '_small.jpg' and
146
+ # '_medium.jpg' have been deprecated for a single medium
147
+ # cover file named '.cover'.
148
+
149
+ Dir['*' + '_medium.jpg'].each do |medium_cover|
150
+ begin
151
+ FileUtils.mv(medium_cover,
152
+ medium_cover.sub(/_medium\.jpg$/,
153
+ EXT[:cover]))
154
+ rescue
155
+ end
156
+ end
157
+
158
+ Dir['*' + EXT[:cover]].each do |cover|
159
+ next if cover[0] == 'g'
160
+ md = /(.+)\.cover/.match(cover)
161
+ begin
162
+ ean = canonicalise_ean(md[1])
163
+ rescue
164
+ ean = md[1]
165
+ end
166
+ begin
167
+ FileUtils.mv(cover, ean + EXT[:cover]) unless cover == ean + EXT[:cover]
168
+ rescue
169
+ end
170
+ end
171
+
172
+ FileUtils.rm_f(Dir['*_small.jpg'])
173
+ end
174
+ library.ruined_books = ruined_books
175
+
176
+ library
177
+ end
178
+
179
+ def self.regularize_book_from_yaml(name)
180
+ text = IO.read(name)
181
+
182
+ # Code to remove the mystery string in books imported from Amazon
183
+ # (In the past, still?) To allow ruby-amazon to be removed.
184
+
185
+ # The string is removed on load, but can't make it stick, maybe has to do with cache
186
+
187
+ if /!str:Amazon::Search::Response/.match(text)
188
+ log.debug { "Removing Ruby/Amazon strings from #{name}" }
189
+ text.gsub!('!str:Amazon::Search::Response', '')
190
+ end
191
+
192
+ # Backward compatibility with versions <= 0.6.0, where the
193
+ # loaned_since field was a numeric.
194
+ if (md = FIX_BIGNUM_REGEX.match(text))
195
+ new_yaml = Time.at(md[2].to_i).to_yaml
196
+ # Remove the "---" prefix.
197
+ new_yaml.sub!(/^\s*\-+\s*/, '')
198
+ text.sub!(md[0], "loaned_since: #{new_yaml}\n")
199
+ end
200
+ book = YAML.load(text)
201
+ unless book.isbn.class == String
202
+ # HACK
203
+ md = /isbn: (.+)/.match(text)
204
+ if md
205
+ string_isbn = md[1].strip
206
+ book.isbn = string_isbn
207
+ end
208
+ end
209
+
210
+ # another HACK of the same type as above
211
+ unless book.saved_ident.class == String
212
+
213
+ md2 = /saved_ident: (.+)/.match(text)
214
+ if md2
215
+ string_saved_ident = md2[1].strip
216
+ log.debug { "fixing saved_ident #{book.saved_ident} -> #{string_saved_ident}" }
217
+ book.saved_ident = string_saved_ident
218
+ end
219
+ end
220
+ if book.isbn.class == String and book.isbn.length == 0
221
+ book.isbn = nil # save trouble later
222
+ end
223
+ book
224
+ end
225
+
226
+ def self.loadall
227
+ a = []
228
+ begin
229
+ Dir.entries(DIR).each do |file|
230
+ # Skip hidden files.
231
+ next if /^\./.match(file)
232
+ # Skip non-directory files.
233
+ next unless File.stat(File.join(DIR, file)).directory?
234
+
235
+ a << load(file)
236
+ end
237
+
238
+ rescue Errno::ENOENT
239
+ FileUtils.mkdir_p(DIR)
240
+ end
241
+ # Create the default library if there is no library yet.
242
+
243
+ if a.empty?
244
+ a << load(_('My Library'))
245
+ end
246
+
247
+ a
248
+ end
249
+
250
+ def self.move(source_library, dest_library, *books)
251
+ dest = dest_library.path
252
+ books.each do |book|
253
+ FileUtils.mv(source_library.yaml(book), dest)
254
+ if File.exist?(source_library.cover(book))
255
+ FileUtils.mv(source_library.cover(book), dest)
256
+ end
257
+
258
+ source_library.changed
259
+ source_library.old_delete(book)
260
+ source_library.notify_observers(source_library,
261
+ BOOK_REMOVED,
262
+ book)
263
+
264
+ dest_library.changed
265
+ dest_library.delete_if { |book2| book2.ident == book.ident }
266
+ dest_library << book
267
+ dest_library.notify_observers(dest_library, BOOK_ADDED, book)
268
+ end
269
+ end
270
+
271
+ class NoISBNError < StandardError
272
+ def initialize(msg)
273
+ super(msg)
274
+ end
275
+ end
276
+
277
+ class InvalidISBNError < StandardError
278
+ attr_reader :isbn
279
+ def initialize(isbn = nil)
280
+ super()
281
+ @isbn = isbn
282
+ end
283
+ end
284
+
285
+ def self.extract_numbers(isbn)
286
+ raise NoISBNError.new('Nil ISBN') if isbn.nil? || isbn.empty?
287
+
288
+ isbn.delete('- ').upcase.split('').map do |x|
289
+ raise InvalidISBNError.new(isbn) unless x =~ /[\dX]/
290
+ x == 'X' ? 10 : x.to_i
291
+ end
292
+ end
293
+
294
+ def self.isbn_checksum(numbers)
295
+ sum = (0...numbers.length).reduce(0) do |accumulator, i|
296
+ accumulator + numbers[i] * (i + 1)
297
+ end % 11
298
+
299
+ sum == 10 ? 'X' : sum
300
+ end
301
+
302
+ def self.valid_isbn?(isbn)
303
+ numbers = extract_numbers(isbn)
304
+ numbers.length == 10 and isbn_checksum(numbers) == 0
305
+ rescue InvalidISBNError
306
+ false
307
+ end
308
+
309
+ def self.ean_checksum(numbers)
310
+ (10 - ([1, 3, 5, 7, 9, 11].map { |x| numbers[x] }.sum * 3 +
311
+ [0, 2, 4, 6, 8, 10].map { |x| numbers[x] }.sum)) % 10
312
+ end
313
+
314
+ def self.valid_ean?(ean)
315
+ numbers = extract_numbers(ean)
316
+ (numbers.length == 13 and
317
+ ean_checksum(numbers[0..11]) == numbers[12]) or
318
+ (numbers.length == 18 and
319
+ ean_checksum(numbers[0..11]) == numbers[12])
320
+ rescue InvalidISBNError
321
+ false
322
+ end
323
+
324
+ def self.upc_checksum(numbers)
325
+ (10 - ([0, 2, 4, 6, 8, 10].map { |x| numbers[x] }.sum * 3 +
326
+ [1, 3, 5, 7, 9].map { |x| numbers[x] }.sum)) % 10
327
+ end
328
+
329
+ def self.valid_upc?(upc)
330
+ numbers = extract_numbers(upc)
331
+ (numbers.length == 17 and
332
+ upc_checksum(numbers[0..10]) == numbers[11])
333
+ rescue InvalidISBNError
334
+ false
335
+ end
336
+
337
+ AMERICAN_UPC_LOOKUP = {
338
+ '014794' => '08041', '018926' => '0445', '02778' => '0449',
339
+ '037145' => '0812', '042799' => '0785', '043144' => '0688',
340
+ '044903' => '0312', '045863' => '0517', '046594' => '0064',
341
+ '047132' => '0152', '051487' => '08167', '051488' => '0140',
342
+ '060771' => '0002', '065373' => '0373', '070992' => '0523',
343
+ '070993' => '0446', '070999' => '0345', '071001' => '0380',
344
+ '071009' => '0440', '071125' => '088677', '071136' => '0451',
345
+ '071149' => '0451', '071152' => '0515', '071162' => '0451',
346
+ '071268' => '08217', '071831' => '0425', '071842' => '08439',
347
+ '072742' => '0441', '076714' => '0671', '076783' => '0553',
348
+ '076814' => '0449', '078021' => '0872', '079808' => '0394',
349
+ '090129' => '0679', '099455' => '0061', '099769' => '0451'
350
+ }
351
+
352
+ def self.upc_convert(upc)
353
+ test_upc = upc.map(&:to_s).join
354
+ extract_numbers(AMERICAN_UPC_LOOKUP[test_upc])
355
+ end
356
+
357
+ def self.canonicalise_ean(code)
358
+ code = code.to_s.delete('- ')
359
+ if self.valid_ean?(code)
360
+ return code
361
+ elsif self.valid_isbn?(code)
362
+ code = '978' + code[0..8]
363
+ return code + String(ean_checksum(extract_numbers(code)))
364
+ elsif self.valid_upc?(code)
365
+ isbn10 = canonicalise_isbn
366
+ code = '978' + isbn10[0..8]
367
+ return code + String(ean_checksum(extract_numbers(code)))
368
+ ## raise "fix function Alexandria::Library.canonicalise_ean"
369
+ else
370
+ raise InvalidISBNError.new(code)
371
+ end
372
+ end
373
+
374
+ def self.canonicalise_isbn(isbn)
375
+ numbers = extract_numbers(isbn)
376
+ if self.valid_ean?(isbn) and numbers[0..2] != [9, 7, 8]
377
+ return isbn
378
+ end
379
+ canonical = if self.valid_ean?(isbn)
380
+ # Looks like an EAN number -- extract the intersting part and
381
+ # calculate a checksum. It would be nice if we could validate
382
+ # the EAN number somehow.
383
+ numbers[3..11] + [isbn_checksum(numbers[3..11])]
384
+ elsif self.valid_upc?(isbn)
385
+ # Seems to be a valid UPC number.
386
+ prefix = upc_convert(numbers[0..5])
387
+ isbn_sans_chcksm = prefix + numbers[(8 + prefix.length)..17]
388
+ isbn_sans_chcksm + [isbn_checksum(isbn_sans_chcksm)]
389
+ elsif self.valid_isbn?(isbn)
390
+ # Seems to be a valid ISBN number.
391
+ numbers[0..-2] + [isbn_checksum(numbers[0..-2])]
392
+ else
393
+ raise InvalidISBNError.new(isbn)
394
+ end
395
+
396
+ canonical.map(&:to_s).join
397
+ end
398
+
399
+ def simple_save(book)
400
+ # Let's initialize the saved identifier if not already
401
+ # (backward compatibility from 0.4.0)
402
+ # book.saved_ident ||= book.ident
403
+ if book.saved_ident.nil? or book.saved_ident.empty?
404
+ book.saved_ident = book.ident
405
+ end
406
+ if book.ident != book.saved_ident
407
+ # log.debug { "Backwards compatibility step: #{book.saved_ident.inspect}, #{book.ident.inspect}" }
408
+ FileUtils.rm(yaml(book.saved_ident))
409
+ end
410
+ if File.exist?(cover(book.saved_ident))
411
+ begin
412
+ FileUtils.mv(cover(book.saved_ident), cover(book.ident))
413
+ rescue
414
+ end
415
+ end
416
+ book.saved_ident = book.ident
417
+
418
+ filename = book.saved_ident.to_s + '.yaml'
419
+ File.open(filename, 'w') { |io| io.puts book.to_yaml }
420
+ filename
421
+ end
422
+
423
+ def save(book, final = false)
424
+ changed unless final
425
+
426
+ # Let's initialize the saved identifier if not already
427
+ # (backward compatibility from 0.4.0).
428
+ book.saved_ident ||= book.ident
429
+
430
+ if book.ident != book.saved_ident
431
+ FileUtils.rm(yaml(book.saved_ident))
432
+ if File.exist?(cover(book.saved_ident))
433
+ FileUtils.mv(cover(book.saved_ident), cover(book.ident))
434
+ end
435
+
436
+ # Notify before updating the saved identifier, so the views
437
+ # can still use the old one to update their models.
438
+ notify_observers(self, BOOK_UPDATED, book) unless final
439
+ book.saved_ident = book.ident
440
+ end
441
+ # #was File.exist? but that returns true for empty files... CathalMagus
442
+ already_there = (File.size?(yaml(book)) and
443
+ !@deleted_books.include?(book))
444
+
445
+ temp_book = book.dup
446
+ temp_book.library = nil
447
+ File.open(yaml(temp_book), 'w') { |io| io.puts temp_book.to_yaml }
448
+
449
+ # Do not notify twice.
450
+ if changed?
451
+ notify_observers(self,
452
+ already_there ? BOOK_UPDATED : BOOK_ADDED,
453
+ book)
454
+ end
455
+ end
456
+
457
+ def transport
458
+ config = Alexandria::Preferences.instance.http_proxy_config
459
+ config ? Net::HTTP.Proxy(*config) : Net::HTTP
460
+ end
461
+
462
+ def save_cover(book, cover_uri)
463
+ Dir.chdir(path) do
464
+ # Fetch the cover picture.
465
+ cover_file = cover(book)
466
+ File.open(cover_file, 'w') do |io|
467
+ uri = URI.parse(cover_uri)
468
+ if uri.scheme.nil?
469
+ # Regular filename.
470
+ File.open(cover_uri) { |io2| io.puts io2.read }
471
+ else
472
+ # Try open-uri.
473
+ io.puts transport.get(uri)
474
+ end
475
+ end
476
+
477
+ # Remove the file if its blank.
478
+ if Alexandria::UI::Icons.blank?(cover_file)
479
+ File.delete(cover_file)
480
+ end
481
+ end
482
+ end
483
+
484
+ @@deleted_libraries = []
485
+
486
+ def self.deleted_libraries
487
+ @@deleted_libraries
488
+ end
489
+
490
+ def self.really_delete_deleted_libraries
491
+ @@deleted_libraries.each do |library|
492
+ FileUtils.rm_rf(library.path)
493
+ end
494
+ end
495
+
496
+ def really_delete_deleted_books
497
+ @deleted_books.each do |book|
498
+ [yaml(book), cover(book)].each do |file|
499
+ FileUtils.rm_f(file)
500
+ end
501
+ end
502
+ end
503
+
504
+ alias_method :old_delete, :delete
505
+ def delete(book = nil)
506
+ if book.nil?
507
+ # Delete the whole library.
508
+ raise if @@deleted_libraries.include?(self)
509
+ @@deleted_libraries << self
510
+ else
511
+ if @deleted_books.include?(book)
512
+ doubles = @deleted_books.reject { |b| !b.equal?(book) }
513
+ raise "Book #{book.isbn} was already deleted" unless doubles.empty?
514
+ end
515
+ @deleted_books << book
516
+ i = index(book)
517
+ # We check object IDs there because the user could have added
518
+ # a book with the same identifier as another book he/she
519
+ # previously deleted and that he/she is trying to redo.
520
+ if i and self[i].equal? book
521
+ changed
522
+ old_delete(book) # FIX this will old_delete all '==' books
523
+ notify_observers(self, BOOK_REMOVED, book)
524
+ end
525
+ end
526
+ end
527
+
528
+ def deleted?
529
+ @@deleted_libraries.include?(self)
530
+ end
531
+
532
+ def undelete(book = nil)
533
+ if book.nil?
534
+ # Undelete the whole library.
535
+ raise unless @@deleted_libraries.include?(self)
536
+ @@deleted_libraries.delete(self)
537
+ else
538
+ raise unless @deleted_books.include?(book)
539
+ @deleted_books.delete(book)
540
+ unless self.include?(book)
541
+ changed
542
+ self << book
543
+ notify_observers(self, BOOK_ADDED, book)
544
+ end
545
+ end
546
+ end
547
+
548
+ alias_method :old_select, :select
549
+ def select
550
+ filtered_library = Library.new(@name)
551
+ each do |book|
552
+ filtered_library << book if yield(book)
553
+ end
554
+ filtered_library
555
+ end
556
+
557
+ def old_cover(book)
558
+ File.join(path, book.ident.to_s + EXT[:cover])
559
+ end
560
+
561
+ def cover(something)
562
+ ident = case something
563
+ when Book
564
+ if something.isbn && !something.isbn.empty?
565
+ something.ident
566
+ else
567
+ "g#{something.ident}" # g is for generated id...
568
+ end
569
+ when String
570
+ something
571
+ when Bignum
572
+ something
573
+ when Fixnum
574
+ something
575
+ else
576
+ raise "#{something} is a #{something.class}"
577
+ end
578
+ File.join(path, ident.to_s + EXT[:cover])
579
+ end
580
+
581
+ def yaml(something, basedir = path)
582
+ ident = case something
583
+ when Book
584
+ something.ident
585
+ when String
586
+ something
587
+ when Bignum
588
+ something
589
+ when Fixnum
590
+ something
591
+ else
592
+ raise "#{something} is #{something.class}"
593
+ end
594
+ File.join(basedir, ident.to_s + EXT[:book])
595
+ end
596
+
597
+ def name=(name)
598
+ File.rename(path, File.join(DIR, name))
599
+ @name = name
600
+ end
601
+
602
+ def n_rated
603
+ select { |x| !x.rating.nil? and x.rating > 0 }.length
604
+ end
605
+
606
+ def n_unrated
607
+ length - n_rated
608
+ end
609
+
610
+ def ==(object)
611
+ object.is_a?(self.class) && object.name == name
612
+ end
613
+
614
+ def copy_covers(somewhere)
615
+ FileUtils.rm_rf(somewhere) if File.exist?(somewhere)
616
+ FileUtils.mkdir(somewhere)
617
+ each do |book|
618
+ next unless File.exist?(cover(book))
619
+ FileUtils.cp(cover(book),
620
+ File.join(somewhere, final_cover(book)))
621
+ end
622
+ end
623
+
624
+ def self.jpeg?(file)
625
+ 'JFIF' == IO.read(file, 10)[6..9]
626
+ end
627
+
628
+ def final_cover(book)
629
+ # TODO what about PNG?
630
+ book.ident + (Library.jpeg?(cover(book)) ? '.jpg' : '.gif')
631
+ end
632
+
633
+ #########
634
+ protected
635
+ #########
636
+
637
+ def initialize(name)
638
+ @name = name
639
+ @deleted_books = []
640
+ end
641
+ end
642
+
643
+ class Libraries
644
+ attr_reader :all_libraries, :ruined_books, :deleted_books
645
+
646
+ include Observable
647
+ include Singleton
648
+
649
+ def reload
650
+ @all_libraries.clear
651
+ @all_libraries.concat(Library.loadall)
652
+ @all_libraries.concat(SmartLibrary.loadall)
653
+
654
+ ruined = []
655
+ deleted = []
656
+ all_regular_libraries.each {|library|
657
+ ruined += library.ruined_books
658
+ # make deleted books from each library accessible so we don't crash on smart libraries
659
+ deleted += library.deleted_books
660
+ }
661
+ @ruined_books = ruined
662
+ @deleted_books = deleted
663
+ end
664
+
665
+ def all_regular_libraries
666
+ @all_libraries.select { |x| x.is_a?(Library) }
667
+ end
668
+
669
+ def all_smart_libraries
670
+ @all_libraries.select { |x| x.is_a?(SmartLibrary) }
671
+ end
672
+
673
+ # def all_dynamic_libraries
674
+ # @all_libraries.select { |x| x.is_a?(SmartLibrary) }
675
+ # end
676
+
677
+ LIBRARY_ADDED, LIBRARY_REMOVED = 1, 2
678
+
679
+ def add_library(library)
680
+ @all_libraries << library
681
+ notify(LIBRARY_ADDED, library)
682
+ end
683
+
684
+ def remove_library(library)
685
+ @all_libraries.delete(library)
686
+ notify(LIBRARY_REMOVED, library)
687
+ end
688
+
689
+ def really_delete_deleted_libraries
690
+ Library.really_delete_deleted_libraries
691
+ SmartLibrary.really_delete_deleted_libraries
692
+ end
693
+
694
+ def really_save_all_books
695
+ all_regular_libraries.each do |library|
696
+ library.each { |book| library.save(book, true) }
697
+ end
698
+ end
699
+
700
+ #######
701
+ private
702
+ #######
703
+
704
+ def initialize
705
+ @all_libraries = []
706
+ @deleted_books = []
707
+ end
708
+
709
+ def notify(action, library)
710
+ changed
711
+ notify_observers(self, action, library)
712
+ end
713
+ end
714
+ end