rss-opds 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +0 -2
- data/README.markdown +64 -5
- data/Rakefile +8 -0
- data/examples/make-catalog.rb +138 -0
- data/lib/rss/maker/opds.rb +83 -0
- data/lib/rss/opds.rb +109 -19
- data/lib/rss/opds/version.rb +1 -1
- data/rss-opds.gemspec +8 -4
- data/test/helper.rb +19 -0
- data/test/test_maker.rb +227 -0
- metadata +60 -23
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -2,7 +2,7 @@ RSS::OPDS
|
|
2
2
|
=========
|
3
3
|
|
4
4
|
[OPDS][opds] parser and maker.
|
5
|
-
OPDS(Open Publication Distribution System) is feed
|
5
|
+
OPDS(Open Publication Distribution System) is feed which syndicates information about ebooks.
|
6
6
|
|
7
7
|
[opds]:http://opds-spec.org/specs/opds-catalog-1-1
|
8
8
|
|
@@ -26,15 +26,74 @@ Or install it yourself as:
|
|
26
26
|
Usage
|
27
27
|
-----
|
28
28
|
|
29
|
+
### Parsing OPDS
|
30
|
+
|
29
31
|
require 'open-uri'
|
30
32
|
require 'rss/opds'
|
31
33
|
|
32
34
|
opds = RSS::Parser.parse open(uri)
|
33
35
|
opds.entries.each do |entry|
|
34
|
-
|
35
|
-
|
36
|
+
price_elem = entry.links.select {|link| link.opds_price}.first
|
37
|
+
puts price_elem
|
36
38
|
end
|
37
39
|
|
40
|
+
If you need performance, install [rss-nokogiri][rss-nokogiri] gem and write `require 'rss/nokogiri'`
|
41
|
+
before calling `RSS::Parser.parse`.
|
42
|
+
|
43
|
+
[rss-nokogiri]: https://rubygems.org/gems/rss-nokogiri
|
44
|
+
|
45
|
+
### Making OPDS
|
46
|
+
|
47
|
+
You may make OPDS feeds as well as normal Atom feeds.
|
48
|
+
|
49
|
+
require 'rss/opds'
|
50
|
+
|
51
|
+
recent = RSS::Maker.make('atom') do |maker|
|
52
|
+
maker.channel.about = 'http://example.net/'
|
53
|
+
maker.channel.title = 'Example New Catalog'
|
54
|
+
maker.channel.description = 'New books in this site'
|
55
|
+
maker.channel.links.new_link do |link|
|
56
|
+
link.href = 'http://example.net/new.opds'
|
57
|
+
link.rel = 'self'
|
58
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
59
|
+
end
|
60
|
+
maker.channel.links.new_link do |link|
|
61
|
+
link.href = 'http://example.net/root.opds'
|
62
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
63
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
64
|
+
end
|
65
|
+
maker.channel.updated = '2012-08-14T04:23:00'
|
66
|
+
maker.channel.author = 'KITAITI Makoto'
|
67
|
+
|
68
|
+
new_books.each do |book|
|
69
|
+
maker.items.new_item do |entry|
|
70
|
+
entry.title = book.title
|
71
|
+
entry.updated = book.updated
|
72
|
+
entry.summary = book.summary
|
73
|
+
entry.link = book.link
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
puts recent # => output OPDS feed
|
78
|
+
|
79
|
+
And now, this library provides utility methods which help you make OPDS navigation feeds.
|
80
|
+
|
81
|
+
root = RSS::Maker.make('atom') {|maker|
|
82
|
+
maker.channel.about = ...
|
83
|
+
|
84
|
+
maker.items.add_relation recent, RSS::OPDS::RELATIONS['new']
|
85
|
+
# or just:
|
86
|
+
# maker.items.add_new recent
|
87
|
+
#
|
88
|
+
# these are equivalent to:
|
89
|
+
# maker.items.new_item do |item|
|
90
|
+
# item.id = recent.id
|
91
|
+
# item.title = recent.title
|
92
|
+
# # ...
|
93
|
+
# end
|
94
|
+
}
|
95
|
+
puts root # => output XML including entry with 'new' sorting relation
|
96
|
+
|
38
97
|
Contributing
|
39
98
|
------------
|
40
99
|
|
@@ -47,10 +106,10 @@ Contributing
|
|
47
106
|
Development Plan
|
48
107
|
----------------
|
49
108
|
|
50
|
-
1. Parser for OPDS catalogs
|
109
|
+
1. Parser for OPDS catalogs[DONE]
|
51
110
|
2. Validation as OPDS
|
52
111
|
3. Utility methods like, for example, `#buy`(gets `link` element with `rel="acquisition/buy"`), `#price`(`opds:price` element) and so on
|
53
|
-
4. Maker for OPDS feeds and entries
|
112
|
+
4. Maker for OPDS feeds and entries[DONE]
|
54
113
|
|
55
114
|
References
|
56
115
|
----------
|
data/Rakefile
CHANGED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'rss'
|
2
|
+
require 'rss/opds'
|
3
|
+
require 'rss/maker/opds'
|
4
|
+
|
5
|
+
Book = Struct.new 'Book',
|
6
|
+
:title, :author, :summary, :updated, :link, :popularity
|
7
|
+
|
8
|
+
def main
|
9
|
+
puts root
|
10
|
+
puts popular
|
11
|
+
puts recent
|
12
|
+
end
|
13
|
+
|
14
|
+
def root
|
15
|
+
RSS::Maker.make('atom') {|maker|
|
16
|
+
maker.channel.about = 'http://example.net/'
|
17
|
+
maker.channel.title = 'Example Catalog Root'
|
18
|
+
maker.channel.description = 'Sample OPDS'
|
19
|
+
maker.channel.links.new_link do |link|
|
20
|
+
link.href = 'http://example.net/root.opds'
|
21
|
+
link.rel = RSS::OPDS::RELATIONS['self']
|
22
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
23
|
+
end
|
24
|
+
maker.channel.links.new_link do |link|
|
25
|
+
link.href = 'http://example.net/root.opds'
|
26
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
27
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
28
|
+
end
|
29
|
+
maker.channel.updated = '2012-08-14T04:23:00'
|
30
|
+
maker.channel.author = 'KITAITI Makoto'
|
31
|
+
|
32
|
+
maker.items.new_item do |entry|
|
33
|
+
entry.links.new_link do |link|
|
34
|
+
link.href = 'http://example.net/popular.opds'
|
35
|
+
link.rel = RSS::OPDS::RELATIONS['popular']
|
36
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
37
|
+
end
|
38
|
+
entry.title = 'Example Popular Books'
|
39
|
+
entry.updated = '2012-07-31T00:00:00'
|
40
|
+
entry.summary = 'Popular books in this site'
|
41
|
+
end
|
42
|
+
maker.items.new_item do |entry|
|
43
|
+
entry.links.new_link do |link|
|
44
|
+
link.href = 'http://example.net/new.opds'
|
45
|
+
link.rel = RSS::OPDS::RELATIONS['new']
|
46
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
47
|
+
end
|
48
|
+
entry.title = 'Example New Books'
|
49
|
+
entry.updated = '2012-08-14T04:23:00'
|
50
|
+
entry.summary = 'New books in this site'
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def popular
|
56
|
+
RSS::Maker.make('atom') do |maker|
|
57
|
+
maker.channel.about = 'http://example.net/'
|
58
|
+
maker.channel.title = 'Example Popular Catalog'
|
59
|
+
maker.channel.description = 'Popular books in this site'
|
60
|
+
maker.channel.links.new_link do |link|
|
61
|
+
link.href = 'http://example.net/popular.opds'
|
62
|
+
link.rel = RSS::OPDS::RELATIONS['self']
|
63
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
64
|
+
end
|
65
|
+
maker.channel.links.new_link do |link|
|
66
|
+
link.href = 'http://example.net/root.opds'
|
67
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
68
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
69
|
+
end
|
70
|
+
maker.channel.updated = '2012-07-31T00:00:00'
|
71
|
+
maker.channel.author = 'KITAITI Makoto'
|
72
|
+
|
73
|
+
popular_books.each do |book|
|
74
|
+
maker.items.new_item do |entry|
|
75
|
+
entry.title = book.title
|
76
|
+
entry.updated = book.updated
|
77
|
+
entry.summary = book.summary
|
78
|
+
entry.link = book.link
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def recent
|
85
|
+
RSS::Maker.make('atom') do |maker|
|
86
|
+
maker.channel.about = 'http://example.net/'
|
87
|
+
maker.channel.title = 'Example New Catalog'
|
88
|
+
maker.channel.description = 'New books in this site'
|
89
|
+
maker.channel.links.new_link do |link|
|
90
|
+
link.href = 'http://example.net/new.opds'
|
91
|
+
link.rel = 'self'
|
92
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
93
|
+
end
|
94
|
+
maker.channel.links.new_link do |link|
|
95
|
+
link.href = 'http://example.net/root.opds'
|
96
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
97
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
98
|
+
end
|
99
|
+
maker.channel.updated = '2012-08-14T04:23:00'
|
100
|
+
maker.channel.author = 'KITAITI Makoto'
|
101
|
+
|
102
|
+
new_books.each do |book|
|
103
|
+
maker.items.new_item do |entry|
|
104
|
+
entry.title = book.title
|
105
|
+
entry.updated = book.updated
|
106
|
+
entry.summary = book.summary
|
107
|
+
|
108
|
+
entry.links.new_link do |link|
|
109
|
+
link.href = book.link
|
110
|
+
link.opds_prices.new_opds_price do |price|
|
111
|
+
price.value = 100
|
112
|
+
price.currencycode = 'JPY'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def books
|
121
|
+
[
|
122
|
+
Book.new('book1', 'author1', 'summary1', Time.gm(2012, 8, 14, 3), 'http://example.net/book/book1', 1),
|
123
|
+
Book.new('book2', 'author2', 'summary2', Time.gm(2012, 8, 10, 14), 'http://example.net/book/book2', 2),
|
124
|
+
Book.new('book3', 'author3', 'summary3', Time.gm(2012, 6, 30, 12), 'http://example.net/book/book3', 3)
|
125
|
+
]
|
126
|
+
end
|
127
|
+
|
128
|
+
def popular_books
|
129
|
+
books.sort_by {|book| book.popularity}.reverse_each
|
130
|
+
end
|
131
|
+
|
132
|
+
def new_books
|
133
|
+
books.sort_by {|book| book.updated}.reverse_each
|
134
|
+
end
|
135
|
+
|
136
|
+
if $0 == __FILE__
|
137
|
+
main
|
138
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'rss/maker'
|
2
|
+
require 'rss/opds'
|
3
|
+
|
4
|
+
module RSS
|
5
|
+
module Maker
|
6
|
+
module Atom
|
7
|
+
class Feed < RSSBase
|
8
|
+
class Items < ItemsBase
|
9
|
+
class Item < ItemBase
|
10
|
+
class Links < LinksBase
|
11
|
+
class Link < LinkBase
|
12
|
+
%w[facetGroup activeFacet].each do |attr|
|
13
|
+
def_other_element attr
|
14
|
+
end
|
15
|
+
def_classed_elements 'opds_price', 'value', 'Prices'
|
16
|
+
|
17
|
+
# @note Defined to prevent NoMethodError
|
18
|
+
def setup_opds_prices(feed, current)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @note Should provide this method as the one of a module
|
22
|
+
def to_feed(feed, current)
|
23
|
+
super # AtomLink#to_feed
|
24
|
+
opds_prices.to_feed(feed, current.links.last)
|
25
|
+
end
|
26
|
+
|
27
|
+
class Prices < Base
|
28
|
+
def_array_element 'opds_price', nil, 'Price'
|
29
|
+
|
30
|
+
class Price < Base
|
31
|
+
%w[value currencycode].each do |attr|
|
32
|
+
attr_accessor attr
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_feed(feed, current)
|
36
|
+
price = ::RSS::OPDS::Price.new
|
37
|
+
price.value = value
|
38
|
+
price.currencycode = currencycode
|
39
|
+
current.opds_prices << price
|
40
|
+
set_parent price, current
|
41
|
+
setup_other_elements(feed)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def required_variable_names
|
47
|
+
%w[currencycode]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_relation(feed, relation, href=nil)
|
56
|
+
self_link = maker.channel.links.find {|link| link.rel == RSS::OPDS::RELATIONS['self']}
|
57
|
+
is_navigation_feed = self_link && self_link.type == RSS::OPDS::TYPES['navigation']
|
58
|
+
raise TypeError, 'Only navigatfion feed can accept feed' unless is_navigation_feed
|
59
|
+
raise ArgumentError, 'Only acquisition feed can be accepted' unless feed.acquisition_feed?
|
60
|
+
new_item do |entry|
|
61
|
+
[:id, :title, :dc_description, :updated].each do |attr|
|
62
|
+
val = feed.__send__(attr)
|
63
|
+
entry.__send__("#{attr}=", val.content) if val
|
64
|
+
end
|
65
|
+
entry.links.new_link do |link|
|
66
|
+
href = feed.links.find {|ln| ln.rel == RSS::OPDS::RELATIONS['self']}.href unless href
|
67
|
+
link.href = href
|
68
|
+
link.rel = relation
|
69
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
::RSS::OPDS::CATALOG_RELATIONS.each_pair do |type, uri|
|
75
|
+
define_method "add_#{type}" do |feed, href=nil|
|
76
|
+
add_relation feed, uri, href
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/rss/opds.rb
CHANGED
@@ -5,13 +5,13 @@ require 'rss/opds/version'
|
|
5
5
|
|
6
6
|
module RSS
|
7
7
|
module Utils
|
8
|
-
module
|
8
|
+
module TrueNil
|
9
9
|
module_function
|
10
10
|
def parse(value)
|
11
11
|
if value === true
|
12
12
|
value
|
13
13
|
else
|
14
|
-
/\Atrue\z/i.match(value.to_s) ? true :
|
14
|
+
/\Atrue\z/i.match(value.to_s) ? true : nil
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -19,22 +19,23 @@ module RSS
|
|
19
19
|
|
20
20
|
module BaseModel
|
21
21
|
private
|
22
|
-
|
22
|
+
|
23
|
+
def true_nil_attr_reader(*attrs)
|
23
24
|
attrs.each do |attr|
|
24
25
|
attr = attr.id2name if attr.kind_of?(Integer)
|
25
26
|
module_eval(<<-EOC, __FILE__, __LINE__ + 1)
|
26
27
|
attr_reader(:#{attr})
|
27
28
|
def #{attr}?
|
28
|
-
|
29
|
+
TrueNil.parse(@#{attr})
|
29
30
|
end
|
30
31
|
EOC
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
def
|
35
|
+
def true_nil_writer(name, disp_name=name)
|
35
36
|
module_eval(<<-EOC, __FILE__, __LINE__ + 1)
|
36
37
|
def #{name}=(new_value)
|
37
|
-
new_value = 'true'
|
38
|
+
new_value = [true, 'true'].include?(new_value) ? 'true' : nil
|
38
39
|
@#{name} = new_value
|
39
40
|
end
|
40
41
|
EOC
|
@@ -46,8 +47,9 @@ module RSS
|
|
46
47
|
alias rss_def_corresponded_attr_writer def_corresponded_attr_writer
|
47
48
|
def def_corresponded_attr_writer(name, type=nil, disp_name=nil)
|
48
49
|
disp_name ||= name
|
49
|
-
|
50
|
-
|
50
|
+
case type
|
51
|
+
when :true_nil
|
52
|
+
true_nil_writer name, disp_name
|
51
53
|
else
|
52
54
|
rss_def_corresponded_attr_writer name, type, disp_name
|
53
55
|
end
|
@@ -55,8 +57,9 @@ module RSS
|
|
55
57
|
|
56
58
|
alias rss_def_corresponded_attr_reader def_corresponded_attr_reader
|
57
59
|
def def_corresponded_attr_reader(name, type=nil)
|
58
|
-
|
59
|
-
|
60
|
+
case type
|
61
|
+
when :true_nil
|
62
|
+
true_nil_attr_reader name, type
|
60
63
|
else
|
61
64
|
rss_def_corresponded_attr_reader name, type
|
62
65
|
end
|
@@ -71,14 +74,102 @@ module RSS
|
|
71
74
|
'navigation' => 'application/atom+xml;profile=opds-catalog;kind=navigation',
|
72
75
|
'acquisition' => 'application/atom+xml;profile=opds-catalog;kind=acquisition'
|
73
76
|
}
|
77
|
+
REGISTERED_RELATIONS = %w[
|
78
|
+
alternate
|
79
|
+
appendix
|
80
|
+
archives
|
81
|
+
author
|
82
|
+
bookmark
|
83
|
+
canonical
|
84
|
+
chapter
|
85
|
+
collection
|
86
|
+
contents
|
87
|
+
copyright
|
88
|
+
current
|
89
|
+
describedby
|
90
|
+
disclosure
|
91
|
+
duplicate
|
92
|
+
edit
|
93
|
+
edit-media
|
94
|
+
enclosure
|
95
|
+
first
|
96
|
+
glossary
|
97
|
+
help
|
98
|
+
hosts
|
99
|
+
hub
|
100
|
+
icon
|
101
|
+
index
|
102
|
+
item
|
103
|
+
last
|
104
|
+
latest-version
|
105
|
+
license
|
106
|
+
lrdd
|
107
|
+
monitor
|
108
|
+
monitor-group
|
109
|
+
next
|
110
|
+
next-archive
|
111
|
+
nofollow
|
112
|
+
noreferrer
|
113
|
+
payment
|
114
|
+
predecessor-version
|
115
|
+
prefetch
|
116
|
+
prev
|
117
|
+
previous
|
118
|
+
prev-archive
|
119
|
+
related
|
120
|
+
replies
|
121
|
+
search
|
122
|
+
section
|
123
|
+
self
|
124
|
+
service
|
125
|
+
start
|
126
|
+
stylesheet
|
127
|
+
subsection
|
128
|
+
successor-version
|
129
|
+
tag
|
130
|
+
up
|
131
|
+
version-history
|
132
|
+
via
|
133
|
+
working-copy
|
134
|
+
working-copy-of
|
135
|
+
].reduce({}) {|relations, relation|
|
136
|
+
relations[relation] = relation
|
137
|
+
relations
|
138
|
+
}
|
139
|
+
CATALOG_RELATIONS = {
|
140
|
+
'start' => 'start',
|
141
|
+
'subsection' => 'subsection',
|
142
|
+
'new' => 'http://opds-spec.org/sort/new',
|
143
|
+
'popular' => 'http://opds-spec.org/sort/popular',
|
144
|
+
'featured' => 'http://opds-spec.org/featured',
|
145
|
+
'recommended' => 'http://opds-spec.org/recommended',
|
146
|
+
'shelf' => 'http://opds-spec.org/shelf',
|
147
|
+
'subscriptions' => 'http://opds-spec.org/subscriptions',
|
148
|
+
'facet' => 'http://opds-spec.org/facet',
|
149
|
+
'crawlable' => 'http://opds-spec.org/crawlable'
|
150
|
+
}
|
151
|
+
ENTRY_RELATIONS = {
|
152
|
+
'acquisition' => 'http://opds-spec.org/acquisition',
|
153
|
+
'open-access' => 'http://opds-spec.org/acquisition/open-access',
|
154
|
+
'borrow' => 'http://opds-spec.org/acquisition/borrow',
|
155
|
+
'buy' => 'http://opds-spec.org/acquisition/buy',
|
156
|
+
'sample' => 'http://opds-spec.org/acquisition/sample',
|
157
|
+
'subscribe' => 'http://opds-spec.org/acquisition/subscribe',
|
158
|
+
'image' => 'http://opds-spec.org/image',
|
159
|
+
'thumbnail' => 'http://opds-spec.org/image/thumbnail'
|
160
|
+
}
|
161
|
+
RELATIONS = Hash.new {|h, k|
|
162
|
+
ENTRY_RELATIONS[k] or CATALOG_RELATIONS[k] or REGISTERED_RELATIONS[k] or
|
163
|
+
raise KeyError, "Unsupported relation type: #{k.inspect}"
|
164
|
+
}
|
74
165
|
|
75
166
|
class Price < Element
|
76
167
|
include Atom::CommonModel
|
77
|
-
include Atom::ContentModel
|
78
168
|
|
79
169
|
install_ns(PREFIX, URI)
|
80
170
|
install_must_call_validator('opds', URI)
|
81
171
|
install_get_attribute('currencycode', '')
|
172
|
+
attr_accessor :value
|
82
173
|
|
83
174
|
class << self
|
84
175
|
def required_prefix
|
@@ -99,9 +190,6 @@ module RSS
|
|
99
190
|
def full_name
|
100
191
|
tag_name_with_prefix(PREFIX)
|
101
192
|
end
|
102
|
-
|
103
|
-
alias value content
|
104
|
-
alias value= content=
|
105
193
|
end
|
106
194
|
|
107
195
|
BaseListener.install_class_name(URI, 'price', 'Price')
|
@@ -121,22 +209,24 @@ module RSS
|
|
121
209
|
end
|
122
210
|
end
|
123
211
|
|
124
|
-
class Link
|
212
|
+
class Link < Element
|
125
213
|
[
|
126
214
|
['facetGroup', nil],
|
127
|
-
['activeFacet', [:
|
215
|
+
['activeFacet', [:true_nil, :true_nil]]
|
128
216
|
].each do |name, type|
|
129
217
|
disp_name = "#{OPDS::PREFIX}:#{name}"
|
130
218
|
install_get_attribute(name, OPDS::URI, false, type, nil, disp_name)
|
131
219
|
alias_method to_attr_name(name), name
|
132
220
|
alias_method "#{to_attr_name(name)}=", "#{name}="
|
133
221
|
end
|
222
|
+
|
223
|
+
Price = OPDS::Price
|
224
|
+
install_have_children_element 'opds_price', OPDS::URI, '*'
|
134
225
|
end
|
135
226
|
|
136
227
|
class Entry
|
137
|
-
|
138
|
-
|
139
|
-
install_have_children_element 'price', OPDS::URI, '*', 'opds_price'
|
228
|
+
def price
|
229
|
+
links.find {|link| link.opds_price}.opds_price
|
140
230
|
end
|
141
231
|
end
|
142
232
|
end
|
data/lib/rss/opds/version.rb
CHANGED
data/rss-opds.gemspec
CHANGED
@@ -6,7 +6,8 @@ Gem::Specification.new do |gem|
|
|
6
6
|
gem.email = ["KitaitiMakoto@gmail.com"]
|
7
7
|
gem.description = %q{OPDS parser and maker}
|
8
8
|
gem.summary = %q{This gem extends Ruby bundled RSS library to parse and make OPDS catalogs}
|
9
|
-
gem.homepage = ""
|
9
|
+
gem.homepage = "http://rss-ext.rubyforge.org/"
|
10
|
+
gem.rubyforge_project = "http://rss-ext.rubyforge.org/"
|
10
11
|
|
11
12
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
13
|
gem.files = `git ls-files`.split("\n")
|
@@ -15,11 +16,14 @@ Gem::Specification.new do |gem|
|
|
15
16
|
gem.require_paths = ["lib"]
|
16
17
|
gem.version = RSS::OPDS::VERSION
|
17
18
|
|
19
|
+
gem.add_runtime_dependency 'rss-dcterms'
|
20
|
+
gem.add_runtime_dependency 'rss-atom-feed_history'
|
21
|
+
gem.add_runtime_dependency 'money'
|
22
|
+
|
18
23
|
gem.add_development_dependency 'test-unit'
|
19
24
|
gem.add_development_dependency 'simplecov'
|
20
25
|
gem.add_development_dependency 'yard'
|
26
|
+
gem.add_development_dependency 'redcarpet'
|
21
27
|
gem.add_development_dependency 'pry'
|
22
|
-
|
23
|
-
gem.add_runtime_dependency 'rss-dcterms'
|
24
|
-
gem.add_runtime_dependency 'rss-atom-feed_history'
|
28
|
+
gem.add_development_dependency 'pry-doc'
|
25
29
|
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start do
|
3
|
+
add_filter '/test|vendor/'
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'rss/opds'
|
7
|
+
require 'test/unit'
|
8
|
+
require 'pathname'
|
9
|
+
require 'rexml/document'
|
10
|
+
|
11
|
+
class TestOPDS < Test::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
@fixtures_dir = Pathname(__FILE__).dirname.expand_path + 'fixtures'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Book = Struct.new 'Book',
|
18
|
+
:title, :author, :summary, :updated, :link, :popularity
|
19
|
+
|
data/test/test_maker.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
require 'rss/maker/opds'
|
3
|
+
|
4
|
+
class TestMaker < TestOPDS
|
5
|
+
def test_set_price
|
6
|
+
feed = RSS::Maker.make('atom') {|maker|
|
7
|
+
%w[id author title].each do |property|
|
8
|
+
maker.channel.send "#{property}=", property
|
9
|
+
end
|
10
|
+
maker.channel.updated = Time.now
|
11
|
+
|
12
|
+
maker.items.new_item do |entry|
|
13
|
+
entry.id = 'entry id'
|
14
|
+
entry.title = 'entry title'
|
15
|
+
entry.author = 'entry author'
|
16
|
+
entry.updated = Time.now
|
17
|
+
|
18
|
+
entry.links.new_link do |link|
|
19
|
+
link.href = 'uri/to/purchase'
|
20
|
+
link.opds_prices.new_opds_price do |price|
|
21
|
+
price.value = 100
|
22
|
+
price.currencycode = 'JPY'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
}
|
27
|
+
doc = REXML::Document.new(feed.to_s)
|
28
|
+
assert_not_empty REXML::XPath.match(doc, "//*[namespace-uri()='http://opds-spec.org/2010/catalog']")
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_set_active_facet
|
32
|
+
feed = RSS::Maker.make('atom') {|maker|
|
33
|
+
%w[id author title].each do |property|
|
34
|
+
maker.channel.send "#{property}=", property
|
35
|
+
end
|
36
|
+
maker.channel.updated = Time.now
|
37
|
+
|
38
|
+
maker.items.new_item do |entry|
|
39
|
+
entry.id = 'entry id'
|
40
|
+
entry.title = 'entry title'
|
41
|
+
entry.author = 'entry author'
|
42
|
+
entry.updated = Time.now
|
43
|
+
|
44
|
+
entry.links.new_link do |link|
|
45
|
+
link.href = 'uri/to/facet'
|
46
|
+
link.activeFacet = true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
}
|
50
|
+
doc = REXML::Document.new(feed.to_s)
|
51
|
+
links = REXML::XPath.match(doc, "//[@opds:activeFacet]")
|
52
|
+
assert_not_empty links
|
53
|
+
assert_equal 'true', links.first.attributes['opds:activeFacet']
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_set_inactive_facet
|
57
|
+
feed = RSS::Maker.make('atom') {|maker|
|
58
|
+
%w[id author title].each do |property|
|
59
|
+
maker.channel.send "#{property}=", property
|
60
|
+
end
|
61
|
+
maker.channel.updated = Time.now
|
62
|
+
|
63
|
+
maker.items.new_item do |entry|
|
64
|
+
entry.id = 'entry id'
|
65
|
+
entry.title = 'entry title'
|
66
|
+
entry.author = 'entry author'
|
67
|
+
entry.updated = Time.now
|
68
|
+
|
69
|
+
entry.links.new_link do |link|
|
70
|
+
link.href = 'uri/to/facet'
|
71
|
+
link.activeFacet = false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
}
|
75
|
+
doc = REXML::Document.new(feed.to_s)
|
76
|
+
assert_empty REXML::XPath.match(doc, "//[@opds:activeFacet]")
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_navigation_feed_accepts_aqcuisition_feed_as_item
|
80
|
+
catalog_root = nil
|
81
|
+
assert_nothing_raised do
|
82
|
+
catalog_root = RSS::Maker.make('atom') {|maker|
|
83
|
+
maker.channel.about = 'http://example.net/'
|
84
|
+
maker.channel.title = 'Example Catalog Root'
|
85
|
+
maker.channel.description = 'Sample OPDS'
|
86
|
+
maker.channel.links.new_link do |link|
|
87
|
+
link.href = 'http://example.net/root.opds'
|
88
|
+
link.rel = RSS::OPDS::RELATIONS['self']
|
89
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
90
|
+
end
|
91
|
+
maker.channel.links.new_link do |link|
|
92
|
+
link.href = 'http://example.net/root.opds'
|
93
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
94
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
95
|
+
end
|
96
|
+
maker.channel.updated = '2012-08-14T05:30:00'
|
97
|
+
maker.channel.author = 'KITAITI Makoto'
|
98
|
+
|
99
|
+
maker.items.add_relation recent, RSS::OPDS::RELATIONS['new']
|
100
|
+
maker.items.add_popular popular
|
101
|
+
}
|
102
|
+
end
|
103
|
+
doc = REXML::Document.new(catalog_root.to_s)
|
104
|
+
links = REXML::XPath.match(doc, "/feed/entry/link")
|
105
|
+
assert_equal 'http://opds-spec.org/sort/new', links.first.attributes['rel']
|
106
|
+
assert_equal 'http://opds-spec.org/sort/popular', links[1].attributes['rel']
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_utility_to_add_entry_relations
|
110
|
+
pend
|
111
|
+
end
|
112
|
+
|
113
|
+
def root
|
114
|
+
RSS::Maker.make('atom') {|maker|
|
115
|
+
maker.channel.about = 'http://example.net/'
|
116
|
+
maker.channel.title = 'Example Catalog Root'
|
117
|
+
maker.channel.description = 'Sample OPDS'
|
118
|
+
maker.channel.links.new_link do |link|
|
119
|
+
link.href = 'http://example.net/root.opds'
|
120
|
+
link.rel = RSS::OPDS::RELATIONS['self']
|
121
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
122
|
+
end
|
123
|
+
maker.channel.links.new_link do |link|
|
124
|
+
link.href = 'http://example.net/root.opds'
|
125
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
126
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
127
|
+
end
|
128
|
+
maker.channel.updated = '2012-08-14T04:23:00'
|
129
|
+
maker.channel.author = 'KITAITI Makoto'
|
130
|
+
|
131
|
+
maker.items.new_item do |entry|
|
132
|
+
entry.links.new_link do |link|
|
133
|
+
link.href = 'http://example.net/popular.opds'
|
134
|
+
link.rel = RSS::OPDS::RELATIONS['popular']
|
135
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
136
|
+
end
|
137
|
+
entry.title = 'Example Popular Books'
|
138
|
+
entry.updated = '2012-07-31T00:00:00'
|
139
|
+
entry.summary = 'Popular books in this site'
|
140
|
+
end
|
141
|
+
maker.items.new_item do |entry|
|
142
|
+
entry.links.new_link do |link|
|
143
|
+
link.href = 'http://example.net/new.opds'
|
144
|
+
link.rel = RSS::OPDS::RELATIONS['new']
|
145
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
146
|
+
end
|
147
|
+
entry.title = 'Example New Books'
|
148
|
+
entry.updated = '2012-08-14T04:23:00'
|
149
|
+
entry.summary = 'New books in this site'
|
150
|
+
end
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def popular
|
155
|
+
RSS::Maker.make('atom') do |maker|
|
156
|
+
maker.channel.about = 'http://example.net/'
|
157
|
+
maker.channel.title = 'Example Popular Catalog'
|
158
|
+
maker.channel.description = 'Popular books in this site'
|
159
|
+
maker.channel.links.new_link do |link|
|
160
|
+
link.href = 'http://example.net/popular.opds'
|
161
|
+
link.rel = RSS::OPDS::RELATIONS['self']
|
162
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
163
|
+
end
|
164
|
+
maker.channel.links.new_link do |link|
|
165
|
+
link.href = 'http://example.net/root.opds'
|
166
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
167
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
168
|
+
end
|
169
|
+
maker.channel.updated = '2012-07-31T00:00:00'
|
170
|
+
maker.channel.author = 'KITAITI Makoto'
|
171
|
+
|
172
|
+
popular_books.each do |book|
|
173
|
+
maker.items.new_item do |entry|
|
174
|
+
entry.title = book.title
|
175
|
+
entry.updated = book.updated
|
176
|
+
entry.summary = book.summary
|
177
|
+
entry.link = book.link
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def recent
|
184
|
+
RSS::Maker.make('atom') do |maker|
|
185
|
+
maker.channel.about = 'http://example.net/'
|
186
|
+
maker.channel.title = 'Example New Catalog'
|
187
|
+
maker.channel.description = 'New books in this site'
|
188
|
+
maker.channel.links.new_link do |link|
|
189
|
+
link.href = 'http://example.net/new.opds'
|
190
|
+
link.rel = 'self'
|
191
|
+
link.type = RSS::OPDS::TYPES['acquisition']
|
192
|
+
end
|
193
|
+
maker.channel.links.new_link do |link|
|
194
|
+
link.href = 'http://example.net/root.opds'
|
195
|
+
link.rel = RSS::OPDS::RELATIONS['start']
|
196
|
+
link.type = RSS::OPDS::TYPES['navigation']
|
197
|
+
end
|
198
|
+
maker.channel.updated = '2012-08-14T04:23:00'
|
199
|
+
maker.channel.author = 'KITAITI Makoto'
|
200
|
+
|
201
|
+
new_books.each do |book|
|
202
|
+
maker.items.new_item do |entry|
|
203
|
+
entry.title = book.title
|
204
|
+
entry.updated = book.updated
|
205
|
+
entry.summary = book.summary
|
206
|
+
entry.link = book.link
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def books
|
213
|
+
[
|
214
|
+
Book.new('book1', 'author1', 'summary1', Time.gm(2012, 8, 14, 3), 'http://example.net/book/book1', 1),
|
215
|
+
Book.new('book2', 'author2', 'summary2', Time.gm(2012, 8, 10, 14), 'http://example.net/book/book2', 2),
|
216
|
+
Book.new('book3', 'author3', 'summary3', Time.gm(2012, 6, 30, 12), 'http://example.net/book/book3', 3)
|
217
|
+
]
|
218
|
+
end
|
219
|
+
|
220
|
+
def popular_books
|
221
|
+
books.sort_by {|book| book.popularity}.reverse_each
|
222
|
+
end
|
223
|
+
|
224
|
+
def new_books
|
225
|
+
books.sort_by {|book| book.updated}.reverse_each
|
226
|
+
end
|
227
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rss-opds
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,44 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rss-dcterms
|
16
|
+
requirement: &11264540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *11264540
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rss-atom-feed_history
|
27
|
+
requirement: &11264060 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *11264060
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: money
|
38
|
+
requirement: &11263600 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *11263600
|
14
47
|
- !ruby/object:Gem::Dependency
|
15
48
|
name: test-unit
|
16
|
-
requirement: &
|
49
|
+
requirement: &11263060 !ruby/object:Gem::Requirement
|
17
50
|
none: false
|
18
51
|
requirements:
|
19
52
|
- - ! '>='
|
@@ -21,10 +54,10 @@ dependencies:
|
|
21
54
|
version: '0'
|
22
55
|
type: :development
|
23
56
|
prerelease: false
|
24
|
-
version_requirements: *
|
57
|
+
version_requirements: *11263060
|
25
58
|
- !ruby/object:Gem::Dependency
|
26
59
|
name: simplecov
|
27
|
-
requirement: &
|
60
|
+
requirement: &11262540 !ruby/object:Gem::Requirement
|
28
61
|
none: false
|
29
62
|
requirements:
|
30
63
|
- - ! '>='
|
@@ -32,10 +65,10 @@ dependencies:
|
|
32
65
|
version: '0'
|
33
66
|
type: :development
|
34
67
|
prerelease: false
|
35
|
-
version_requirements: *
|
68
|
+
version_requirements: *11262540
|
36
69
|
- !ruby/object:Gem::Dependency
|
37
70
|
name: yard
|
38
|
-
requirement: &
|
71
|
+
requirement: &11262100 !ruby/object:Gem::Requirement
|
39
72
|
none: false
|
40
73
|
requirements:
|
41
74
|
- - ! '>='
|
@@ -43,10 +76,10 @@ dependencies:
|
|
43
76
|
version: '0'
|
44
77
|
type: :development
|
45
78
|
prerelease: false
|
46
|
-
version_requirements: *
|
79
|
+
version_requirements: *11262100
|
47
80
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
49
|
-
requirement: &
|
81
|
+
name: redcarpet
|
82
|
+
requirement: &11261600 !ruby/object:Gem::Requirement
|
50
83
|
none: false
|
51
84
|
requirements:
|
52
85
|
- - ! '>='
|
@@ -54,29 +87,29 @@ dependencies:
|
|
54
87
|
version: '0'
|
55
88
|
type: :development
|
56
89
|
prerelease: false
|
57
|
-
version_requirements: *
|
90
|
+
version_requirements: *11261600
|
58
91
|
- !ruby/object:Gem::Dependency
|
59
|
-
name:
|
60
|
-
requirement: &
|
92
|
+
name: pry
|
93
|
+
requirement: &11261140 !ruby/object:Gem::Requirement
|
61
94
|
none: false
|
62
95
|
requirements:
|
63
96
|
- - ! '>='
|
64
97
|
- !ruby/object:Gem::Version
|
65
98
|
version: '0'
|
66
|
-
type: :
|
99
|
+
type: :development
|
67
100
|
prerelease: false
|
68
|
-
version_requirements: *
|
101
|
+
version_requirements: *11261140
|
69
102
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
71
|
-
requirement: &
|
103
|
+
name: pry-doc
|
104
|
+
requirement: &11260700 !ruby/object:Gem::Requirement
|
72
105
|
none: false
|
73
106
|
requirements:
|
74
107
|
- - ! '>='
|
75
108
|
- !ruby/object:Gem::Version
|
76
109
|
version: '0'
|
77
|
-
type: :
|
110
|
+
type: :development
|
78
111
|
prerelease: false
|
79
|
-
version_requirements: *
|
112
|
+
version_requirements: *11260700
|
80
113
|
description: OPDS parser and maker
|
81
114
|
email:
|
82
115
|
- KitaitiMakoto@gmail.com
|
@@ -89,11 +122,15 @@ files:
|
|
89
122
|
- LICENSE
|
90
123
|
- README.markdown
|
91
124
|
- Rakefile
|
125
|
+
- examples/make-catalog.rb
|
126
|
+
- lib/rss/maker/opds.rb
|
92
127
|
- lib/rss/opds.rb
|
93
128
|
- lib/rss/opds/version.rb
|
94
129
|
- rss-opds.gemspec
|
95
130
|
- setup.rb
|
96
|
-
|
131
|
+
- test/helper.rb
|
132
|
+
- test/test_maker.rb
|
133
|
+
homepage: http://rss-ext.rubyforge.org/
|
97
134
|
licenses: []
|
98
135
|
post_install_message:
|
99
136
|
rdoc_options: []
|
@@ -107,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
144
|
version: '0'
|
108
145
|
segments:
|
109
146
|
- 0
|
110
|
-
hash: -
|
147
|
+
hash: -1275098366759959895
|
111
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
149
|
none: false
|
113
150
|
requirements:
|
@@ -116,9 +153,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
153
|
version: '0'
|
117
154
|
segments:
|
118
155
|
- 0
|
119
|
-
hash: -
|
156
|
+
hash: -1275098366759959895
|
120
157
|
requirements: []
|
121
|
-
rubyforge_project:
|
158
|
+
rubyforge_project: http://rss-ext.rubyforge.org/
|
122
159
|
rubygems_version: 1.8.8
|
123
160
|
signing_key:
|
124
161
|
specification_version: 3
|