alexandria-book-collection-manager 0.7.5 → 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +9 -0
  3. data/.github/workflows/ruby.yml +72 -0
  4. data/.gitignore +4 -1
  5. data/.rubocop.yml +65 -30
  6. data/.rubocop_todo.yml +49 -165
  7. data/.simplecov +5 -2
  8. data/CHANGELOG.md +64 -0
  9. data/ChangeLog.0 +19 -19
  10. data/INSTALL.md +26 -16
  11. data/README.md +31 -35
  12. data/Rakefile +18 -16
  13. data/alexandria-book-collection-manager.gemspec +35 -29
  14. data/doc/FAQ +2 -2
  15. data/doc/dependency_decisions.yml +22 -3
  16. data/lib/alexandria/about.rb +1 -1
  17. data/lib/alexandria/book_providers/bl_provider.rb +88 -0
  18. data/lib/alexandria/book_providers/douban.rb +2 -2
  19. data/lib/alexandria/book_providers/loc_provider.rb +38 -0
  20. data/lib/alexandria/book_providers/pseudomarc.rb +61 -71
  21. data/lib/alexandria/book_providers/sbn_provider.rb +108 -0
  22. data/lib/alexandria/book_providers/{thalia.rb → thalia_provider.rb} +37 -74
  23. data/lib/alexandria/book_providers/web.rb +2 -2
  24. data/lib/alexandria/book_providers/worldcat.rb +34 -38
  25. data/lib/alexandria/book_providers/z3950_provider.rb +199 -0
  26. data/lib/alexandria/book_providers.rb +48 -65
  27. data/lib/alexandria/default_preferences.rb +2 -1
  28. data/lib/alexandria/execution_queue.rb +13 -12
  29. data/lib/alexandria/export_library.rb +21 -22
  30. data/lib/alexandria/image_fetcher.rb +25 -0
  31. data/lib/alexandria/import_library.rb +46 -70
  32. data/lib/alexandria/import_library_csv.rb +16 -16
  33. data/lib/alexandria/library_sort_order.rb +3 -1
  34. data/lib/alexandria/library_store.rb +19 -20
  35. data/lib/alexandria/logging.rb +5 -9
  36. data/lib/alexandria/models/book.rb +15 -2
  37. data/lib/alexandria/models/library.rb +31 -35
  38. data/lib/alexandria/net.rb +1 -2
  39. data/lib/alexandria/preferences.rb +27 -33
  40. data/lib/alexandria/scanners/cue_cat.rb +6 -6
  41. data/lib/alexandria/scanners/keyboard.rb +1 -1
  42. data/lib/alexandria/scanners.rb +2 -2
  43. data/lib/alexandria/smart_library.rb +22 -26
  44. data/lib/alexandria/ui/about_dialog.rb +1 -1
  45. data/lib/alexandria/ui/acquire_dialog.rb +15 -19
  46. data/lib/alexandria/ui/alert_dialog.rb +36 -19
  47. data/lib/alexandria/ui/bad_isbns_dialog.rb +13 -9
  48. data/lib/alexandria/ui/barcode_animation.rb +6 -6
  49. data/lib/alexandria/ui/book_properties_dialog.rb +2 -3
  50. data/lib/alexandria/ui/book_properties_dialog_base.rb +35 -137
  51. data/lib/alexandria/ui/calendar_popup.rb +58 -0
  52. data/lib/alexandria/ui/callbacks.rb +144 -123
  53. data/lib/alexandria/ui/completion_models.rb +2 -6
  54. data/lib/alexandria/ui/confirm_erase_dialog.rb +1 -1
  55. data/lib/alexandria/ui/conflict_while_copying_dialog.rb +2 -2
  56. data/lib/alexandria/ui/error_dialog.rb +1 -1
  57. data/lib/alexandria/ui/export_dialog.rb +19 -18
  58. data/lib/alexandria/ui/icons.rb +34 -40
  59. data/lib/alexandria/ui/iconview_tooltips.rb +40 -53
  60. data/lib/alexandria/ui/import_dialog.rb +49 -48
  61. data/lib/alexandria/ui/init.rb +14 -12
  62. data/lib/alexandria/ui/keep_bad_isbn_dialog.rb +2 -2
  63. data/lib/alexandria/ui/libraries_combo.rb +10 -9
  64. data/lib/alexandria/ui/listview.rb +6 -7
  65. data/lib/alexandria/ui/main_app.rb +2 -2
  66. data/lib/alexandria/ui/multi_drag_treeview.rb +5 -7
  67. data/lib/alexandria/ui/new_book_dialog.rb +63 -65
  68. data/lib/alexandria/ui/new_book_dialog_manual.rb +1 -1
  69. data/lib/alexandria/ui/new_provider_dialog.rb +12 -11
  70. data/lib/alexandria/ui/new_smart_library_dialog.rb +39 -27
  71. data/lib/alexandria/ui/preferences_dialog.rb +25 -84
  72. data/lib/alexandria/ui/provider_preferences_base_dialog.rb +10 -6
  73. data/lib/alexandria/ui/provider_preferences_dialog.rb +5 -5
  74. data/lib/alexandria/ui/really_delete_dialog.rb +2 -2
  75. data/lib/alexandria/ui/sidepane_manager.rb +38 -38
  76. data/lib/alexandria/ui/skip_entry_dialog.rb +3 -2
  77. data/lib/alexandria/ui/smart_library_properties_dialog.rb +35 -36
  78. data/lib/alexandria/ui/smart_library_properties_dialog_base.rb +61 -244
  79. data/lib/alexandria/ui/smart_library_rule_box.rb +119 -0
  80. data/lib/alexandria/ui/sound.rb +4 -6
  81. data/lib/alexandria/ui/ui_manager.rb +80 -83
  82. data/lib/alexandria/ui.rb +7 -7
  83. data/lib/alexandria/version.rb +2 -2
  84. data/lib/alexandria/web_themes.rb +15 -15
  85. data/lib/alexandria.rb +2 -2
  86. data/po/cs.po +947 -865
  87. data/po/cy.po +913 -864
  88. data/po/de.po +961 -865
  89. data/po/el.po +956 -861
  90. data/po/es.po +952 -857
  91. data/po/fr.po +950 -865
  92. data/po/ga.po +866 -819
  93. data/po/gl.po +946 -861
  94. data/po/it.po +945 -858
  95. data/po/ja.po +921 -836
  96. data/po/mk.po +953 -858
  97. data/po/nb.po +932 -847
  98. data/po/nl.po +955 -849
  99. data/po/pl.po +999 -963
  100. data/po/pt.po +946 -850
  101. data/po/pt_BR.po +944 -859
  102. data/po/ru.po +959 -868
  103. data/po/sk.po +950 -863
  104. data/po/sv.po +944 -859
  105. data/po/uk.po +925 -846
  106. data/po/zh_TW.po +926 -841
  107. data/schemas/alexandria.schemas +1 -1
  108. data/share/alexandria/glade/main_app__builder.glade +6 -21
  109. data/share/gnome/help/alexandria/C/adding-books.xml +3 -4
  110. data/share/gnome/help/alexandria/C/introduction.xml +0 -16
  111. data/share/gnome/help/alexandria/C/searching.xml +1 -4
  112. data/share/gnome/help/alexandria/C/settings.xml +0 -30
  113. data/share/gnome/help/alexandria/C/smart-libraries.xml +2 -2
  114. data/share/gnome/help/alexandria/C/working-with-libraries.xml +1 -1
  115. data/share/gnome/help/alexandria/fr/alexandria.xml +5 -160
  116. data/share/gnome/help/alexandria/ja/adding-books.xml +1 -1
  117. data/share/gnome/help/alexandria/ja/introduction.xml +0 -15
  118. data/share/gnome/help/alexandria/ja/searching.xml +3 -7
  119. data/share/gnome/help/alexandria/ja/settings.xml +0 -27
  120. data/share/gnome/help/alexandria/ja/smart-libraries.xml +1 -1
  121. data/spec/alexandria/book_providers/bl_provider_spec.rb +13 -0
  122. data/spec/alexandria/book_providers/loc_provider_spec.rb +17 -0
  123. data/spec/alexandria/book_providers/sbn_provider_spec.rb +13 -0
  124. data/spec/alexandria/book_providers/thalia_provider_spec.rb +119 -0
  125. data/spec/alexandria/book_providers/world_cat_provider_spec.rb +160 -0
  126. data/spec/alexandria/book_providers_spec.rb +0 -154
  127. data/spec/alexandria/console_spec.rb +0 -5
  128. data/spec/alexandria/export_library_spec.rb +27 -38
  129. data/spec/alexandria/library_spec.rb +76 -46
  130. data/spec/alexandria/preferences_spec.rb +29 -3
  131. data/spec/alexandria/scanners/cue_cat_spec.rb +1 -1
  132. data/spec/alexandria/ui/about_dialog_spec.rb +1 -1
  133. data/spec/alexandria/ui/acquire_dialog_spec.rb +1 -1
  134. data/spec/alexandria/ui/alert_dialog_spec.rb +1 -1
  135. data/spec/alexandria/ui/bad_isbns_dialog_spec.rb +1 -1
  136. data/spec/alexandria/ui/book_properties_dialog_spec.rb +47 -5
  137. data/spec/alexandria/ui/confirm_erase_dialog_spec.rb +1 -1
  138. data/spec/alexandria/ui/conflict_while_copying_dialog_spec.rb +1 -1
  139. data/spec/alexandria/ui/error_dialog_spec.rb +1 -1
  140. data/spec/alexandria/ui/export_dialog_spec.rb +25 -4
  141. data/spec/alexandria/ui/icons_spec.rb +26 -0
  142. data/spec/alexandria/ui/iconview_spec.rb +1 -1
  143. data/spec/alexandria/ui/import_dialog_spec.rb +35 -3
  144. data/spec/alexandria/ui/keep_bad_isbn_dialog_spec.rb +1 -1
  145. data/spec/alexandria/ui/main_app_spec.rb +1 -1
  146. data/spec/alexandria/ui/new_book_dialog_manual_spec.rb +39 -3
  147. data/spec/alexandria/ui/new_provider_dialog_spec.rb +19 -3
  148. data/spec/alexandria/ui/new_smart_library_dialog_spec.rb +28 -3
  149. data/spec/alexandria/ui/preferences_dialog_spec.rb +2 -2
  150. data/spec/alexandria/ui/provider_preferences_dialog_spec.rb +23 -8
  151. data/spec/alexandria/ui/really_delete_dialog_spec.rb +1 -1
  152. data/spec/alexandria/ui/sidepane_manager_spec.rb +2 -2
  153. data/spec/alexandria/ui/skip_entry_dialog_spec.rb +1 -1
  154. data/spec/alexandria/ui/smart_library_properties_dialog_spec.rb +37 -6
  155. data/spec/alexandria/ui/ui_manager_spec.rb +116 -2
  156. data/spec/data/libraries/0.6.2/My Library/9780571147168.yaml +2 -0
  157. data/spec/end_to_end/basic_run_spec.rb +3 -8
  158. data/spec/fixtures/cover.jpg +0 -0
  159. data/spec/spec_helper.rb +47 -3
  160. data/tasks/spec.rake +3 -5
  161. data/util/rake/fileinstall.rb +16 -15
  162. data/util/rake/omfgenerate.rb +1 -1
  163. metadata +141 -52
  164. data/.travis.yml +0 -39
  165. data/lib/alexandria/book_providers/adlibris.rb +0 -196
  166. data/lib/alexandria/book_providers/amazon_aws.rb +0 -252
  167. data/lib/alexandria/book_providers/amazon_ecs_util.rb +0 -388
  168. data/lib/alexandria/book_providers/barnes_and_noble.rb +0 -209
  169. data/lib/alexandria/book_providers/proxis.rb +0 -175
  170. data/lib/alexandria/book_providers/siciliano.rb +0 -257
  171. data/lib/alexandria/book_providers/z3950.rb +0 -415
  172. data/spec/alexandria/ui/ui_utilities_spec.rb +0 -62
  173. data/spec/alexandria/utilities_spec.rb +0 -52
