tivohmo 0.1.0

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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +17 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +514 -0
  7. data/README.md +50 -0
  8. data/Rakefile +7 -0
  9. data/bin/tivohmo +8 -0
  10. data/contrib/tivohmo.conf +17 -0
  11. data/contrib/tivohmo.plist +22 -0
  12. data/lib/tivohmo/adapters/filesystem/application.rb +23 -0
  13. data/lib/tivohmo/adapters/filesystem/file_item.rb +29 -0
  14. data/lib/tivohmo/adapters/filesystem/folder_container.rb +105 -0
  15. data/lib/tivohmo/adapters/filesystem.rb +3 -0
  16. data/lib/tivohmo/adapters/plex/application.rb +41 -0
  17. data/lib/tivohmo/adapters/plex/category.rb +72 -0
  18. data/lib/tivohmo/adapters/plex/episode.rb +26 -0
  19. data/lib/tivohmo/adapters/plex/metadata.rb +24 -0
  20. data/lib/tivohmo/adapters/plex/movie.rb +26 -0
  21. data/lib/tivohmo/adapters/plex/qualified_category.rb +51 -0
  22. data/lib/tivohmo/adapters/plex/season.rb +39 -0
  23. data/lib/tivohmo/adapters/plex/section.rb +48 -0
  24. data/lib/tivohmo/adapters/plex/show.rb +39 -0
  25. data/lib/tivohmo/adapters/plex/transcoder.rb +19 -0
  26. data/lib/tivohmo/adapters/plex.rb +11 -0
  27. data/lib/tivohmo/adapters/streamio/metadata.rb +26 -0
  28. data/lib/tivohmo/adapters/streamio/transcoder.rb +239 -0
  29. data/lib/tivohmo/adapters/streamio.rb +17 -0
  30. data/lib/tivohmo/api/application.rb +35 -0
  31. data/lib/tivohmo/api/container.rb +31 -0
  32. data/lib/tivohmo/api/item.rb +32 -0
  33. data/lib/tivohmo/api/metadata.rb +98 -0
  34. data/lib/tivohmo/api/node.rb +115 -0
  35. data/lib/tivohmo/api/server.rb +24 -0
  36. data/lib/tivohmo/api/transcoder.rb +39 -0
  37. data/lib/tivohmo/api.rb +7 -0
  38. data/lib/tivohmo/beacon.rb +69 -0
  39. data/lib/tivohmo/cli.rb +182 -0
  40. data/lib/tivohmo/logging.rb +50 -0
  41. data/lib/tivohmo/server/views/_container.builder +19 -0
  42. data/lib/tivohmo/server/views/_item.builder +57 -0
  43. data/lib/tivohmo/server/views/container.builder +26 -0
  44. data/lib/tivohmo/server/views/item_details.builder +153 -0
  45. data/lib/tivohmo/server/views/layout.builder +2 -0
  46. data/lib/tivohmo/server/views/layout.erb +10 -0
  47. data/lib/tivohmo/server/views/server.builder +18 -0
  48. data/lib/tivohmo/server/views/server_info.builder +7 -0
  49. data/lib/tivohmo/server/views/unsupported.erb +7 -0
  50. data/lib/tivohmo/server/views/video_formats.builder +10 -0
  51. data/lib/tivohmo/server.rb +306 -0
  52. data/lib/tivohmo/version.rb +3 -0
  53. data/lib/tivohmo.rb +5 -0
  54. data/spec/adapters/filesystem/application_spec.rb +19 -0
  55. data/spec/adapters/filesystem/file_item_spec.rb +33 -0
  56. data/spec/adapters/filesystem/folder_container_spec.rb +115 -0
  57. data/spec/adapters/plex/application_spec.rb +20 -0
  58. data/spec/adapters/plex/category_spec.rb +66 -0
  59. data/spec/adapters/plex/episode_spec.rb +22 -0
  60. data/spec/adapters/plex/metadata_spec.rb +24 -0
  61. data/spec/adapters/plex/movie_spec.rb +22 -0
  62. data/spec/adapters/plex/qualified_category_spec.rb +51 -0
  63. data/spec/adapters/plex/season_spec.rb +22 -0
  64. data/spec/adapters/plex/section_spec.rb +38 -0
  65. data/spec/adapters/plex/show_spec.rb +22 -0
  66. data/spec/adapters/plex/transcoder_spec.rb +27 -0
  67. data/spec/adapters/streamio/metadata_spec.rb +34 -0
  68. data/spec/adapters/streamio/transcoder_spec.rb +42 -0
  69. data/spec/api/application_spec.rb +63 -0
  70. data/spec/api/container_spec.rb +34 -0
  71. data/spec/api/item_spec.rb +53 -0
  72. data/spec/api/metadata_spec.rb +59 -0
  73. data/spec/api/node_spec.rb +178 -0
  74. data/spec/api/server_spec.rb +24 -0
  75. data/spec/api/transcoder_spec.rb +25 -0
  76. data/spec/beacon_spec.rb +87 -0
  77. data/spec/cli_spec.rb +227 -0
  78. data/spec/server_spec.rb +458 -0
  79. data/spec/spec_helper.rb +123 -0
  80. data/tivohmo.gemspec +46 -0
  81. metadata +416 -0
