tivohmo 0.1.0

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