@@ -1,252 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright (C) 2004-2006 Laurent Sansonetti
4
- # Copyright (C) 2008 Cathal Mc Ginley
5
- # Copyright (C) 2014, 2016 Matijs van Zuijlen
6
- #
7
- # Alexandria is free software; you can redistribute it and/or
8
- # modify it under the terms of the GNU General Public License as
9
- # published by the Free Software Foundation; either version 2 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # Alexandria is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
- # General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public
18
- # License along with Alexandria; see the file COPYING. If not,
19
- # write to the Free Software Foundation, Inc., 51 Franklin Street,
20
- # Fifth Floor, Boston, MA 02110-1301 USA.
21
-
22
- # http://en.wikipedia.org/wiki/Amazon
23
-
24
- require "hpricot"
25
- require "alexandria/book_providers/amazon_ecs_util"
26
-
27
- module Alexandria
28
- class BookProviders
29
- class AmazonProvider < GenericProvider
30
- include Logging
31
- include GetText
32
- GetText.bindtextdomain(Alexandria::TEXTDOMAIN, charset: "UTF-8")
33
-
34
- # CACHE_DIR = File.join(Alexandria::Library::DIR, '.amazon_cache')
35
-
36
- LOCALES = ["ca", "de", "fr", "jp", "uk", "us"].freeze
37
-
38
- def initialize
39
- super("Amazon", "Amazon")
40
- # prefs.add("enabled", _("Enabled"), true, [true,false])
41
- prefs.add("locale", _("Locale"), "us", AmazonProvider::LOCALES)
42
- prefs.add("dev_token", _("Access key ID"), "")
43
- prefs.add("secret_key", _("Secret access key"), "")
44
- prefs.add("associate_tag", _("Associate Tag"), "")
45
-
46
- prefs.read
47
- token = prefs.variable_named("dev_token")
48
- # kill old (shorter) tokens, or previously distributed Access Key Id (see #26250)
49
-
50
- if token
51
- token.new_value = token.value.strip if token.value != token.value.strip
52
- end
53
- if token && ((token.value.size != 20) || (token.value == "0J356Z09CN88KB743582"))
54
- token.new_value = ""
55
- end
56
-
57
- secret = prefs.variable_named("secret_key")
58
- if secret
59
- secret.new_value = secret.value.strip if secret.value != secret.value.strip
60
- end
61
-
62
- associate = prefs.variable_named("associate_tag")
63
- if associate
64
- associate.new_value = "rubyalexa-20" if associate.value.strip.empty?
65
- if associate.value != associate.value.strip
66
- associate.new_value = associate.value.strip
67
- end
68
- end
69
- end
70
-
71
- def search(criterion, type)
72
- prefs.read
73
-
74
- if prefs["secret_key"].empty?
75
- raise(Amazon::RequestError,
76
- "Secret Access Key required for Authentication:" \
77
- " you must sign up for your own Amazon AWS account")
78
- end
79
-
80
- if (config = Alexandria::Preferences.instance.http_proxy_config)
81
- host, port, user, pass = config
82
- url = "http://"
83
- url += user + ":" + pass + "@" if user && pass
84
- url += host + ":" + port.to_s
85
- ENV["http_proxy"] = url
86
- end
87
-
88
- access_key_id = prefs["dev_token"]
89
-
90
- Amazon::Ecs.options = { aWS_access_key_id: access_key_id,
91
- associateTag: prefs["associate_tag"] }
92
- Amazon::Ecs.secret_access_key = prefs["secret_key"]
93
- # #req.cache = Amazon::Search::Cache.new(CACHE_DIR)
94
- locales = AmazonProvider::LOCALES.dup
95
- locales.delete prefs["locale"]
96
- locales.unshift prefs["locale"]
97
- locales.reverse!
98
-
99
- begin
100
- request_locale = locales.pop.intern
101
- products = []
102
- case type
103
- when SEARCH_BY_ISBN
104
- criterion = Library.canonicalise_isbn(criterion)
105
- # This isn't ideal : I'd like to do an ISBN/EAN-specific search
106
- res = Amazon::Ecs.item_search(criterion,
107
- response_group: "ItemAttributes,Images",
108
- country: request_locale)
109
- res.items.each do |item|
110
- products << item
111
- end
112
- # #req.asin_search(criterion) do |product|
113
-
114
- # Shouldn't happen.
115
- # raise TooManyResultsError if products.length > 1
116
-
117
- # I had assumed that publishers were bogusly publishing
118
- # multiple editions of a book with the same ISBN, and
119
- # Amazon was distinguishing between them. So we'll log
120
- # this case, and arbitrarily return the FIRST item
121
-
122
- # Actually, this may be due to Amazon recommending a
123
- # preferred later edition of a book, in spite of our
124
- # searching on a single ISBN it can return more than one
125
- # result with different ISBNs
126
-
127
- if products.length > 1
128
- log.warn do
129
- "ISBN search at Amazon[#{request_locale}] got #{products.length} results;" \
130
- " returning the first result only"
131
- end
132
- end
133
-
134
- when SEARCH_BY_TITLE
135
- res = Amazon::Ecs.item_search(criterion,
136
- response_group: "ItemAttributes,Images",
137
- country: request_locale)
138
-
139
- res.items.each do |item|
140
- products << item if /#{criterion}/i.match?(item.get("itemattributes/title"))
141
- end
142
- # #req.keyword_search(criterion) do |product|
143
-
144
- when SEARCH_BY_AUTHORS
145
- criterion = "author:#{criterion}"
146
- res = Amazon::Ecs.item_search(criterion,
147
- response_group: "ItemAttributes,Images",
148
- country: request_locale, type: "Power")
149
- res.items.each do |item|
150
- products << item
151
- end
152
- # #req.author_search(criterion) do |product|
153
-
154
- when SEARCH_BY_KEYWORD
155
- res = Amazon::Ecs.item_search(criterion,
156
- response_group: "ItemAttributes,Images",
157
- country: request_locale)
158
-
159
- res.items.each do |item|
160
- products << item
161
- end
162
-
163
- else
164
- raise InvalidSearchTypeError
165
- end
166
- raise Amazon::RequestError, "No products" if products.empty?
167
- # raise NoResultsError if products.empty?
168
- rescue Amazon::RequestError => ex
169
- log.debug { "Got Amazon::RequestError at #{request_locale}: #{ex}" }
170
- retry unless locales.empty?
171
- raise NoResultsError
172
- end
173
-
174
- results = []
175
- products.each do |item|
176
- next unless item.get("itemattributes/productgroup") == "Book"
177
-
178
- atts = item.search_and_convert("itemattributes")
179
- title = normalize(atts.get("title"))
180
-
181
- media = normalize(atts.get("binding"))
182
- media = nil if media == "Unknown Binding"
183
-
184
- isbn = normalize(atts.get("isbn"))
185
- isbn = (Library.canonicalise_ean(isbn) if isbn && Library.valid_isbn?(isbn))
186
- # hack, extract year by regexp (not Y10K compatible :-)
187
- /([1-9][0-9]{3})/ =~ atts.get("publicationdate")
188
- publishing_year = Regexp.last_match[1] ? Regexp.last_match[1].to_i : nil
189
- book = Book.new(title,
190
- atts.get_array("author").map { |x| normalize(x) },
191
- isbn,
192
- normalize(atts.get("manufacturer")),
193
- publishing_year,
194
- media)
195
-
196
- image_url = item.get("mediumimage/url")
197
- log.info { "Found at Amazon[#{request_locale}]: #{book.title}" }
198
- results << [book, image_url]
199
- end
200
- if type == SEARCH_BY_ISBN
201
- if results.size == 1
202
- results.first
203
- else
204
- log.info { "Found multiple results for lookup: checking each" }
205
- query_isbn_canon = Library.canonicalise_ean(criterion)
206
- results.each do |rslt|
207
- book = rslt[0]
208
- book_isbn_canon = Library.canonicalise_ean(book.isbn)
209
- return rslt if query_isbn_canon == book_isbn_canon
210
-
211
- log.debug { "rejected possible result #{book}" }
212
- end
213
- # gone through all and no ISBN match, so just return first result
214
- log.info do
215
- "no more results to check. Returning first result, just an approximation"
216
- end
217
- results.first
218
- end
219
- else
220
- results
221
- end
222
- end
223
-
224
- def url(book)
225
- isbn = Library.canonicalise_isbn(book.isbn)
226
- url = case prefs["locale"]
227
- when "fr"
228
- "http://www.amazon.fr/exec/obidos/ASIN/%s"
229
- when "uk"
230
- "http://www.amazon.co.uk/exec/obidos/ASIN/%s"
231
- when "de"
232
- "http://www.amazon.de/exec/obidos/ASIN/%s"
233
- when "ca"
234
- "http://www.amazon.ca/exec/obidos/ASIN/%s"
235
- when "jp"
236
- "http://www.amazon.jp/exec/obidos/ASIN/%s"
237
- when "us"
238
- "http://www.amazon.com/exec/obidos/ASIN/%s"
239
- end
240
- url % isbn
241
- rescue StandardError => ex
242
- log.warn { "Cannot create url for book #{book}; #{ex.message}" }
243
- nil
244
- end
245
-
246
- def normalize(str)
247
- str = str.squeeze(" ").strip unless str.nil?
248
- str
249
- end
250
- end
251
- end
252
- end
@@ -1,388 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file is part of Alexandria.
4
- #
5
- # See the file README.md for authorship and licensing information.
6
-
7
- require "net/http"
8
- require "hpricot"
9
- require "cgi"
10
-
11
- require "digest/sha2"
12
-
13
- module Amazon
14
- class RequestError < StandardError; end
15
-
16
- class Ecs
17
- include Alexandria::Logging
18
-
19
- SERVICE_URLS = {
20
- us: "http://webservices.amazon.com/onca/xml?Service=AWSECommerceService",
21
- uk: "http://webservices.amazon.co.uk/onca/xml?Service=AWSECommerceService",
22
- ca: "http://webservices.amazon.ca/onca/xml?Service=AWSECommerceService",
23
- de: "http://webservices.amazon.de/onca/xml?Service=AWSECommerceService",
24
- jp: "http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService",
25
- fr: "http://webservices.amazon.fr/onca/xml?Service=AWSECommerceService"
26
- }.freeze
27
-
28
- @@options = {}
29
- @@debug = false
30
-
31
- @@secret_access_key = ""
32
-
33
- # Default search options
34
- def self.options
35
- @@options
36
- end
37
-
38
- def self.secret_access_key=(key)
39
- @@secret_access_key = key
40
- end
41
-
42
- # Set default search options
43
- def self.options=(opts)
44
- @@options = opts
45
- end
46
-
47
- # Get debug flag.
48
- def self.debug
49
- @@debug
50
- end
51
-
52
- # Set debug flag to true or false.
53
- def self.debug=(dbg)
54
- @@debug = dbg
55
- end
56
-
57
- def self.configure(&_proc)
58
- raise ArgumentError, "Block is required." unless block_given?
59
-
60
- yield @@options
61
- end
62
-
63
- # Search amazon items with search terms. Default search index option is 'Books'.
64
- # For other search type other than keywords, please specify
65
- # :type => [search type param name].
66
- def self.item_search(terms, opts = {})
67
- opts[:operation] = "ItemSearch"
68
- opts[:search_index] = opts[:search_index] || "Books"
69
-
70
- type = opts.delete(:type)
71
- if type
72
- opts[type.to_sym] = terms
73
- else
74
- opts[:keywords] = terms
75
- end
76
-
77
- send_request(opts)
78
- end
79
-
80
- # Search an item by ASIN no.
81
- def self.item_lookup(item_id, opts = {})
82
- opts[:operation] = "ItemLookup"
83
- opts[:item_id] = item_id
84
-
85
- send_request(opts)
86
- end
87
-
88
- # HACK : copied from book_providers.rb
89
- def self.transport
90
- config = Alexandria::Preferences.instance.http_proxy_config
91
- config ? Net::HTTP.Proxy(*config) : Net::HTTP
92
- end
93
-
94
- # Generic send request to ECS REST service. You have to specify the
95
- # :operation parameter.
96
- def self.send_request(opts)
97
- opts = options.merge(opts) if options
98
- request_url = prepare_url(opts)
99
- log.debug { "Request URL: #{request_url}" }
100
-
101
- res = transport.get_response(URI.parse(request_url))
102
- unless res.is_a? Net::HTTPSuccess
103
- raise Amazon::RequestError, "HTTP Response: #{res.code} #{res.message}"
104
- end
105
-
106
- Response.new(res.body)
107
- end
108
-
109
- # Response object returned after a REST call to Amazon service.
110
- class Response
111
- # XML input is in string format
112
- def initialize(xml)
113
- @doc = Hpricot(xml)
114
- end
115
-
116
- # Return Hpricot object.
117
- attr_reader :doc
118
-
119
- # Return true if request is valid.
120
- def is_valid_request?
121
- (@doc / "isvalid").inner_html == "True"
122
- end
123
-
124
- # Return true if response has an error.
125
- def has_error?
126
- !(error.nil? || error.empty?)
127
- end
128
-
129
- # Return error message.
130
- def error
131
- Element.get(@doc, "error/message")
132
- end
133
-
134
- # Return an array of Amazon::Element item objects.
135
- def items
136
- @items ||= (@doc / "item").map { |item| Element.new(item) }
137
- @items
138
- end
139
-
140
- # Return the first item (Amazon::Element)
141
- def first_item
142
- items.first
143
- end
144
-
145
- # Return current page no if :item_page option is when initiating the request.
146
- def item_page
147
- @item_page ||= (@doc / "itemsearchrequest/itempage").inner_html.to_i
148
- @item_page
149
- end
150
-
151
- # Return total results.
152
- def total_results
153
- @total_results ||= (@doc / "totalresults").inner_html.to_i
154
- @total_results
155
- end
156
-
157
- # Return total pages.
158
- def total_pages
159
- @total_pages ||= (@doc / "totalpages").inner_html.to_i
160
- @total_pages
161
- end
162
- end
163
-
164
- # protected
165
- # def self.log(s)
166
- # return unless self.debug
167
- # if defined? RAILS_DEFAULT_LOGGER
168
- # RAILS_DEFAULT_LOGGER.error(s)
169
- # elsif defined? LOGGER
170
- # LOGGER.error(s)
171
- # else
172
- # puts s
173
- # end
174
- # end
175
-
176
- def self.prepare_url(opts)
177
- country = opts.delete(:country)
178
- country = country.nil? ? "us" : country
179
- request_url = SERVICE_URLS[country.to_sym]
180
- raise Amazon::RequestError, "Invalid country '#{country}'" unless request_url
181
-
182
- qs = ""
183
- opts.each do |k, v|
184
- next unless v
185
-
186
- v = v.join(",") if v.is_a? Array
187
- qs << "&#{camelize(k.to_s)}=#{URI.encode(v.to_s)}"
188
- end
189
- url = "#{request_url}#{qs}"
190
- # puts ">>> base url >> #{url}"
191
- signed_url = sign_request(url)
192
- # puts ">>> SIGNED >> #{signed_url}"
193
- signed_url
194
- end
195
-
196
- def self.camelize(s)
197
- s.to_s
198
- .gsub(%r{/(.?)}) { "::" + Regexp.last_match[1].upcase }
199
- .gsub(/(^|_)(.)/) { Regexp.last_match[2].upcase }
200
- end
201
-
202
- def self.hmac_sha256(message, key)
203
- block_size = 64
204
- ipad = "\x36" * block_size
205
- opad = "\x5c" * block_size
206
- if key.size > block_size
207
- d = Digest::SHA256.new
208
- key = d.digest(key)
209
- end
210
-
211
- ipad_bytes = ipad.bytes.map { |b| b }
212
- opad_bytes = opad.bytes.map { |b| b }
213
- key_bytes = key.bytes.map { |b| b }
214
- ipad_xor = ""
215
- opad_xor = ""
216
- (0..key.size - 1).each do |i|
217
- ipad_xor << (ipad_bytes[i] ^ key_bytes[i])
218
- opad_xor << (opad_bytes[i] ^ key_bytes[i])
219
- end
220
-
221
- ipad = ipad_xor + ipad[key.size..-1]
222
- opad = opad_xor + opad[key.size..-1]
223
-
224
- # inner hash
225
- d1 = Digest::SHA256.new
226
- d1.update(ipad)
227
- d1.update(message)
228
- msg_hash = d1.digest
229
-
230
- # outer hash
231
- d2 = Digest::SHA256.new
232
- d2.update(opad)
233
- d2.update(msg_hash)
234
- d2.digest
235
- end
236
-
237
- def self.sign_request(request)
238
- raise AmazonNotConfiguredError unless @@secret_access_key
239
-
240
- # Step 0 : Split apart request string
241
- url_pattern = %r{http://([^/]+)(/[^\?]+)\?(.*$)}
242
- url_pattern =~ request
243
- host = Regexp.last_match[1]
244
- path = Regexp.last_match[2]
245
- param_string = Regexp.last_match[3]
246
-
247
- # Step 1: enter the timestamp
248
- t = Time.now.getutc # MUST be in UTC
249
- stamp = t.strftime("%Y-%m-%dT%H:%M:%SZ")
250
- param_string += "&Timestamp=#{stamp}"
251
-
252
- # Step 2 : URL-encode
253
- param_string = param_string.gsub(",", "%2C").gsub(":", "%3A")
254
- # NOTE : take care not to double-encode
255
-
256
- # Step 3 : Split the parameter/value pairs
257
- params = param_string.split("&")
258
-
259
- # Step 4 : Sort params
260
- params.sort!
261
-
262
- # Step 5 : Rejoin the param string
263
- canonical_param_string = params.join("&")
264
-
265
- # Steps 6 & 7: Prepend HTTP request info
266
- string_to_sign = "GET\n#{host}\n#{path}\n#{canonical_param_string}"
267
-
268
- # puts string_to_sign
269
-
270
- # Step 8 : Calculate RFC 2104-compliant HMAC with SHA256 hash algorithm
271
- sig = hmac_sha256(string_to_sign, @@secret_access_key)
272
- base64_sig = [sig].pack("m").strip
273
-
274
- # Step 9 : URL-encode + and = in sig
275
- base64_sig = CGI.escape(base64_sig)
276
-
277
- # Step 10 : Add the URL encoded signature to your request
278
- "http://#{host}#{path}?#{param_string}&Signature=#{base64_sig}"
279
- end
280
- end
281
-
282
- # Internal wrapper class to provide convenient method to access Hpricot element value.
283
- class Element
284
- # Pass Hpricot::Elements object
285
- def initialize(element)
286
- @element = element
287
- end
288
-
289
- # Returns Hpricot::Elments object
290
- def elem
291
- @element
292
- end
293
-
294
- # Find Hpricot::Elements matching the given path. Example: element/"author".
295
- def /(path)
296
- elements = @element / path
297
- return nil if elements.empty?
298
-
299
- elements
300
- end
301
-
302
- # Find Hpricot::Elements matching the given path, and convert to Amazon::Element.
303
- # Returns an array Amazon::Elements if more than Hpricot::Elements size is
304
- # greater than 1.
305
- def search_and_convert(path)
306
- elements = self./(path)
307
- return unless elements
308
-
309
- elements = elements.map { |element| Element.new(element) }
310
- return elements.first if elements.size == 1
311
-
312
- elements
313
- end
314
-
315
- # Get the text value of the given path, leave empty to retrieve current element value.
316
- def get(path = "")
317
- Element.get(@element, path)
318
- end
319
-
320
- # Get the unescaped HTML text of the given path.
321
- def get_unescaped(path = "")
322
- Element.get_unescaped(@element, path)
323
- end
324
-
325
- # Get the array values of the given path.
326
- def get_array(path = "")
327
- Element.get_array(@element, path)
328
- end
329
-
330
- # Get the children element text values in hash format with the element
331
- # names as the hash keys.
332
- def get_hash(path = "")
333
- Element.get_hash(@element, path)
334
- end
335
-
336
- # Similar to #get, except an element object must be passed-in.
337
- def self.get(element, path = "")
338
- return unless element
339
-
340
- result = element.at(path)
341
- ## inner_html doesn't decode entities, hence bug #21659
342
- # result = result.inner_html if result
343
- result = result.inner_text if result
344
- result
345
- end
346
-
347
- # Similar to #get_unescaped, except an element object must be passed-in.
348
- def self.get_unescaped(element, path = "")
349
- result = get(element, path)
350
- CGI.unescapeHTML(result) if result
351
- end
352
-
353
- # Similar to #get_array, except an element object must be passed-in.
354
- def self.get_array(element, path = "")
355
- return unless element
356
-
357
- result = element / path
358
- if (result.is_a? Hpricot::Elements) || (result.is_a? Array)
359
- parsed_result = []
360
- result.each do |item|
361
- parsed_result << Element.get(item)
362
- end
363
- parsed_result
364
- else
365
- [Element.get(result)]
366
- end
367
- end
368
-
369
- # Similar to #get_hash, except an element object must be passed-in.
370
- def self.get_hash(element, path = "")
371
- return unless element
372
-
373
- result = element.at(path)
374
- if result
375
- hash = {}
376
- result = result.children
377
- result.each do |item|
378
- hash[item.name.to_sym] = item.inner_html
379
- end
380
- hash
381
- end
382
- end
383
-
384
- def to_s
385
- elem&.to_s
386
- end
387
- end
388
- end