@@ -0,0 +1,306 @@
1
+ require 'builder'
2
+ require 'sinatra/base'
3
+ require 'active_support/core_ext/string'
4
+ require 'zlib'
5
+ require 'time'
6
+
7
+ module TivoHMO
8
+
9
+ # The http server for serving the Application/Container/Item tree,
10
+ # including an endpoint for transcoding video from the Item to the
11
+ # tivo format
12
+ class Server < Sinatra::Base
13
+ include GemLogger::LoggerSupport
14
+
15
+ disable :logging
16
+ set :root, File.expand_path("../server", __FILE__)
17
+ set :reload_templates, true
18
+ set :builder, :content_type => 'text/xml'
19
+
20
+ before do
21
+ logger.info "Request from #{request.ip} \"#{request.request_method} #{request.url}\""
22
+ logger.debug "Headers: #{headers.inspect}"
23
+ end
24
+
25
+ after do
26
+ logger.info "Response to #{request.ip} for \"#{request.request_method} #{request.url}\" [#{response.status}]"
27
+ logger.debug "Headers: #{response.header}"
28
+ if ! response.body.is_a?(Sinatra::Helpers::Stream)
29
+ logger.debug "Body:\n"
30
+ logger.debug response.body.join("\n")
31
+ end
32
+ end
33
+
34
+
35
+ def self.start(server, port, &block)
36
+ Rack::Handler.default.run new(server), Port: port, &block
37
+ end
38
+
39
+ def initialize(server)
40
+ @server = server
41
+ super
42
+ end
43
+
44
+ module Helpers
45
+ include Rack::Utils
46
+
47
+ def logger
48
+ Server.logger
49
+ end
50
+
51
+ def server
52
+ @server
53
+ end
54
+
55
+ def unsupported
56
+ status 404
57
+ erb :unsupported, layout: true
58
+ end
59
+
60
+ def tsn
61
+ headers['TiVo_TCD_ID']
62
+ end
63
+
64
+ def sort(items, sort_order)
65
+ sort_order = sort_order.split(/\s*,\s*/)
66
+
67
+ sort_order.each do |order|
68
+
69
+ reverse = false
70
+ if order[0] == '!'
71
+ reverse = true
72
+ order = order[1..-1]
73
+ end
74
+
75
+ case order
76
+ when 'Title'
77
+ items = items.sort_by(&:title)
78
+ when 'CaptureDate'
79
+ items = items.sort_by(&:created_at)
80
+ end
81
+
82
+ items = items.reverse if reverse
83
+ end
84
+ items
85
+ end
86
+
87
+ def container_url(container)
88
+ url = "/TiVoConnect"
89
+ params = {
90
+ Command: 'QueryContainer',
91
+ Container: container.title_path
92
+ }
93
+ url << "?" << build_query(params)
94
+ end
95
+
96
+ def item_url(item)
97
+ item.title_path
98
+ end
99
+
100
+ def item_detail_url(item)
101
+ url = "/TiVoConnect"
102
+ container = item.app.title
103
+ file = item.title_path.sub("/#{container}/", '')
104
+ params = {
105
+ Command: 'TVBusQuery',
106
+ Container: container,
107
+ File: file
108
+ }
109
+ url << "?" << build_query(params)
110
+ end
111
+
112
+ # to format uuids as needed by tivo in builders
113
+ def format_uuid(s)
114
+ Zlib.crc32(s)
115
+ end
116
+
117
+ # to format date/time as needed by tivo in builders
118
+ def format_date(time)
119
+ "0x#{time.to_i.to_s(16)}"
120
+ end
121
+
122
+ def format_iso_date(time)
123
+ return "" unless time
124
+ time.utc.strftime("%FT%T.%6N")
125
+ end
126
+
127
+ def format_iso_duration(duration)
128
+ return "" unless duration
129
+ seconds = duration % 60
130
+ minutes = (duration / 60) % 60
131
+ hours = (duration / (60 * 60)) % 24
132
+ days = (duration / (60 * 60 * 24)).to_i
133
+ 'P%sDT%sH%sM%sS' % [days, hours, minutes, seconds]
134
+ end
135
+
136
+ def pad(length, align)
137
+ extra = length % align
138
+ extra = align - extra if extra > 0
139
+ extra
140
+ end
141
+
142
+ def tivo_header(item, mime)
143
+ if mime == 'video/x-tivo-mpeg-ts'
144
+ flag = 45
145
+ else
146
+ flag = 13
147
+ end
148
+
149
+ item_details_xml = builder :item_details, layout: true, locals: {item: item}
150
+ ld = item_details_xml.bytesize
151
+ chunk = item_details_xml + '\0' * (pad(ld, 4) + 4)
152
+ lc = chunk.bytesize
153
+ blocklen = lc * 2 + 40
154
+ padding = pad(blocklen, 1024)
155
+
156
+ data = 'TiVo'
157
+ data << [4, flag, 0, padding + blocklen, 2].pack('nnnNn')
158
+ data << [lc + 12, ld, 1, 0].pack('NNnn')
159
+ data << chunk
160
+ data << [lc + 12, ld, 2, 0].pack('NNnn')
161
+ data << chunk
162
+ data << "\0" * padding
163
+
164
+ data
165
+ end
166
+
167
+ end
168
+
169
+ helpers do
170
+ include Helpers
171
+ end
172
+
173
+ # Get the xml doc describing the active HME applications
174
+ get '/TiVoConnect' do
175
+ command = params['Command']
176
+
177
+ # pagination
178
+ item_start = (params['ItemStart'] || 0).to_i
179
+ item_count = (params['ItemCount'] || 8).to_i
180
+
181
+ # Yes or No, default no
182
+ recurse = (params['Recurse'] == 'Yes')
183
+
184
+ # csv of Type, Title, CreationDate, LastChangeDate, Random
185
+ # reverse with preceding !
186
+ sort_order = params['SortOrder']
187
+
188
+ # csv of mime types from Details.ContentType, wildcards allowed,
189
+ # negation is preceding !, default/missing is */*
190
+ filter = params['Filter']
191
+ serial = params['SerialNum']
192
+
193
+ container_path = params['Container'] || '/'
194
+
195
+ anchor_item = params['AnchorItem']
196
+ if anchor_item && anchor_item =~ /QueryContainer/
197
+ query = parse_query(URI(anchor_item).query)
198
+ anchor_item = query['Container']
199
+ end
200
+
201
+ anchor_offset = params['AnchorOffset'].to_i
202
+
203
+ locals = {
204
+ show_genres: params['DoGenres'],
205
+ item_start: item_start,
206
+ item_count: item_count
207
+ }
208
+
209
+ case command
210
+
211
+ when 'QueryContainer' then
212
+
213
+ container = server.find(container_path)
214
+ halt 404, "No container found for #{container_path}" unless container
215
+
216
+ children = container.children
217
+ children = sort(children, sort_order) if sort_order
218
+
219
+ if anchor_item
220
+ anchor = server.find(anchor_item)
221
+ if anchor
222
+ idx = children.index(anchor)
223
+ if idx
224
+ # -1 means show starting at the anchor item
225
+ # ItemStart should be the index of the anchor
226
+ anchor_offset = anchor_offset + 1 if anchor_offset < 0
227
+ item_start = idx + anchor_offset
228
+ item_start = 0 if item_start < 0
229
+ locals[:item_start] = item_start
230
+ else
231
+ logger.warn "Anchor not in container: #{container}, #{anchor_item}"
232
+ end
233
+ else
234
+ logger.warn "Anchor not found: #{anchor_item}"
235
+ end
236
+ end
237
+
238
+ if container.root?
239
+ builder :server, layout: true,
240
+ locals: locals.merge(container: container, children: children)
241
+ else
242
+ builder :container, layout: true,
243
+ locals: locals.merge(container: container, children: children)
244
+ end
245
+
246
+
247
+ when 'TVBusQuery' then
248
+ container_path = params['Container']
249
+ item_title = params['File']
250
+ halt 404, "Need Container and File params" unless container_path && item_title
251
+
252
+ path = "#{container_path}/#{item_title}"
253
+ item = server.find(path)
254
+ halt 404, "No item found for #{path}" unless item
255
+
256
+ builder :item_details, layout: true, locals: locals.merge(item: item)
257
+
258
+ when 'QueryFormats' then
259
+ sf = params['SourceFormat']
260
+ if sf.to_s.start_with?('video')
261
+ formats = ["video/x-tivo-mpeg"]
262
+ formats << "video/x-tivo-mpeg-ts" # if is_ts_capable(tsn)
263
+ builder :video_formats, layout: true, locals: locals.merge(formats: formats)
264
+ else
265
+ unsupported
266
+ end
267
+
268
+ when 'QueryServer' then
269
+ builder :server_info, layout: true
270
+
271
+ when 'QueryItem' then
272
+ status 200
273
+ body ""
274
+
275
+ when 'FlushServer' then
276
+ status 200
277
+ body ""
278
+
279
+ when 'ResetServer' then
280
+ status 200
281
+ body ""
282
+
283
+ else
284
+ unsupported
285
+ end
286
+
287
+ end
288
+
289
+ get '/*' do
290
+ title_path = params[:splat].first
291
+ format = params['Format']
292
+ item = server.find(title_path)
293
+ halt 404, "No item found for #{title_path}" unless item && item.is_a?(TivoHMO::API::Item)
294
+
295
+ status 206
296
+ response["Content-Type"] = format
297
+
298
+ stream do |out|
299
+ out << tivo_header(item, format)
300
+ item.transcoder.transcode(out, format)
301
+ end
302
+
303
+ end
304
+
305
+ end
306
+ end
@@ -0,0 +1,3 @@
1
+ module TivoHMO
2
+ VERSION = "0.1.0"
3
+ end
data/lib/tivohmo.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'gem_logger'
2
+ require "tivohmo/version"
3
+ require "tivohmo/api"
4
+ require "tivohmo/server"
5
+ require "tivohmo/beacon"
@@ -0,0 +1,19 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/filesystem'
3
+
4
+ describe TivoHMO::Adapters::Filesystem::Application do
5
+
6
+
7
+ describe "#initialize" do
8
+
9
+ it "should instantiate" do
10
+ app = described_class.new(File.dirname(__FILE__))
11
+ expect(app).to be_a described_class
12
+ expect(app).to be_a TivoHMO::Adapters::Filesystem::FolderContainer
13
+ expect(app.metadata_class).to eq TivoHMO::Adapters::StreamIO::Metadata
14
+ expect(app.transcoder_class).to eq TivoHMO::Adapters::StreamIO::Transcoder
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/filesystem'
3
+
4
+ describe TivoHMO::Adapters::Filesystem::FileItem do
5
+
6
+
7
+ describe "#initialize" do
8
+
9
+ describe "#initialize" do
10
+
11
+ it "should fail with non-existant file" do
12
+ expect{described_class.new("not-here")}.to raise_error(ArgumentError, /existing file/)
13
+ end
14
+
15
+ it "should fail with dir" do
16
+ expect{described_class.new(File.dirname(__FILE__))}.to raise_error(ArgumentError, /existing file/)
17
+ end
18
+
19
+ it "should instantiate" do
20
+ file = __FILE__
21
+ subject = described_class.new(file)
22
+ expect(subject).to be_a described_class
23
+ expect(subject.full_path).to eq File.expand_path(file)
24
+ expect(subject.title).to eq File.basename(file)
25
+ expect(subject.modified_at).to_not be_nil
26
+ expect(subject.created_at).to_not be_nil
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,115 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/filesystem'
3
+
4
+ describe TivoHMO::Adapters::Filesystem::FolderContainer do
5
+
6
+ describe "#initialize" do
7
+
8
+ it "should fail with non-existant dir" do
9
+ expect{described_class.new("not-here")}.to raise_error(ArgumentError, /existing directory/)
10
+ end
11
+
12
+ it "should fail with file" do
13
+ expect{described_class.new(__FILE__)}.to raise_error(ArgumentError, /existing directory/)
14
+ end
15
+
16
+ it "should instantiate" do
17
+ with_file_tree(foo: [1]) do |dir|
18
+ subject = described_class.new("#{dir}/foo")
19
+ expect(subject).to be_a described_class
20
+ expect(subject.full_path).to eq "#{dir}/foo"
21
+ expect(subject.title).to eq 'Foo'
22
+ expect(subject.modified_at).to_not be_nil
23
+ expect(subject.created_at).to_not be_nil
24
+ expect(subject.allowed_item_types).to eq %i[file dir]
25
+ expect(subject.allowed_item_extensions).to eq described_class::VIDEO_EXTENSIONS
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ describe "#allowed_container?" do
32
+
33
+ it "is false when not allowed" do
34
+ with_file_tree(1, foo: []) do |dir|
35
+ subject = described_class.new(dir)
36
+ subject.allowed_item_types = %i[dir]
37
+ expect(subject.send(:allowed_container?, "#{dir}/foo")).to eq(true)
38
+ expect(subject.send(:allowed_container?, "#{dir}/1")).to eq(false)
39
+ expect(subject.send(:allowed_container?, "#{dir}/nothere")).to eq(false)
40
+ subject.allowed_item_types = %i[]
41
+ expect(subject.send(:allowed_container?, "#{dir}/foo")).to eq(false)
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ describe "#allowed_item?" do
48
+
49
+ it "is false when not allowed" do
50
+ with_file_tree('1.avi', '1.bad', foo: []) do |dir|
51
+ subject = described_class.new(dir)
52
+ subject.allowed_item_types = %i[file]
53
+ subject.allowed_item_extensions = %w[avi]
54
+ expect(subject.send(:allowed_item?, "#{dir}/1.avi")).to eq(true)
55
+ expect(subject.send(:allowed_item?, "#{dir}/1.bad")).to eq(false)
56
+ expect(subject.send(:allowed_item?, "#{dir}/foo")).to eq(false)
57
+ expect(subject.send(:allowed_item?, "#{dir}/nothere")).to eq(false)
58
+ subject.allowed_item_types = %i[]
59
+ expect(subject.send(:allowed_item?, "#{dir}/1.avi")).to eq(false)
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ describe "#children" do
66
+
67
+ it "contains filesystem children" do
68
+ with_file_tree('1.avi', foo: ['2.avi']) do |dir|
69
+ subject = described_class.new(dir)
70
+ expect(subject.children.collect(&:title)).to match_array ['1.avi', 'Foo']
71
+ expect(subject.find('1.avi')).to be_a(TivoHMO::API::Item)
72
+ expect(subject.find('1.avi')).to be_a(TivoHMO::Adapters::Filesystem::FileItem)
73
+ expect(subject.find('Foo')).to be_a(TivoHMO::API::Container)
74
+ expect(subject.find('Foo')).to be_a(TivoHMO::Adapters::Filesystem::FolderContainer)
75
+ end
76
+ end
77
+
78
+ it "memoizes" do
79
+ subject = described_class.new(File.dirname(__FILE__))
80
+ expect(subject.children.object_id).to eq subject.children.object_id
81
+ end
82
+
83
+ it "watches for filesystem addition" do
84
+ with_file_tree('1.avi', foo: ['2.avi']) do |dir|
85
+ subject = described_class.new(dir)
86
+ expect(subject.children.collect(&:title)).to match_array ['1.avi', 'Foo']
87
+ FileUtils.touch "#{dir}/3.avi"
88
+ sleep 0.5
89
+ expect(subject.children.collect(&:title)).to match_array ['3.avi','1.avi', 'Foo']
90
+ end
91
+ end
92
+
93
+ it "watches for filesystem removal" do
94
+ with_file_tree('1.avi', foo: ['2.avi']) do |dir|
95
+ subject = described_class.new(dir)
96
+ expect(subject.children.collect(&:title)).to match_array ['1.avi', 'Foo']
97
+ FileUtils.rm "#{dir}/1.avi"
98
+ sleep 0.5
99
+ expect(subject.children.collect(&:title)).to match_array ['Foo']
100
+ end
101
+ end
102
+
103
+ it "doesn't watch children" do
104
+ with_file_tree('1.avi', foo: ['2.avi']) do |dir|
105
+ subject = described_class.new(dir)
106
+ oldid = subject.children.object_id
107
+ FileUtils.touch "#{dir}/foo/3.avi"
108
+ sleep 0.5
109
+ expect(subject.children.object_id).to eq oldid
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::Application do
5
+
6
+
7
+ describe "#initialize" do
8
+
9
+ it "should instantiate" do
10
+ app = described_class.new('localhost')
11
+ expect(app).to be_a described_class
12
+ expect(app).to be_a TivoHMO::API::Application
13
+ expect(app).to be_a TivoHMO::API::Container
14
+ expect(app.metadata_class).to eq TivoHMO::Adapters::Plex::Metadata
15
+ expect(app.transcoder_class).to eq TivoHMO::Adapters::Plex::Transcoder
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,66 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::Category do
5
+
6
+ let(:plex_delegate) { plex_stub(::Plex::Section) }
7
+
8
+ describe "#initialize" do
9
+
10
+ it "should instantiate" do
11
+ category = described_class.new(plex_delegate, :newest)
12
+ expect(category).to be_a described_class
13
+ expect(category).to be_a TivoHMO::API::Container
14
+ expect(category.category_type).to eq(:newest)
15
+ expect(category.category_value).to be_nil
16
+ expect(category.title).to eq('Newest')
17
+ expect(category.identifier).to eq(plex_delegate.key)
18
+ expect(category.modified_at).to eq(Time.at(plex_delegate.updated_at))
19
+ expect(category.created_at).to eq(Time.at(plex_delegate.added_at))
20
+ end
21
+
22
+ it "should use_category_value for title if present" do
23
+ cval = {title: 'MyTitle', key: '/plex/key'}
24
+ category = described_class.new(plex_delegate, :by_year, cval)
25
+ expect(category.category_type).to eq(:by_year)
26
+ expect(category.category_value).to eq(cval)
27
+ expect(category.title).to eq('MyTitle')
28
+ end
29
+
30
+ end
31
+
32
+ describe "#children" do
33
+
34
+ it "should memoize" do
35
+ listing = [plex_stub(::Plex::Movie)]
36
+ allow(plex_delegate).to receive(:newest).and_return(listing)
37
+ section = described_class.new(plex_delegate, :newest)
38
+ expect(section.children.object_id).to eq(section.children.object_id)
39
+ end
40
+
41
+ it "should have children" do
42
+ listing = [
43
+ plex_stub(::Plex::Movie),
44
+ plex_stub(::Plex::Episode),
45
+ plex_stub(::Plex::Show)
46
+ ]
47
+ allow(plex_delegate).to receive(:newest).and_return(listing)
48
+ section = described_class.new(plex_delegate, :newest)
49
+ expect(section.children.size).to eq(3)
50
+ end
51
+
52
+ it "should use category_value for children" do
53
+ listing = [
54
+ plex_stub(::Plex::Movie),
55
+ plex_stub(::Plex::Episode),
56
+ plex_stub(::Plex::Show)
57
+ ]
58
+ cval = {title: 'Title', key: 'key'}
59
+ allow(plex_delegate).to receive(:by_year).with('key').and_return(listing)
60
+ section = described_class.new(plex_delegate, :by_year, cval)
61
+ expect(section.children.size).to eq(3)
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::Episode do
5
+
6
+ let(:plex_delegate) { plex_stub(::Plex::Episode) }
7
+
8
+ describe "#initialize" do
9
+
10
+ it "should instantiate" do
11
+ episode = described_class.new(plex_delegate)
12
+ expect(episode).to be_a described_class
13
+ expect(episode).to be_a TivoHMO::API::Item
14
+ expect(episode.title).to eq(plex_delegate.title)
15
+ expect(episode.identifier).to eq(plex_delegate.key)
16
+ expect(episode.modified_at).to eq(Time.at(plex_delegate.updated_at))
17
+ expect(episode.created_at).to eq(Time.at(plex_delegate.added_at))
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::Metadata do
5
+
6
+ let(:plex_delegate) { plex_stub(::Plex::Movie,
7
+ summary: 'Summary',
8
+ duration: 10000) }
9
+
10
+ let(:item) { double('item', delegate: plex_delegate)}
11
+
12
+ describe "#initialize" do
13
+
14
+ it "should instantiate" do
15
+ md = described_class.new(item)
16
+ expect(md).to be_a described_class
17
+ expect(md).to be_a TivoHMO::API::Metadata
18
+ expect(md.duration).to eq(plex_delegate.duration / 1000)
19
+ expect(md.description).to eq plex_delegate.summary
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::Movie do
5
+
6
+ let(:plex_delegate) { plex_stub(::Plex::Movie) }
7
+
8
+ describe "#initialize" do
9
+
10
+ it "should instantiate" do
11
+ movie = described_class.new(plex_delegate)
12
+ expect(movie).to be_a described_class
13
+ expect(movie).to be_a TivoHMO::API::Item
14
+ expect(movie.title).to eq(plex_delegate.title)
15
+ expect(movie.identifier).to eq(plex_delegate.key)
16
+ expect(movie.modified_at).to eq(Time.at(plex_delegate.updated_at))
17
+ expect(movie.created_at).to eq(Time.at(plex_delegate.added_at))
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,51 @@
1
+ require_relative "../../spec_helper"
2
+ require 'tivohmo/adapters/plex'
3
+
4
+ describe TivoHMO::Adapters::Plex::QualifiedCategory do
5
+
6
+ let(:plex_delegate) { plex_stub(::Plex::Section) }
7
+
8
+
9
+ describe "#initialize" do
10
+
11
+ it "should instantiate" do
12
+ qcat = described_class.new(plex_delegate, :by_year, '2000')
13
+ expect(qcat).to be_a described_class
14
+ expect(qcat).to be_a TivoHMO::API::Container
15
+ expect(qcat.title).to eq("By Year")
16
+ expect(qcat.category_qualifier).to eq('2000')
17
+ expect(qcat.title).to eq("By Year")
18
+ expect(qcat.identifier).to eq(plex_delegate.key)
19
+ expect(qcat.modified_at).to eq(Time.at(plex_delegate.updated_at))
20
+ expect(qcat.created_at).to eq(Time.at(plex_delegate.added_at))
21
+ end
22
+
23
+ end
24
+
25
+ describe "#children" do
26
+
27
+ it "should memoize" do
28
+ listing = [
29
+ {title: 'Title1', key: 'key1'}
30
+ ]
31
+ allow(plex_delegate).to receive(:years).and_return(listing)
32
+ section = described_class.new(plex_delegate, :by_year, :years)
33
+ expect(section.children.object_id).to eq(section.children.object_id)
34
+ end
35
+
36
+ it "should have children" do
37
+ listing = [
38
+ {title: 'Title1', key: 'key1'},
39
+ {title: 'Title2', key: 'key2'}
40
+ ]
41
+ allow(plex_delegate).to receive(:years).and_return(listing)
42
+ section = described_class.new(plex_delegate, :by_year, :years)
43
+ expect(section.children.size).to eq(2)
44
+ expect(section.children[0]).to be_instance_of(TivoHMO::Adapters::Plex::Category)
45
+ expect(section.children[0].category_type).to be(:by_year)
46
+ expect(section.children[0].category_value).to eq(listing[0])
47
+ end
48
+
49
+ end
50
+
51
+ end