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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +514 -0
- data/README.md +50 -0
- data/Rakefile +7 -0
- data/bin/tivohmo +8 -0
- data/contrib/tivohmo.conf +17 -0
- data/contrib/tivohmo.plist +22 -0
- data/lib/tivohmo/adapters/filesystem/application.rb +23 -0
- data/lib/tivohmo/adapters/filesystem/file_item.rb +29 -0
- data/lib/tivohmo/adapters/filesystem/folder_container.rb +105 -0
- data/lib/tivohmo/adapters/filesystem.rb +3 -0
- data/lib/tivohmo/adapters/plex/application.rb +41 -0
- data/lib/tivohmo/adapters/plex/category.rb +72 -0
- data/lib/tivohmo/adapters/plex/episode.rb +26 -0
- data/lib/tivohmo/adapters/plex/metadata.rb +24 -0
- data/lib/tivohmo/adapters/plex/movie.rb +26 -0
- data/lib/tivohmo/adapters/plex/qualified_category.rb +51 -0
- data/lib/tivohmo/adapters/plex/season.rb +39 -0
- data/lib/tivohmo/adapters/plex/section.rb +48 -0
- data/lib/tivohmo/adapters/plex/show.rb +39 -0
- data/lib/tivohmo/adapters/plex/transcoder.rb +19 -0
- data/lib/tivohmo/adapters/plex.rb +11 -0
- data/lib/tivohmo/adapters/streamio/metadata.rb +26 -0
- data/lib/tivohmo/adapters/streamio/transcoder.rb +239 -0
- data/lib/tivohmo/adapters/streamio.rb +17 -0
- data/lib/tivohmo/api/application.rb +35 -0
- data/lib/tivohmo/api/container.rb +31 -0
- data/lib/tivohmo/api/item.rb +32 -0
- data/lib/tivohmo/api/metadata.rb +98 -0
- data/lib/tivohmo/api/node.rb +115 -0
- data/lib/tivohmo/api/server.rb +24 -0
- data/lib/tivohmo/api/transcoder.rb +39 -0
- data/lib/tivohmo/api.rb +7 -0
- data/lib/tivohmo/beacon.rb +69 -0
- data/lib/tivohmo/cli.rb +182 -0
- data/lib/tivohmo/logging.rb +50 -0
- data/lib/tivohmo/server/views/_container.builder +19 -0
- data/lib/tivohmo/server/views/_item.builder +57 -0
- data/lib/tivohmo/server/views/container.builder +26 -0
- data/lib/tivohmo/server/views/item_details.builder +153 -0
- data/lib/tivohmo/server/views/layout.builder +2 -0
- data/lib/tivohmo/server/views/layout.erb +10 -0
- data/lib/tivohmo/server/views/server.builder +18 -0
- data/lib/tivohmo/server/views/server_info.builder +7 -0
- data/lib/tivohmo/server/views/unsupported.erb +7 -0
- data/lib/tivohmo/server/views/video_formats.builder +10 -0
- data/lib/tivohmo/server.rb +306 -0
- data/lib/tivohmo/version.rb +3 -0
- data/lib/tivohmo.rb +5 -0
- data/spec/adapters/filesystem/application_spec.rb +19 -0
- data/spec/adapters/filesystem/file_item_spec.rb +33 -0
- data/spec/adapters/filesystem/folder_container_spec.rb +115 -0
- data/spec/adapters/plex/application_spec.rb +20 -0
- data/spec/adapters/plex/category_spec.rb +66 -0
- data/spec/adapters/plex/episode_spec.rb +22 -0
- data/spec/adapters/plex/metadata_spec.rb +24 -0
- data/spec/adapters/plex/movie_spec.rb +22 -0
- data/spec/adapters/plex/qualified_category_spec.rb +51 -0
- data/spec/adapters/plex/season_spec.rb +22 -0
- data/spec/adapters/plex/section_spec.rb +38 -0
- data/spec/adapters/plex/show_spec.rb +22 -0
- data/spec/adapters/plex/transcoder_spec.rb +27 -0
- data/spec/adapters/streamio/metadata_spec.rb +34 -0
- data/spec/adapters/streamio/transcoder_spec.rb +42 -0
- data/spec/api/application_spec.rb +63 -0
- data/spec/api/container_spec.rb +34 -0
- data/spec/api/item_spec.rb +53 -0
- data/spec/api/metadata_spec.rb +59 -0
- data/spec/api/node_spec.rb +178 -0
- data/spec/api/server_spec.rb +24 -0
- data/spec/api/transcoder_spec.rb +25 -0
- data/spec/beacon_spec.rb +87 -0
- data/spec/cli_spec.rb +227 -0
- data/spec/server_spec.rb +458 -0
- data/spec/spec_helper.rb +123 -0
- data/tivohmo.gemspec +46 -0
- 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
|
data/lib/tivohmo.rb
ADDED
@@ -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
|