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,458 @@
1
+ require_relative "spec_helper"
2
+ require 'rack/test'
3
+ require 'nokogiri'
4
+
5
+ Sinatra::Base.set :environment, :test
6
+
7
+ describe TivoHMO::Server do
8
+ include Rack::Test::Methods
9
+
10
+ let(:application) do
11
+ a = TestAPI::Application.new('a1')
12
+ a.metadata_class = TestAPI::Metadata
13
+ a.transcoder_class = TestAPI::Transcoder
14
+ a
15
+ end
16
+
17
+ let(:server) do
18
+ s = TivoHMO::API::Server.new
19
+ s.add_child(application)
20
+ s
21
+ end
22
+
23
+ # for Rack::Test
24
+ let(:app) { TivoHMO::Server.new(server) }
25
+
26
+ before(:each) do
27
+ make_api_tree(application, c1: ['i1'])
28
+ end
29
+
30
+ let(:container) { server.find('/a1/c1') }
31
+ let(:item) { server.find('/a1/c1/i1') }
32
+ let(:helpers) { (Class.new { include TivoHMO::Server::Helpers }).new }
33
+
34
+ describe "helpers" do
35
+
36
+ include Rack::Utils
37
+
38
+ describe "#item_url" do
39
+
40
+ it "uses title_path as url" do
41
+ expect(helpers.item_url(item)).to eq item.title_path
42
+ end
43
+
44
+ end
45
+
46
+ describe "#container_url" do
47
+
48
+ it "generates a url" do
49
+ url = helpers.container_url(container)
50
+ uri = URI(url)
51
+ expect(uri.path).to eq("/TiVoConnect")
52
+ expect(parse_query(uri.query)).to eq('Command' => 'QueryContainer',
53
+ 'Container' => container.title_path)
54
+ end
55
+
56
+ end
57
+
58
+ describe "#item_detail_url" do
59
+
60
+ it "generates a url" do
61
+ url = helpers.item_detail_url(item)
62
+ uri = URI(url)
63
+ expect(uri.path).to eq("/TiVoConnect")
64
+ expect(parse_query(uri.query)).to eq('Command' => 'TVBusQuery',
65
+ 'Container' => 'a1',
66
+ 'File' => 'c1/i1')
67
+ end
68
+
69
+ end
70
+
71
+ describe "#format_uuid" do
72
+
73
+ it "uses crc to format" do
74
+ uuid = SecureRandom.uuid
75
+ expect(helpers.format_uuid(uuid)).to eq(Zlib.crc32(uuid))
76
+ end
77
+
78
+ end
79
+
80
+ describe "#format_date" do
81
+
82
+ it "produces date as hex" do
83
+ date = Time.now
84
+ expect(helpers.format_date(date)).to eq("0x#{date.to_i.to_s(16)}")
85
+ end
86
+
87
+ end
88
+
89
+ describe "#format_iso_date" do
90
+
91
+ it "is empty string for nil" do
92
+ date = nil
93
+ expect(helpers.format_iso_date(date)).to eq("")
94
+ end
95
+
96
+ it "is formatted for real date" do
97
+ date = Time.now
98
+ expect(helpers.format_iso_date(date)).to eq(date.utc.strftime("%FT%T.%6N"))
99
+ end
100
+
101
+ end
102
+
103
+ describe "#format_iso_duration" do
104
+
105
+ it "is empty string for nil" do
106
+ expect(helpers.format_iso_duration(nil)).to eq("")
107
+ end
108
+
109
+ it "produces iso formatted string" do
110
+ duration = (1 * 86400) + (1 * 3600) + (1 * 60) + 1
111
+ expect(helpers.format_iso_duration(duration)).to eq('P1DT1H1M1S')
112
+ end
113
+
114
+ end
115
+
116
+ describe "#tivo_header" do
117
+
118
+ it "determines pad length" do
119
+ expect(helpers.pad(7, 4)).to eq(1)
120
+ end
121
+
122
+ it "tivo_header" do
123
+ expect(helpers).to receive(:builder).
124
+ with(:item_details, layout: true, locals: {item: item}).
125
+ and_return("<TvBusMarshalledStruct/>")
126
+ expect(helpers.tivo_header(item, 'video/x-tivo-mpeg')).to include('TvBusMarshalledStruct')
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
133
+ describe "TivoConnect" do
134
+
135
+ it "should fail for no command" do
136
+ get '/TiVoConnect'
137
+ expect(last_response.status).to eq(404)
138
+ end
139
+
140
+ it "should have a stub response for ResetServer" do
141
+ get '/TiVoConnect?Command=ResetServer'
142
+ expect(last_response.status).to eq(200)
143
+ end
144
+
145
+ it "should have a stub response for FlushServer" do
146
+ get '/TiVoConnect?Command=FlushServer'
147
+ expect(last_response.status).to eq(200)
148
+ end
149
+
150
+ it "should have a stub response for QueryItem" do
151
+ get '/TiVoConnect?Command=QueryItem'
152
+ expect(last_response.status).to eq(200)
153
+ end
154
+
155
+ it "should have a response for QueryServer" do
156
+ get '/TiVoConnect?Command=QueryServer'
157
+ expect(last_response.status).to eq(200)
158
+ doc = Nokogiri::XML(last_response.body)
159
+ name = doc.at_xpath('/TiVoServer/InternalName').content
160
+ expect(name).to eq('TivoHMO')
161
+
162
+ end
163
+
164
+ describe "QueryFormats" do
165
+
166
+ it "should have an error for QueryFormats with video" do
167
+ get '/TiVoConnect?Command=QueryFormats'
168
+ expect(last_response.status).to eq(404)
169
+ end
170
+
171
+ it "should have a response for QueryFormats" do
172
+ get '/TiVoConnect?Command=QueryFormats&SourceFormat=video%2Fx-tivo-mpeg'
173
+ expect(last_response.status).to eq(200)
174
+ doc = Nokogiri::XML(last_response.body)
175
+ cts = doc.xpath('/TiVoFormats/Format/ContentType')
176
+ expect(cts.collect(&:content)).to eq(['video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts'])
177
+ end
178
+
179
+ end
180
+
181
+ describe "TVBusQuery" do
182
+
183
+ it "fails if it doesn't have params" do
184
+ get "/TiVoConnect?Command=TVBusQuery"
185
+ expect(last_response.status).to eq(404)
186
+ end
187
+
188
+ it "fails if it doesn't exist" do
189
+ get "/TiVoConnect?Command=TVBusQuery&Container=Foo&File=Bar"
190
+ expect(last_response.status).to eq(404)
191
+ end
192
+
193
+ it "provides item details" do
194
+ get "/TiVoConnect?Command=TVBusQuery&Container=a1&File=c1/i1"
195
+ expect(last_response.status).to eq(200)
196
+ doc = Nokogiri::XML(last_response.body)
197
+ expect(doc).to_not be_nil
198
+ end
199
+
200
+ end
201
+
202
+ describe "QueryContainer" do
203
+
204
+ it "displays root if it doesn't have params" do
205
+ get "/TiVoConnect?Command=QueryContainer"
206
+ expect(last_response.status).to eq(200)
207
+ doc = Nokogiri::XML(last_response.body)
208
+ title = doc.at_xpath("/TiVoContainer/Details/Title").content
209
+ expect(title).to eq(server.title)
210
+ end
211
+
212
+ it "uses server as root" do
213
+ get "/TiVoConnect?Command=QueryContainer&Container=/"
214
+ expect(last_response.status).to eq(200)
215
+ doc = Nokogiri::XML(last_response.body)
216
+ title = doc.at_xpath("/TiVoContainer/Details/Title").content
217
+ expect(title).to eq(server.title)
218
+ end
219
+
220
+ it "renders a container" do
221
+ container.add_child(TestAPI::Container.new("c2"))
222
+
223
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1"
224
+ expect(last_response.status).to eq(200)
225
+ doc = Nokogiri::XML(last_response.body)
226
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("0")
227
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("2")
228
+
229
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("2")
230
+ expect(doc.at_xpath("/TiVoContainer/Details/Title").content).to eq(container.title_path)
231
+ expect(doc.at_xpath("/TiVoContainer/Details/ContentType").content).to eq("x-tivo-container/folder")
232
+ expect(doc.at_xpath("/TiVoContainer/Details/SourceFormat").content).to eq("x-tivo-container/folder")
233
+ expect(doc.at_xpath("/TiVoContainer/Details/UniqueId").content).to eq(helpers.format_uuid(container.uuid).to_s)
234
+
235
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (2)
236
+
237
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
238
+ expect(child_titles).to match_array(["c2", "i1"])
239
+ end
240
+
241
+ it "shows details for child item" do
242
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1"
243
+ expect(last_response.status).to eq(200)
244
+ doc = Nokogiri::XML(last_response.body)
245
+
246
+ xml_item = doc.at_xpath("/TiVoContainer/Item")
247
+ expect(xml_item.at_xpath("Details/Title").content).to eq("i1")
248
+ expect(xml_item.at_xpath("Details/ContentType").content).to eq(item.content_type)
249
+ expect(xml_item.at_xpath("Details/SourceFormat").content).to eq(item.source_format)
250
+ expect(xml_item.at_xpath("Details/CaptureDate").content).to eq(helpers.format_date(item.created_at))
251
+ expect(xml_item.at_xpath("Details/UniqueId")).to be_nil
252
+ expect(xml_item.at_xpath("Details/TotalItems")).to be_nil
253
+ expect(xml_item.at_xpath("Details/LastCaptureDate")).to be_nil
254
+
255
+ # if metadata present
256
+ expect(xml_item.at_xpath("Details/SourceSize").content).to_not be_nil
257
+
258
+ xml_details = xml_item.at_xpath("Links/Content")
259
+ expect(xml_details.at_xpath("Url").content).to eq(helpers.item_url(item))
260
+ expect(xml_details.at_xpath("ContentType").content).to eq(item.content_type)
261
+
262
+ expect(xml_item.at_xpath("Links/CustomIcon").content).to_not be_nil
263
+
264
+ expect(xml_item.at_xpath("Links/TiVoVideoDetails/Url").content).to eq(helpers.item_detail_url(item))
265
+ end
266
+
267
+ it "shows details for child container" do
268
+ c2 = application.add_child(TestAPI::Container.new("c2"))
269
+ c3 = c2.add_child(TestAPI::Container.new("c3"))
270
+ c3 << TestAPI::Item.new('i2')
271
+ c3 << TestAPI::Item.new('i3')
272
+
273
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c2"
274
+ expect(last_response.status).to eq(200)
275
+ doc = Nokogiri::XML(last_response.body)
276
+
277
+ xml_item = doc.at_xpath("/TiVoContainer/Item")
278
+ expect(xml_item.at_xpath("Details/Title").content).to eq("c3")
279
+ expect(xml_item.at_xpath("Details/ContentType").content).to eq(c3.content_type)
280
+ expect(xml_item.at_xpath("Details/SourceFormat").content).to eq(c3.source_format)
281
+ expect(xml_item.at_xpath("Details/UniqueId").content).to eq(helpers.format_uuid(c3.uuid).to_s)
282
+ expect(xml_item.at_xpath("Details/TotalItems").content).to eq("2")
283
+ expect(xml_item.at_xpath("Details/LastCaptureDate").content).to eq(helpers.format_date(c3.created_at))
284
+
285
+ xml_details = xml_item.at_xpath("Links/Content")
286
+ expect(xml_details.at_xpath("Url").content).to eq(helpers.container_url(c3))
287
+ expect(xml_details.at_xpath("ContentType").content).to eq("x-tivo-container/folder")
288
+
289
+ end
290
+
291
+ describe "sorting" do
292
+
293
+ before(:each) do
294
+ 3.times do |i|
295
+ container.add_child(TestAPI::Item.new("i#{i + 2}"))
296
+ end
297
+ end
298
+
299
+ it "displays sorted by title" do
300
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&SortOrder=Title"
301
+ expect(last_response.status).to eq(200)
302
+ doc = Nokogiri::XML(last_response.body)
303
+
304
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
305
+ expect(child_titles).to eq(["i1", "i2", "i3", "i4"])
306
+ end
307
+
308
+ it "displays sorted by date" do
309
+ server.find("/a1/c1/i4").created_at = 4.hours.ago
310
+ server.find("/a1/c1/i3").created_at = 3.hours.ago
311
+ server.find("/a1/c1/i2").created_at = 2.hours.ago
312
+ server.find("/a1/c1/i1").created_at = 1.hour.ago
313
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&SortOrder=CaptureDate"
314
+ expect(last_response.status).to eq(200)
315
+ doc = Nokogiri::XML(last_response.body)
316
+
317
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
318
+ expect(child_titles).to eq(["i4", "i3", "i2", "i1"])
319
+ end
320
+
321
+ it "honors direction" do
322
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&SortOrder=!Title"
323
+ expect(last_response.status).to eq(200)
324
+ doc = Nokogiri::XML(last_response.body)
325
+
326
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
327
+ expect(child_titles).to eq(["i4", "i3", "i2", "i1"])
328
+ end
329
+
330
+ end
331
+
332
+ describe "pagination" do
333
+
334
+ before(:each) do
335
+ 19.times do |i|
336
+ container.add_child(TestAPI::Item.new("i#{i + 2}"))
337
+ end
338
+ end
339
+
340
+ it "displays first page" do
341
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1"
342
+ expect(last_response.status).to eq(200)
343
+ doc = Nokogiri::XML(last_response.body)
344
+
345
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("0")
346
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("8")
347
+
348
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
349
+
350
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (8)
351
+
352
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
353
+ expect(child_titles).to match_array(["i1", "i2", "i3", "i4", "i5", "i6", "i7", "i8"])
354
+ end
355
+
356
+ it "honors ItemStart" do
357
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&ItemStart=3"
358
+ expect(last_response.status).to eq(200)
359
+ doc = Nokogiri::XML(last_response.body)
360
+
361
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("3")
362
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("8")
363
+
364
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
365
+
366
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (8)
367
+
368
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
369
+ expect(child_titles).to match_array(["i4", "i5", "i6", "i7", "i8", "i9", "i10", "i11"])
370
+ end
371
+
372
+ it "honors ItemCount" do
373
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&ItemCount=3"
374
+ expect(last_response.status).to eq(200)
375
+ doc = Nokogiri::XML(last_response.body)
376
+
377
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("0")
378
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("3")
379
+
380
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
381
+
382
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (3)
383
+
384
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
385
+ expect(child_titles).to match_array(["i1", "i2", "i3"])
386
+ end
387
+
388
+ it "displays page 2" do
389
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&ItemStart=7"
390
+ expect(last_response.status).to eq(200)
391
+ doc = Nokogiri::XML(last_response.body)
392
+
393
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("7")
394
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("8")
395
+
396
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
397
+
398
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (8)
399
+
400
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
401
+ expect(child_titles).to match_array(["i8", "i9", "i10", "i11", "i12", "i13", "i14", "i15"])
402
+ end
403
+
404
+ it "displays page using anchors" do
405
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&AnchorItem=/a1/c1/i8&AnchorOffset=-1"
406
+ expect(last_response.status).to eq(200)
407
+ doc = Nokogiri::XML(last_response.body)
408
+
409
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("7")
410
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("8")
411
+
412
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
413
+
414
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (8)
415
+
416
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
417
+ expect(child_titles).to match_array(["i8", "i9", "i10", "i11", "i12", "i13", "i14", "i15"])
418
+ end
419
+
420
+ it "honors anchor offset when displaying page using anchors" do
421
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&AnchorItem=/a1/c1/i8&AnchorOffset=-8"
422
+ expect(last_response.status).to eq(200)
423
+ doc = Nokogiri::XML(last_response.body)
424
+
425
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("0")
426
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("8")
427
+
428
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
429
+
430
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (8)
431
+
432
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
433
+ expect(child_titles).to match_array(["i1", "i2", "i3", "i4", "i5", "i6", "i7", "i8"])
434
+ end
435
+
436
+ end
437
+
438
+ end
439
+
440
+ end
441
+
442
+ describe "transcode" do
443
+
444
+ it "fails if it doesn't exist" do
445
+ get "/this/is/not/here"
446
+ expect(last_response.status).to eq(404)
447
+ end
448
+
449
+ it "sends transcoded data" do
450
+ get "/a1/c1/i1?Format=?video/x-tivo-mpeg"
451
+ expect(last_response.status).to eq(206)
452
+ expect(last_response.body).to include("/A1/C1/I1")
453
+ end
454
+
455
+ end
456
+
457
+ end
458
+
@@ -0,0 +1,123 @@
1
+
2
+ require 'rspec'
3
+ RSpec.configure do |config|
4
+ end
5
+
6
+ require 'coveralls'
7
+ Coveralls.wear!
8
+
9
+ require 'tivohmo/logging'
10
+ Logging.logger.root.level = :error
11
+
12
+ require 'tivohmo'
13
+ require 'open-uri'
14
+
15
+ def video_fixture(name)
16
+ fixture_names = {
17
+ tiny: 'MPEG4 by philips.mp4',
18
+ with_audio: 'test_qcif_200_aac_64.mp4'
19
+ }
20
+
21
+ file_name = fixture_names[name]
22
+ raise ArgumentError, "No fixture for :#{name}" unless file_name
23
+
24
+ base_url = 'http://samples.mplayerhq.hu/MPEG-4'
25
+ video_url = "#{base_url}/#{URI.escape(file_name)}"
26
+
27
+ cache_dir = '/tmp/video_fixtures'
28
+ video_file = "#{cache_dir}/#{file_name}"
29
+
30
+ if ! File.exist?(video_file)
31
+ puts "Downloading video fixture #{name} from #{video_url}"
32
+ FileUtils.mkdir_p(cache_dir)
33
+ File.write(video_file, open(video_url).read)
34
+ end
35
+
36
+ video_file
37
+ end
38
+
39
+ module TestAPI
40
+
41
+ class Application
42
+ include TivoHMO::API::Application
43
+ end
44
+
45
+ class Container
46
+ include TivoHMO::API::Container
47
+ end
48
+
49
+ class Item
50
+ include TivoHMO::API::Item
51
+ end
52
+
53
+ class Metadata
54
+ include TivoHMO::API::Metadata
55
+ end
56
+
57
+ class Transcoder
58
+ include TivoHMO::API::Transcoder
59
+ def transcoder_options; {}; end
60
+ def transcode(io, format); io << item.title_path.upcase; end
61
+ end
62
+ end
63
+
64
+ def make_api_tree(parent, *items)
65
+ parent = TivoHMO::API::Server.new unless parent
66
+ if parent.is_a?(TivoHMO::API::Server)
67
+ parent = parent.add_child(TestAPI::Application.new('a'))
68
+ end
69
+
70
+ items.each do |item|
71
+ if item.is_a?(Hash)
72
+ item.each do |name, child_items|
73
+ c = parent.add_child(TestAPI::Container.new(name.to_s))
74
+ make_api_tree(c, *child_items)
75
+ end
76
+ elsif item.is_a?(Enumerable)
77
+ make_api_tree(parent, *item)
78
+ else
79
+ parent.add_child(TestAPI::Item.new(item.to_s))
80
+ end
81
+ end
82
+
83
+ parent
84
+ end
85
+
86
+ def mktree(path, items)
87
+ items.each do |item|
88
+ if item.is_a?(Hash)
89
+ item.each do |dir, contents|
90
+ p = "#{path}/#{dir}"
91
+ FileUtils.mkdir p
92
+ mktree(p, contents)
93
+ end
94
+ else
95
+ p = "#{path}/#{item}"
96
+ FileUtils.touch p
97
+ end
98
+ end
99
+ end
100
+
101
+ def with_file_tree(*tree)
102
+ Dir.mktmpdir('tivohmo_test') do |dir|
103
+ mktree(dir, tree)
104
+ yield dir
105
+ end
106
+ end
107
+
108
+ def plex_stub(clazz, method_stubs={})
109
+ default_stubs = {
110
+ key: '/some/key',
111
+ title: 'Title',
112
+ updated_at: Time.now.to_i,
113
+ added_at: Time.now.to_i,
114
+ refresh: nil
115
+ }
116
+ d = double(clazz, default_stubs.merge(method_stubs))
117
+
118
+ allow(d).to receive(:is_a?) do |arg|
119
+ arg == clazz
120
+ end
121
+
122
+ d
123
+ end
data/tivohmo.gemspec ADDED
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tivohmo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tivohmo"
8
+ spec.version = TivoHMO::VERSION
9
+ spec.authors = ["Matt Conway"]
10
+ spec.email = ["matt@conwaysplace.com"]
11
+ spec.summary = %q{Ruby SDK for Tivo Home Media Option}
12
+ spec.description = %q{Allows one to author Tivo HMO applications using ruby}
13
+ spec.homepage = ""
14
+ spec.license = "LGPL"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "pry"
24
+ spec.add_development_dependency "pry-byebug"
25
+ spec.add_development_dependency "awesome_print"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "rack-test"
28
+ spec.add_development_dependency "nokogiri"
29
+
30
+ # core dependencies
31
+ spec.add_dependency "activesupport"
32
+ spec.add_dependency "gem_logger"
33
+ spec.add_dependency "logging"
34
+ spec.add_dependency "sigdump"
35
+ spec.add_dependency "clamp"
36
+ spec.add_dependency "sinatra"
37
+ spec.add_dependency "builder"
38
+ spec.add_dependency "puma"
39
+
40
+ # filesystem adapter dependencies, make optional?
41
+ spec.add_dependency "listen"
42
+ spec.add_dependency "streamio-ffmpeg"
43
+
44
+ # plex adapter dependencies, make optional?
45
+ spec.add_dependency "plex-ruby"
46
+ end