tivohmo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +17 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +514 -0
  7. data/README.md +50 -0
  8. data/Rakefile +7 -0
  9. data/bin/tivohmo +8 -0
  10. data/contrib/tivohmo.conf +17 -0
  11. data/contrib/tivohmo.plist +22 -0
  12. data/lib/tivohmo/adapters/filesystem/application.rb +23 -0
  13. data/lib/tivohmo/adapters/filesystem/file_item.rb +29 -0
  14. data/lib/tivohmo/adapters/filesystem/folder_container.rb +105 -0
  15. data/lib/tivohmo/adapters/filesystem.rb +3 -0
  16. data/lib/tivohmo/adapters/plex/application.rb +41 -0
  17. data/lib/tivohmo/adapters/plex/category.rb +72 -0
  18. data/lib/tivohmo/adapters/plex/episode.rb +26 -0
  19. data/lib/tivohmo/adapters/plex/metadata.rb +24 -0
  20. data/lib/tivohmo/adapters/plex/movie.rb +26 -0
  21. data/lib/tivohmo/adapters/plex/qualified_category.rb +51 -0
  22. data/lib/tivohmo/adapters/plex/season.rb +39 -0
  23. data/lib/tivohmo/adapters/plex/section.rb +48 -0
  24. data/lib/tivohmo/adapters/plex/show.rb +39 -0
  25. data/lib/tivohmo/adapters/plex/transcoder.rb +19 -0
  26. data/lib/tivohmo/adapters/plex.rb +11 -0
  27. data/lib/tivohmo/adapters/streamio/metadata.rb +26 -0
  28. data/lib/tivohmo/adapters/streamio/transcoder.rb +239 -0
  29. data/lib/tivohmo/adapters/streamio.rb +17 -0
  30. data/lib/tivohmo/api/application.rb +35 -0
  31. data/lib/tivohmo/api/container.rb +31 -0
  32. data/lib/tivohmo/api/item.rb +32 -0
  33. data/lib/tivohmo/api/metadata.rb +98 -0
  34. data/lib/tivohmo/api/node.rb +115 -0
  35. data/lib/tivohmo/api/server.rb +24 -0
  36. data/lib/tivohmo/api/transcoder.rb +39 -0
  37. data/lib/tivohmo/api.rb +7 -0
  38. data/lib/tivohmo/beacon.rb +69 -0
  39. data/lib/tivohmo/cli.rb +182 -0
  40. data/lib/tivohmo/logging.rb +50 -0
  41. data/lib/tivohmo/server/views/_container.builder +19 -0
  42. data/lib/tivohmo/server/views/_item.builder +57 -0
  43. data/lib/tivohmo/server/views/container.builder +26 -0
  44. data/lib/tivohmo/server/views/item_details.builder +153 -0
  45. data/lib/tivohmo/server/views/layout.builder +2 -0
  46. data/lib/tivohmo/server/views/layout.erb +10 -0
  47. data/lib/tivohmo/server/views/server.builder +18 -0
  48. data/lib/tivohmo/server/views/server_info.builder +7 -0
  49. data/lib/tivohmo/server/views/unsupported.erb +7 -0
  50. data/lib/tivohmo/server/views/video_formats.builder +10 -0
  51. data/lib/tivohmo/server.rb +306 -0
  52. data/lib/tivohmo/version.rb +3 -0
  53. data/lib/tivohmo.rb +5 -0
  54. data/spec/adapters/filesystem/application_spec.rb +19 -0
  55. data/spec/adapters/filesystem/file_item_spec.rb +33 -0
  56. data/spec/adapters/filesystem/folder_container_spec.rb +115 -0
  57. data/spec/adapters/plex/application_spec.rb +20 -0
  58. data/spec/adapters/plex/category_spec.rb +66 -0
  59. data/spec/adapters/plex/episode_spec.rb +22 -0
  60. data/spec/adapters/plex/metadata_spec.rb +24 -0
  61. data/spec/adapters/plex/movie_spec.rb +22 -0
  62. data/spec/adapters/plex/qualified_category_spec.rb +51 -0
  63. data/spec/adapters/plex/season_spec.rb +22 -0
  64. data/spec/adapters/plex/section_spec.rb +38 -0
  65. data/spec/adapters/plex/show_spec.rb +22 -0
  66. data/spec/adapters/plex/transcoder_spec.rb +27 -0
  67. data/spec/adapters/streamio/metadata_spec.rb +34 -0
  68. data/spec/adapters/streamio/transcoder_spec.rb +42 -0
  69. data/spec/api/application_spec.rb +63 -0
  70. data/spec/api/container_spec.rb +34 -0
  71. data/spec/api/item_spec.rb +53 -0
  72. data/spec/api/metadata_spec.rb +59 -0
  73. data/spec/api/node_spec.rb +178 -0
  74. data/spec/api/server_spec.rb +24 -0
  75. data/spec/api/transcoder_spec.rb +25 -0
  76. data/spec/beacon_spec.rb +87 -0
  77. data/spec/cli_spec.rb +227 -0
  78. data/spec/server_spec.rb +458 -0
  79. data/spec/spec_helper.rb +123 -0
  80. data/tivohmo.gemspec +46 -0
  81. metadata +416 -0
@@ -0,0 +1,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
@@ -0,0 +1,7 @@
1
+ require 'tivohmo/api/node'
2
+ require 'tivohmo/api/item'
3
+ require 'tivohmo/api/container'
4
+ require 'tivohmo/api/transcoder'
5
+ require 'tivohmo/api/metadata'
6
+ require 'tivohmo/api/application'
7
+ require 'tivohmo/api/server'