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,239 @@
|
|
1
|
+
module TivoHMO
|
2
|
+
module Adapters
|
3
|
+
module StreamIO
|
4
|
+
|
5
|
+
|
6
|
+
# Transcodes video to tivo format using the streamio gem (ffmpeg)
|
7
|
+
class Transcoder
|
8
|
+
include TivoHMO::API::Transcoder
|
9
|
+
include GemLogger::LoggerSupport
|
10
|
+
|
11
|
+
# TODO: add ability to pass through data (copy codec)
|
12
|
+
# for files that are already (partially?) in the right
|
13
|
+
# format for tivo. Check against a mapping of
|
14
|
+
# tivo serial->allowed_formats
|
15
|
+
# https://code.google.com/p/streambaby/wiki/video_compatibility
|
16
|
+
|
17
|
+
def transcode(writeable_io, format="video/x-tivo-mpeg")
|
18
|
+
tmpfile = Tempfile.new('tivohmo_transcode')
|
19
|
+
begin
|
20
|
+
transcode_thread = run_transcode(tmpfile.path, format)
|
21
|
+
|
22
|
+
# give the transcode thread a chance to start up before we
|
23
|
+
# start copying from it. Not strictly necessary, but makes
|
24
|
+
# the log messages show up in the right order
|
25
|
+
sleep 0.1
|
26
|
+
|
27
|
+
run_copy(tmpfile.path, writeable_io, transcode_thread)
|
28
|
+
ensure
|
29
|
+
tmpfile.close
|
30
|
+
tmpfile.unlink
|
31
|
+
end
|
32
|
+
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def transcoder_options(format="video/x-tivo-mpeg")
|
37
|
+
opts = {
|
38
|
+
video_max_bitrate: 30000,
|
39
|
+
buffer_size: 4096,
|
40
|
+
audio_bitrate: 448,
|
41
|
+
format: format,
|
42
|
+
custom: []
|
43
|
+
}
|
44
|
+
|
45
|
+
opts = select_video_frame_rate(opts)
|
46
|
+
opts = select_video_dimensions(opts)
|
47
|
+
opts = select_video_codec(opts)
|
48
|
+
opts = select_video_bitrate(opts)
|
49
|
+
opts = select_audio_codec(opts)
|
50
|
+
opts = select_audio_sample_rate(opts)
|
51
|
+
opts = select_container(opts)
|
52
|
+
|
53
|
+
opts[:custom] = opts[:custom].join(" ") if opts[:custom]
|
54
|
+
opts.delete(:format)
|
55
|
+
|
56
|
+
opts
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def movie
|
62
|
+
@movie ||= FFMPEG::Movie.new(source_filename)
|
63
|
+
end
|
64
|
+
|
65
|
+
def video_info
|
66
|
+
@video_info ||= begin
|
67
|
+
info_attrs = %w[
|
68
|
+
path duration time bitrate rotation creation_time
|
69
|
+
video_stream video_codec video_bitrate colorspace dar
|
70
|
+
audio_stream audio_codec audio_bitrate audio_sample_rate
|
71
|
+
calculated_aspect_ratio size audio_channels frame_rate container
|
72
|
+
resolution width height
|
73
|
+
]
|
74
|
+
Hash[info_attrs.collect {|attr| [attr.to_sym, movie.send(attr)] }]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def select_container(opts)
|
79
|
+
if opts[:format] == 'video/x-tivo-mpeg-ts'
|
80
|
+
opts[:custom] << "-f mpegts"
|
81
|
+
else
|
82
|
+
opts[:custom] << "-f vob"
|
83
|
+
end
|
84
|
+
opts
|
85
|
+
end
|
86
|
+
|
87
|
+
def select_audio_sample_rate(opts)
|
88
|
+
if video_info[:audio_sample_rate]
|
89
|
+
if AUDIO_SAMPLE_RATES.include?(video_info[:audio_sample_rate])
|
90
|
+
opts[:audio_sample_rate] = video_info[:audio_sample_rate]
|
91
|
+
else
|
92
|
+
opts[:audio_sample_rate] = 48000
|
93
|
+
end
|
94
|
+
end
|
95
|
+
opts
|
96
|
+
end
|
97
|
+
|
98
|
+
def select_audio_codec(opts)
|
99
|
+
if video_info[:audio_codec]
|
100
|
+
if AUDIO_CODECS.any? { |ac| video_info[:audio_codec] =~ /#{ac}/ }
|
101
|
+
opts[:audio_codec] = 'copy'
|
102
|
+
if video_info[:video_codec] =~ /mpeg2video/
|
103
|
+
opts[:custom] << "-copyts"
|
104
|
+
end
|
105
|
+
else
|
106
|
+
opts[:audio_codec] = 'ac3'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
opts
|
110
|
+
end
|
111
|
+
|
112
|
+
def select_video_bitrate(opts)
|
113
|
+
if video_info[:video_bitrate].to_i >= opts[:video_max_bitrate]
|
114
|
+
opts[:video_bitrate] = (video_info[:video_max_bitrate] * 0.95).to_i
|
115
|
+
end
|
116
|
+
opts
|
117
|
+
end
|
118
|
+
|
119
|
+
def select_video_codec(opts)
|
120
|
+
if VIDEO_CODECS.any? { |vc| video_info[:video_codec] =~ /#{vc}/ }
|
121
|
+
opts[:video_codec] = 'copy'
|
122
|
+
if video_info[:video_codec] =~ /h264/
|
123
|
+
opts[:custom] << "-bsf h264_mp4toannexb"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
opts[:video_codec] = 'mpeg2video'
|
127
|
+
opts[:custom] << "-pix_fmt yuv420p"
|
128
|
+
end
|
129
|
+
opts
|
130
|
+
end
|
131
|
+
|
132
|
+
def select_video_dimensions(opts)
|
133
|
+
preserve_aspect = nil
|
134
|
+
video_width = video_info[:width].to_i
|
135
|
+
VIDEO_WIDTHS.each do |w|
|
136
|
+
w = w.to_i
|
137
|
+
if video_width >= w
|
138
|
+
video_width = w
|
139
|
+
opts[:preserve_aspect_ratio] = :width
|
140
|
+
break
|
141
|
+
end
|
142
|
+
end
|
143
|
+
video_width = VIDEO_WIDTHS.last.to_i unless video_width
|
144
|
+
|
145
|
+
video_height = video_info[:height].to_i
|
146
|
+
VIDEO_WIDTHS.each do |h|
|
147
|
+
h = h.to_i
|
148
|
+
if video_height >= h
|
149
|
+
video_height = h
|
150
|
+
opts[:preserve_aspect_ratio] = :height
|
151
|
+
break
|
152
|
+
end
|
153
|
+
end
|
154
|
+
video_height = VIDEO_HEIGHTS.last.to_i unless video_height
|
155
|
+
opts[:resolution] = "#{video_width}x#{video_height}"
|
156
|
+
opts[:preserve_aspect_ratio] = :height unless opts[:preserve_aspect_ratio]
|
157
|
+
opts
|
158
|
+
end
|
159
|
+
|
160
|
+
def select_video_frame_rate(opts)
|
161
|
+
if !VIDEO_FRAME_RATES.include?(video_info[:frame_rate])
|
162
|
+
opts[:frame_rate] = 29.97
|
163
|
+
end
|
164
|
+
opts
|
165
|
+
end
|
166
|
+
|
167
|
+
def run_transcode(output_filename, format)
|
168
|
+
|
169
|
+
logger.debug "Movie Info: " +
|
170
|
+
video_info.collect {|k, v| "#{k}='#{v}'"}.join(' ')
|
171
|
+
|
172
|
+
opts = transcoder_options(format)
|
173
|
+
|
174
|
+
logger.debug "Transcoding options: " +
|
175
|
+
opts.collect {|k, v| "#{k}='#{v}'"}.join(' ')
|
176
|
+
|
177
|
+
|
178
|
+
aspect_opt = opts.delete(:preserve_aspect_ratio)
|
179
|
+
t_opts = {}
|
180
|
+
t_opts[:preserve_aspect_ratio] = aspect_opt if aspect_opt
|
181
|
+
|
182
|
+
|
183
|
+
transcode_thread = Thread.new do
|
184
|
+
begin
|
185
|
+
logger.info "Starting transcode to: #{output_filename}"
|
186
|
+
transcoded_movie = movie.transcode(output_filename, opts, t_opts) do |progress|
|
187
|
+
logger.info "Trancoding #{item}: #{progress}"
|
188
|
+
raise "Halted" if Thread.current[:halt]
|
189
|
+
end
|
190
|
+
logger.info "Transcoding completed, transcoded file size: #{File.size(output_filename)}"
|
191
|
+
rescue => e
|
192
|
+
logger.error ("Transcode failed: #{e}")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
return transcode_thread
|
197
|
+
end
|
198
|
+
|
199
|
+
# we could avoid this if streamio-ffmpeg had a way to output to an IO, but
|
200
|
+
# it only supports file based output for now, so have to manually copy the
|
201
|
+
# file's bytes to our output stream
|
202
|
+
def run_copy(transcoded_filename, writeable_io, transcode_thread)
|
203
|
+
logger.info "Starting stream copy from: #{transcoded_filename}"
|
204
|
+
file = File.open(transcoded_filename, 'rb')
|
205
|
+
begin
|
206
|
+
bytes_copied = 0
|
207
|
+
|
208
|
+
# copying the IO from transcoded file to web output
|
209
|
+
# stream is faster than the transcoding, and thus we
|
210
|
+
# hit eof before transcode is done. Therefore we need
|
211
|
+
# to keep retrying while the transcode thread is alive,
|
212
|
+
# then to avoid a race condition at the end, we keep
|
213
|
+
# going till we've copied all the bytes
|
214
|
+
while transcode_thread.alive? || bytes_copied < File.size(transcoded_filename)
|
215
|
+
# sleep a bit at start of thread so we don't have a
|
216
|
+
# wasteful tight loop when transcoding is really slow
|
217
|
+
sleep 0.2
|
218
|
+
|
219
|
+
while data = file.read(4096)
|
220
|
+
break unless data.size > 0
|
221
|
+
writeable_io << data
|
222
|
+
bytes_copied += data.size
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
logger.info "Stream copy completed, #{bytes_copied} bytes copied"
|
227
|
+
rescue => e
|
228
|
+
logger.error ("Stream copy failed: #{e}")
|
229
|
+
transcode_thread[:halt] = true
|
230
|
+
ensure
|
231
|
+
file.close
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'streamio-ffmpeg'
|
2
|
+
require_relative 'streamio/transcoder'
|
3
|
+
require_relative 'streamio/metadata'
|
4
|
+
|
5
|
+
module TivoHMO
|
6
|
+
module BasicAdapter
|
7
|
+
|
8
|
+
class StreamIO
|
9
|
+
include GemLogger::LoggerSupport
|
10
|
+
|
11
|
+
FFMPEG.logger = self.logger
|
12
|
+
# FFMPEG.ffmpeg_binary =
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module TivoHMO
|
2
|
+
module API
|
3
|
+
|
4
|
+
# Represents the tivo concept of a Server (i.e. the root node
|
5
|
+
# which contains the top level containers). The identifier
|
6
|
+
# passed to the ctor should be a string that makes sense
|
7
|
+
# for initializing a subclass of app, e.g. a directory,
|
8
|
+
# a hostname:port, etc
|
9
|
+
module Application
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include Container
|
12
|
+
include GemLogger::LoggerSupport
|
13
|
+
|
14
|
+
attr_accessor :transcoder_class,
|
15
|
+
:metadata_class
|
16
|
+
|
17
|
+
def initialize(identifier)
|
18
|
+
super(identifier)
|
19
|
+
self.app = self
|
20
|
+
self.content_type = "x-container/tivo-videos"
|
21
|
+
self.source_format = "x-container/folder"
|
22
|
+
end
|
23
|
+
|
24
|
+
def metadata_for(item)
|
25
|
+
metadata_class.new(item) if metadata_class
|
26
|
+
end
|
27
|
+
|
28
|
+
def transcoder_for(item)
|
29
|
+
transcoder_class.new(item) if transcoder_class
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module TivoHMO
|
4
|
+
module API
|
5
|
+
|
6
|
+
# Represents the tivo concept of a Container (i.e. a directory that contains
|
7
|
+
# files or other containers)
|
8
|
+
module Container
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include Node
|
11
|
+
include GemLogger::LoggerSupport
|
12
|
+
|
13
|
+
attr_accessor :uuid
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(identifier)
|
17
|
+
super(identifier)
|
18
|
+
self.uuid = SecureRandom.uuid
|
19
|
+
|
20
|
+
self.content_type = "x-tivo-container/tivo-videos"
|
21
|
+
self.source_format = "x-tivo-container/folder"
|
22
|
+
end
|
23
|
+
|
24
|
+
def refresh
|
25
|
+
self.children = []
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TivoHMO
|
2
|
+
module API
|
3
|
+
|
4
|
+
# Represents the tivo concept of an Item (i.e. a file that can be
|
5
|
+
# displayed), and is always a leaf node in the tree.
|
6
|
+
module Item
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include Node
|
9
|
+
include GemLogger::LoggerSupport
|
10
|
+
|
11
|
+
def initialize(identifier)
|
12
|
+
super(identifier)
|
13
|
+
self.content_type = "video/x-tivo-mpeg"
|
14
|
+
self.source_format = "video/x-tivo-mpeg"
|
15
|
+
end
|
16
|
+
|
17
|
+
def metadata
|
18
|
+
@metadata ||= app.metadata_for(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def transcoder
|
22
|
+
@transcoder ||= app.transcoder_for(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"<#{self.class.name}: #{self.identifier}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module TivoHMO
|
2
|
+
module API
|
3
|
+
|
4
|
+
# Metadata abstraction for containing and displaying supplemental info about an Item
|
5
|
+
module Metadata
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include GemLogger::LoggerSupport
|
8
|
+
|
9
|
+
attr_accessor :item,
|
10
|
+
|
11
|
+
:title,
|
12
|
+
:description,
|
13
|
+
|
14
|
+
:time, # Time
|
15
|
+
:start_time, # Time
|
16
|
+
:stop_time, # Time
|
17
|
+
:source_size, # int, bytes
|
18
|
+
|
19
|
+
:actual_showing,
|
20
|
+
:bookmark,
|
21
|
+
:recording_quality, # hash of :name, :value
|
22
|
+
:duration, # int, seconds
|
23
|
+
|
24
|
+
:showing_bits,
|
25
|
+
:part_count,
|
26
|
+
:part_index,
|
27
|
+
|
28
|
+
:actors,
|
29
|
+
:choreographers,
|
30
|
+
:directors,
|
31
|
+
:producers,
|
32
|
+
:executive_producers,
|
33
|
+
:writers,
|
34
|
+
:hosts,
|
35
|
+
:guest_stars,
|
36
|
+
:program_genres,
|
37
|
+
|
38
|
+
:original_air_date,
|
39
|
+
:movie_year,
|
40
|
+
:advisory,
|
41
|
+
:color_code, # hash of :name, :value
|
42
|
+
:show_type, # hash of :name, :value
|
43
|
+
:program_id,
|
44
|
+
:mpaa_rating, # hash of :name, :value
|
45
|
+
:star_rating, # hash of :name, :value
|
46
|
+
:tv_rating, # hash of :name, :value
|
47
|
+
|
48
|
+
:is_episode,
|
49
|
+
:episode_number,
|
50
|
+
:episode_title,
|
51
|
+
|
52
|
+
:series_genres,
|
53
|
+
:series_title,
|
54
|
+
:series_id,
|
55
|
+
|
56
|
+
:channel # hash of :major_number, :minor_number, :callsign
|
57
|
+
|
58
|
+
|
59
|
+
def initialize(item)
|
60
|
+
self.item = item
|
61
|
+
self.duration = 0
|
62
|
+
self.showing_bits = 4096
|
63
|
+
self.is_episode = true
|
64
|
+
self.recording_quality = {name: "HIGH", value: "75"}
|
65
|
+
self.color_code = {name: 'COLOR', value: '4'}
|
66
|
+
self.show_type = {name: 'SERIES', value: '5'}
|
67
|
+
self.channel = {major_number: '0', minor_number: '0', callsign: ''}
|
68
|
+
end
|
69
|
+
|
70
|
+
def time
|
71
|
+
@time ||= Time.now
|
72
|
+
end
|
73
|
+
|
74
|
+
def start_time
|
75
|
+
@start_time ||= time
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop_time
|
79
|
+
@stop_time ||= time + duration
|
80
|
+
end
|
81
|
+
|
82
|
+
def source_size
|
83
|
+
@source_size ||= estimate_source_size
|
84
|
+
end
|
85
|
+
|
86
|
+
def estimate_source_size
|
87
|
+
# This is needed so that we can give tivo an estimate of transcoded size
|
88
|
+
# so transfer doesn't abort half way through. Using the max audio and
|
89
|
+
# video bit rates for a max estimate
|
90
|
+
opts = item.transcoder.transcoder_options
|
91
|
+
vbr = (opts[:video_bitrate] || opts[:video_max_bitrate] || 30000) * 1000
|
92
|
+
abr = (opts[:audio_bitrate] || 448) * 1000
|
93
|
+
(self.duration * ((abr + vbr) * 1.02 / 8)).to_i
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module TivoHMO
|
2
|
+
module API
|
3
|
+
|
4
|
+
# A tree node. Nodes have a parent, children and a root, with the tree
|
5
|
+
# itself representing the app/container/items heirarchy
|
6
|
+
module Node
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
# We could have used https://github.com/evolve75/RubyTree here instead of
|
9
|
+
# hand coding a tree, but since this is part of the 'api' I figured it
|
10
|
+
# was better not to have any external dependencies
|
11
|
+
|
12
|
+
include GemLogger::LoggerSupport
|
13
|
+
|
14
|
+
attr_accessor :identifier,
|
15
|
+
:parent,
|
16
|
+
:children,
|
17
|
+
:root,
|
18
|
+
:app,
|
19
|
+
:title,
|
20
|
+
:content_type,
|
21
|
+
:source_format,
|
22
|
+
:modified_at,
|
23
|
+
:created_at
|
24
|
+
|
25
|
+
def initialize(identifier)
|
26
|
+
self.identifier = identifier
|
27
|
+
self.title = identifier.to_s
|
28
|
+
self.created_at = Time.now
|
29
|
+
self.modified_at = Time.now
|
30
|
+
@children = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_child(child)
|
34
|
+
raise ArgumentError, "Not a node: #{child}" unless child.is_a?(Node)
|
35
|
+
child.parent = self
|
36
|
+
child.root = self.root if self.root
|
37
|
+
child.app = self.app if self.app
|
38
|
+
@children << child
|
39
|
+
child
|
40
|
+
end
|
41
|
+
|
42
|
+
alias << add_child
|
43
|
+
|
44
|
+
def root?
|
45
|
+
self == self.root
|
46
|
+
end
|
47
|
+
|
48
|
+
def app?
|
49
|
+
self == self.app
|
50
|
+
end
|
51
|
+
|
52
|
+
def find(title_path)
|
53
|
+
|
54
|
+
unless title_path.is_a?(Array)
|
55
|
+
title_path = title_path.split('/')
|
56
|
+
return root if title_path.blank?
|
57
|
+
if title_path.first == ""
|
58
|
+
return root.find(title_path[1..-1])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
next_title, rest = title_path[0], title_path[1..-1]
|
63
|
+
|
64
|
+
self.children.find do |c|
|
65
|
+
if c.title == next_title
|
66
|
+
if rest.blank?
|
67
|
+
return c
|
68
|
+
else
|
69
|
+
return c.find(rest)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def title_path
|
78
|
+
if self == root
|
79
|
+
"/"
|
80
|
+
else
|
81
|
+
if parent == root
|
82
|
+
"/#{self.title}"
|
83
|
+
else
|
84
|
+
"#{parent.title_path}/#{self.title}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def tree_string
|
90
|
+
result = ""
|
91
|
+
if self.root
|
92
|
+
n = root
|
93
|
+
else
|
94
|
+
result << "(no root)\n"
|
95
|
+
n = self
|
96
|
+
end
|
97
|
+
queue = [n]
|
98
|
+
queue.each do |node|
|
99
|
+
ident = node.title_path
|
100
|
+
result << ident << "\n"
|
101
|
+
if node.children.present?
|
102
|
+
queue.concat(node.children)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
"<#{self.class.name}: #{self.identifier}>"
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module TivoHMO
|
4
|
+
module API
|
5
|
+
|
6
|
+
# Represents the tivo concept of a Server (i.e. the root node
|
7
|
+
# which contains the top level applications)
|
8
|
+
class Server
|
9
|
+
include Container
|
10
|
+
include GemLogger::LoggerSupport
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
super('TivoHMO Server')
|
14
|
+
self.root = self
|
15
|
+
self.title = Socket.gethostname.split('.').first
|
16
|
+
self.content_type = "x-container/tivo-server"
|
17
|
+
self.source_format = "x-container/folder"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module TivoHMO
|
3
|
+
module API
|
4
|
+
|
5
|
+
# Transcoder abstraction for reading in the data from an Item and
|
6
|
+
# transcoding it into a format suitable for display on a tivo
|
7
|
+
module Transcoder
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
include GemLogger::LoggerSupport
|
10
|
+
|
11
|
+
# https://code.google.com/p/streambaby/wiki/video_compatibility
|
12
|
+
VIDEO_FRAME_RATES = %w[23.98 24.00 25.00 29.97
|
13
|
+
30.00 50.00 59.94 60.00]
|
14
|
+
VIDEO_CODECS = %w[mpeg2video] # h264 only for push?
|
15
|
+
VIDEO_WIDTHS = %w[1920 1440 1280 720 704 544 480 352]
|
16
|
+
VIDEO_HEIGHTS = %w[1080 720 480 240]
|
17
|
+
|
18
|
+
AUDIO_CODECS = %w[ac3 liba52 mp2]
|
19
|
+
AUDIO_SAMPLE_RATES = %w[44100 48000]
|
20
|
+
|
21
|
+
attr_accessor :item,
|
22
|
+
:source_filename
|
23
|
+
|
24
|
+
def initialize(item)
|
25
|
+
self.item = item
|
26
|
+
self.source_filename = item.identifier
|
27
|
+
end
|
28
|
+
|
29
|
+
def transcode(writeable_io, format)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def transcoder_options(format="video/x-tivo-mpeg")
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|