rss-opds 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|