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