tivohmo 0.1.0

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