tivohmo 0.1.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 75d19e33d89ef251caa838694011d40532beb39d
4
- data.tar.gz: ba27d1117f29cb07106b90d152452fe93bae97cb
3
+ metadata.gz: 6513addd49cd73d0c775c59dc61f1b50c1efb335
4
+ data.tar.gz: f1e455479caff464c3bfd800ca7debd5aa2ed57c
5
5
  SHA512:
6
- metadata.gz: b0d86c363409dbc0527e802c483cfc439c6d088439a898f32c1288fb56c88fdee8e8a03ce6a51cc8a7efa30ba29e037827ac643e1c04bee08c6ebb5bc53dca78
7
- data.tar.gz: 5644691da75bed304c6e862ab4e7660372f6684663bb5d621272259fdb6cb89f00d51b264680ce4a8aa4b7fbcabd1f54d0ca9312364da1e8728818be00acdc1a
6
+ metadata.gz: 1c53ffcccd988944ebb7e95ce86a722052ec42198ca1b01d41e26ce71f5e0230652b7585244414b8ab8f9575a1fd5d0dc3e25f8fde1466ec12cb6af27f3f8627
7
+ data.tar.gz: c00d7b88d4551f34fcace28ebd0bd9b714dc8f21926ce3581e918965a77a87d67f5a61f4e1d8bde6e045117bba0044500be3736b58490d56a7f98353f23c3cb1
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ 0.2.0 (11/23/2014)
2
+ ------------------
3
+
4
+ add more metadata from plex <d048373> [Matt Conway]
5
+ add version as option and to normal startup messaging Capture stdout/err to logfile when provided <dd4bae7> [Matt Conway]
6
+ force encoding for detail xml in tivo_header handle negative ItemCount for tivo jump to end of list command <8229e22> [Matt Conway]
7
+ more function launchagent script <51e62de> [Matt Conway]
8
+ refactor plex metadata <76825e7> [Matt Conway]
9
+ unescape filename from plex <0502964> [Matt Conway]
10
+ use forked streamio-ffmpeg (due to lack of maintenance) to allow use with ffmpeg 2.4.x <c7e16e1> [Matt Conway]
11
+
1
12
  0.1.4 (10/12/2014)
2
13
  ------------------
3
14
 
@@ -3,7 +3,7 @@
3
3
  <!--
4
4
  Launchd (OS X) configuration for TivoHMO
5
5
  Copy to ~/Library/LaunchAgents and edit as desired
6
- Then to start the server, run: launchd load ~/Library/LaunchAgents/tivohmo.plist
6
+ Then to start the server, run: launchctl load ~/Library/LaunchAgents/tivohmo.plist
7
7
  -->
8
8
 
9
9
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -13,10 +13,14 @@ Then to start the server, run: launchd load ~/Library/LaunchAgents/tivohmo.plist
13
13
  <string>TivoHMO</string>
14
14
  <key>ProgramArguments</key>
15
15
  <array>
16
- <string>tivohmo</string>
17
- <string>/usr/local/pytivo/pyTivo.py</string>
16
+ <string>bash</string>
17
+ <string>-l</string>
18
+ <string>-c</string>
19
+ <string>tivohmo -p 9033 -l ~/Library/Logs/tivohmo.log -a TivoHMO::Adapters::Plex::Application -i localhost -t 'Plex Videos'</string>
18
20
  </array>
19
21
  <key>RunAtLoad</key>
20
22
  <true/>
23
+ <key>KeepAlive</key>
24
+ <true/>
21
25
  </dict>
22
26
  </plist>
@@ -19,6 +19,32 @@ module TivoHMO
19
19
  self.created_at = Time.at(delegate.added_at.to_i)
20
20
  end
21
21
 
22
+ def metadata
23
+ md = super
24
+
25
+ md.original_air_date = Time.parse(delegate.originally_available_at) rescue nil
26
+
27
+ rating_name = delegate.content_rating.upcase
28
+ rating_value = TivoHMO::API::Metadata::TV_RATINGS[rating_name]
29
+ if rating_value
30
+ md.tv_rating = {name: rating_name, value: rating_value}
31
+ end
32
+
33
+ md.is_episode = true
34
+ md.episode_number = "%i%02i" % [delegate.parent_index, delegate.index]
35
+ md.series_title = delegate.grandparent_title
36
+ md.episode_title = "%i - %s" % [md.episode_number, title]
37
+ md.title = "%s - %s" % [delegate.grandparent_title, title]
38
+
39
+ # group tv shows under same name if we can extract a seriesId
40
+ guid = delegate.guid
41
+ if guid =~ /thetvdb:\/\/(\d+)/
42
+ md.series_id = "SH#{$1}"
43
+ end
44
+
45
+ md
46
+ end
47
+
22
48
  end
23
49
 
24
50
  end
@@ -12,6 +12,13 @@ module TivoHMO
12
12
  begin
13
13
  self.description = item.delegate.summary
14
14
  self.duration = (item.delegate.duration.to_i / 1000).to_i
15
+
16
+ # plex 0-10 => tivo 1-7 for value, 0-4 in .5 increments for name
17
+ plex_rating = item.delegate.rating.to_f
18
+ rating_value = (plex_rating / 10 * 6).round
19
+ rating_name = [1, 1.5, 2, 2.5, 3, 3.5, 4][rating_value]
20
+ self.star_rating = {name: rating_name, value: rating_value + 1}
21
+
15
22
  rescue => e
16
23
  logger.error "Failed to read plex metadata: #{e}"
17
24
  end
@@ -19,6 +19,19 @@ module TivoHMO
19
19
  self.created_at = Time.at(delegate.added_at.to_i)
20
20
  end
21
21
 
22
+ def metadata
23
+ md = super
24
+ md.movie_year = Time.parse(delegate.originally_available_at).year rescue nil
25
+
26
+ rating_name = delegate.content_rating.upcase
27
+ rating_value = TivoHMO::API::Metadata::MPAA_RATINGS[rating_name]
28
+ if rating_value
29
+ md.mpaa_rating = {name: rating_name, value: rating_value}
30
+ end
31
+
32
+ md
33
+ end
34
+
22
35
  end
23
36
 
24
37
  end
@@ -9,7 +9,7 @@ module TivoHMO
9
9
 
10
10
  def initialize(item)
11
11
  super(item)
12
- self.source_filename = item.delegate.medias.first.parts.first.file
12
+ self.source_filename = CGI.unescape(item.delegate.medias.first.parts.first.file)
13
13
  end
14
14
 
15
15
  end
@@ -35,9 +35,9 @@ module TivoHMO
35
35
 
36
36
  def transcoder_options(format="video/x-tivo-mpeg")
37
37
  opts = {
38
- video_max_bitrate: 30000,
38
+ video_max_bitrate: 30_000_000,
39
39
  buffer_size: 4096,
40
- audio_bitrate: 448,
40
+ audio_bitrate: 448_000,
41
41
  format: format,
42
42
  custom: []
43
43
  }
@@ -47,6 +47,7 @@ module TivoHMO
47
47
  opts = select_video_codec(opts)
48
48
  opts = select_video_bitrate(opts)
49
49
  opts = select_audio_codec(opts)
50
+ opts = select_audio_bitrate(opts)
50
51
  opts = select_audio_sample_rate(opts)
51
52
  opts = select_container(opts)
52
53
 
@@ -96,6 +97,13 @@ module TivoHMO
96
97
  opts
97
98
  end
98
99
 
100
+ def select_audio_bitrate(opts)
101
+ # transcode assumes unit of Kbit, whilst video_info has unit of bit
102
+ opts[:audio_bitrate] = (opts[:audio_bitrate] / 1000).to_i
103
+
104
+ opts
105
+ end
106
+
99
107
  def select_audio_codec(opts)
100
108
  if video_info[:audio_codec]
101
109
  if AUDIO_CODECS.any? { |ac| video_info[:audio_codec] =~ /#{ac}/ }
@@ -111,18 +119,24 @@ module TivoHMO
111
119
  end
112
120
 
113
121
  def select_video_bitrate(opts)
114
-
115
122
  vbr = video_info[:video_bitrate]
123
+ default_vbr = 16_384_000
116
124
 
117
125
  if vbr && vbr > 0
118
126
  if vbr >= opts[:video_max_bitrate]
119
- opts[:video_bitrate] = (video_info[:video_max_bitrate] * 0.95).to_i
120
- else
127
+ opts[:video_bitrate] = (opts[:video_max_bitrate] * 0.95).to_i
128
+ elsif vbr > default_vbr
121
129
  opts[:video_bitrate] = vbr
130
+ else
131
+ opts[:video_bitrate] = default_vbr
122
132
  end
123
133
  end
124
134
 
125
- opts[:video_bitrate] ||= 16384
135
+ opts[:video_bitrate] ||= default_vbr
136
+
137
+ # transcode assumes unit of Kbit, whilst video_info has unit of bit
138
+ opts[:video_bitrate] = (opts[:video_bitrate] / 1000).to_i
139
+ opts[:video_max_bitrate] = (opts[:video_max_bitrate] / 1000).to_i
126
140
 
127
141
  opts
128
142
  end
@@ -141,7 +155,6 @@ module TivoHMO
141
155
  end
142
156
 
143
157
  def select_video_dimensions(opts)
144
- preserve_aspect = nil
145
158
  video_width = video_info[:width].to_i
146
159
  VIDEO_WIDTHS.each do |w|
147
160
  w = w.to_i
@@ -163,34 +176,37 @@ module TivoHMO
163
176
  end
164
177
  end
165
178
  video_height = VIDEO_HEIGHTS.last.to_i unless video_height
179
+
166
180
  opts[:resolution] = "#{video_width}x#{video_height}"
167
- opts[:preserve_aspect_ratio] = :height unless opts[:preserve_aspect_ratio]
181
+ opts[:preserve_aspect_ratio] ||= :height
168
182
  opts
169
183
  end
170
184
 
171
185
  def select_video_frame_rate(opts)
172
186
 
173
- frame_rate = video_info[:frame_rate].to_f
174
- VIDEO_FRAME_RATES.each do |r|
175
- if frame_rate >= r.to_f
176
- opts[:frame_rate] = r
177
- break
178
- end
187
+ frame_rate = video_info[:frame_rate]
188
+ if frame_rate =~ /\A[0-9\.]+\Z/
189
+ frame_rate = frame_rate.to_f
190
+ elsif frame_rate =~ /\A\((\d+)\/(\d+)\)\Z/
191
+ frame_rate = $1.to_f / $2.to_f
179
192
  end
180
193
 
181
- opts[:frame_rate] ||= 29.97
194
+ VIDEO_FRAME_RATES.each do |r|
195
+ opts[:frame_rate] = r
196
+ break if frame_rate >= r.to_f
197
+ end
182
198
 
183
199
  opts
184
200
  end
185
201
 
186
202
  def run_transcode(output_filename, format)
187
203
 
188
- logger.debug "Movie Info: " +
204
+ logger.info "Movie Info: " +
189
205
  video_info.collect {|k, v| "#{k}=#{v.inspect}"}.join(' ')
190
206
 
191
207
  opts = transcoder_options(format)
192
208
 
193
- logger.debug "Transcoding options: " +
209
+ logger.info "Transcoding options: " +
194
210
  opts.collect {|k, v| "#{k}='#{v}'"}.join(' ')
195
211
 
196
212
 
@@ -6,6 +6,21 @@ module TivoHMO
6
6
  extend ActiveSupport::Concern
7
7
  include GemLogger::LoggerSupport
8
8
 
9
+ MPAA_RATINGS = {
10
+ 'G' => 1, 'PG' => 2, 'PG-13' => 3, 'PG13' => 3, 'R' => 4, 'X' => 5,
11
+ 'NC-17' => 6, 'NC17' => 6, 'NR' => 8, 'UNRATED' => 8, 'G1' => 1,
12
+ 'P2' => 2, 'P3' => 3, 'R4' => 4, 'X5' => 5, 'N6' => 6, 'N8' => 8
13
+ }
14
+
15
+ TV_RATINGS = {
16
+ 'TV-Y7' => 1, 'TV-Y' => 2, 'TV-G' => 3, 'TV-PG' => 4, 'TV-14' => 5,
17
+ 'TV-MA' => 6, 'TV-NR' => 7, 'TVY7' => 1, 'TVY' => 2, 'TVG' => 3,
18
+ 'TVPG' => 4, 'TV14' => 5, 'TVMA' => 6, 'TVNR' => 7, 'Y7' => 1,
19
+ 'Y' => 2, 'G' => 3, 'PG' => 4, '14' => 5, 'MA' => 6, 'NR' => 7,
20
+ 'UNRATED' => 7, 'X1' => 1, 'X2' => 2, 'X3' => 3, 'X4' => 4, 'X5' => 5,
21
+ 'X6' => 6, 'X7' => 7
22
+ }
23
+
9
24
  attr_accessor :item,
10
25
 
11
26
  :title,
@@ -42,6 +42,10 @@ module TivoHMO
42
42
  :flag, "debug output\n",
43
43
  default: false
44
44
 
45
+ option ["-v", "--version"],
46
+ :flag, "print version and exit\n",
47
+ default: false
48
+
45
49
  option ["-r", "--preload"],
46
50
  :flag, "Preloads all lazy container listings\n",
47
51
  default: false
@@ -83,8 +87,15 @@ module TivoHMO
83
87
  "LIMIT:INTERVAL", "configure beacon limit and/or interval\n"
84
88
 
85
89
  def execute
90
+ if version?
91
+ puts "TivoHMO Version #{TivoHMO::VERSION}"
92
+ return
93
+ end
94
+
86
95
  setup_logging
87
96
 
97
+ logger.info "TivoHMO #{TivoHMO::VERSION} starting up"
98
+
88
99
  if configuration
89
100
  config = YAML.load_file(configuration)
90
101
 
@@ -146,12 +157,21 @@ module TivoHMO
146
157
  Logging.logger.root.level = :debug if debug?
147
158
 
148
159
  if logfile.present?
149
- Logging.logger.root.appenders = Logging.appenders.file(
160
+ appender = Logging.appenders.rolling_file(
150
161
  logfile,
162
+ truncate: true,
163
+ age: 'daily',
164
+ keep: 3,
151
165
  layout: Logging.layouts.pattern(
152
166
  pattern: Logging.appenders.stdout.layout.pattern
153
167
  )
154
168
  )
169
+
170
+ # hack to assign stdout/err to logfile if logging to file
171
+ io = appender.instance_variable_get(:@io)
172
+ $stdout = $stderr = io
173
+
174
+ Logging.logger.root.appenders = appender
155
175
  end
156
176
  end
157
177
 
@@ -147,6 +147,8 @@ module TivoHMO
147
147
  end
148
148
 
149
149
  item_details_xml = builder :item_details, layout: true, locals: {item: item}
150
+ item_details_xml.force_encoding('ascii-8bit')
151
+
150
152
  ld = item_details_xml.bytesize
151
153
  chunk = item_details_xml + '\0' * (pad(ld, 4) + 4)
152
154
  lc = chunk.bytesize
@@ -233,6 +235,11 @@ module TivoHMO
233
235
  else
234
236
  logger.warn "Anchor not found: #{anchor_item}"
235
237
  end
238
+ else
239
+ if item_count < 0
240
+ locals[:item_start] = children.size + item_count
241
+ locals[:item_count] = - item_count
242
+ end
236
243
  end
237
244
 
238
245
  if container.root?
@@ -1,3 +1,3 @@
1
1
  module TivoHMO
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -3,7 +3,13 @@ require 'tivohmo/adapters/plex'
3
3
 
4
4
  describe TivoHMO::Adapters::Plex::Episode do
5
5
 
6
- let(:plex_delegate) { plex_stub(::Plex::Episode) }
6
+ let(:plex_delegate) { plex_stub(::Plex::Episode,
7
+ content_rating: 'G',
8
+ index: 2,
9
+ parent_index: 1,
10
+ grandparent_title: 'ShowTitle',
11
+ guid: "com.plexapp.agents.thetvdb://269650/1/2?lang=en",
12
+ originally_available_at: "2014-06-04") }
7
13
 
8
14
  describe "#initialize" do
9
15
 
@@ -19,4 +25,22 @@ describe TivoHMO::Adapters::Plex::Episode do
19
25
 
20
26
  end
21
27
 
28
+ describe "#metadata" do
29
+
30
+ it "should populate metadata" do
31
+ episode = described_class.new(plex_delegate)
32
+ episode.app = TivoHMO::Adapters::Plex::Application.new('localhost')
33
+ md = episode.metadata
34
+ expect(md.original_air_date).to eq(Time.parse(plex_delegate.originally_available_at))
35
+ expect(md.tv_rating).to eq({name: 'G', value: 3})
36
+ expect(md.is_episode).to eq(true)
37
+ expect(md.episode_number).to eq("102")
38
+ expect(md.series_title).to eq("ShowTitle")
39
+ expect(md.episode_title).to eq("102 - Title")
40
+ expect(md.title).to eq("ShowTitle - Title")
41
+ expect(md.series_id).to eq("SH269650")
42
+ end
43
+
44
+ end
45
+
22
46
  end
@@ -5,7 +5,8 @@ describe TivoHMO::Adapters::Plex::Metadata do
5
5
 
6
6
  let(:plex_delegate) { plex_stub(::Plex::Movie,
7
7
  summary: 'Summary',
8
- duration: 10000) }
8
+ duration: 10000,
9
+ rating: 10) }
9
10
 
10
11
  let(:item) { double('item', delegate: plex_delegate)}
11
12
 
@@ -17,6 +18,7 @@ describe TivoHMO::Adapters::Plex::Metadata do
17
18
  expect(md).to be_a TivoHMO::API::Metadata
18
19
  expect(md.duration).to eq(plex_delegate.duration / 1000)
19
20
  expect(md.description).to eq plex_delegate.summary
21
+ expect(md.star_rating).to eq({name: 4, value: 7})
20
22
  end
21
23
 
22
24
  end
@@ -3,7 +3,9 @@ require 'tivohmo/adapters/plex'
3
3
 
4
4
  describe TivoHMO::Adapters::Plex::Movie do
5
5
 
6
- let(:plex_delegate) { plex_stub(::Plex::Movie) }
6
+ let(:plex_delegate) { plex_stub(::Plex::Movie,
7
+ originally_available_at: "2013-01-02",
8
+ content_rating: 'G') }
7
9
 
8
10
  describe "#initialize" do
9
11
 
@@ -19,4 +21,16 @@ describe TivoHMO::Adapters::Plex::Movie do
19
21
 
20
22
  end
21
23
 
24
+ describe "#metadata" do
25
+
26
+ it "should populate metadata" do
27
+ movie = described_class.new(plex_delegate)
28
+ movie.app = TivoHMO::Adapters::Plex::Application.new('localhost')
29
+ md = movie.metadata
30
+ expect(md.movie_year).to eq(2013)
31
+ expect(md.mpaa_rating).to eq({name: 'G', value: 1})
32
+ end
33
+
34
+ end
35
+
22
36
  end
@@ -9,7 +9,7 @@ describe TivoHMO::Adapters::Plex::Transcoder do
9
9
  duration: 10000,
10
10
  medias: [double('media',
11
11
  parts: [double('part',
12
- file: '/foo')])])}
12
+ file: '/foo%20bar')])])}
13
13
 
14
14
  let(:item) { double('item', identifier: plex_delegate.key, delegate: plex_delegate)}
15
15
 
@@ -19,7 +19,7 @@ describe TivoHMO::Adapters::Plex::Transcoder do
19
19
  trans = described_class.new(item)
20
20
  expect(trans).to be_a described_class
21
21
  expect(trans).to be_a TivoHMO::API::Transcoder
22
- expect(trans.source_filename).to eq '/foo'
22
+ expect(trans.source_filename).to eq '/foo bar'
23
23
  end
24
24
 
25
25
  end
@@ -39,6 +39,16 @@ describe TivoHMO::CLI do
39
39
 
40
40
  end
41
41
 
42
+ describe "--version" do
43
+
44
+ it "produces version text under standard width" do
45
+ expect(cli).to receive(:setup_logging).never
46
+ expect($stdout).to receive(:puts).with(/#{TivoHMO::VERSION}/)
47
+ cli.run(["--version"])
48
+ end
49
+
50
+ end
51
+
42
52
  describe "--debug" do
43
53
 
44
54
  it "defaults to info log level" do
@@ -385,6 +385,22 @@ describe TivoHMO::Server do
385
385
  expect(child_titles).to match_array(["i1", "i2", "i3"])
386
386
  end
387
387
 
388
+ it "honors negative ItemCount (jump to last page)" do
389
+ get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&ItemCount=-3"
390
+ expect(last_response.status).to eq(200)
391
+ doc = Nokogiri::XML(last_response.body)
392
+
393
+ expect(doc.at_xpath("/TiVoContainer/ItemStart").content).to eq("17")
394
+ expect(doc.at_xpath("/TiVoContainer/ItemCount").content).to eq("3")
395
+
396
+ expect(doc.at_xpath("/TiVoContainer/Details/TotalItems").content).to eq("20")
397
+
398
+ expect(doc.xpath("/TiVoContainer/Item").size).to eq (3)
399
+
400
+ child_titles = doc.xpath("/TiVoContainer/Item/Details/Title").collect(&:content)
401
+ expect(child_titles).to match_array(["i18", "i19", "i20"])
402
+ end
403
+
388
404
  it "displays page 2" do
389
405
  get "/TiVoConnect?Command=QueryContainer&Container=/a1/c1&ItemStart=7"
390
406
  expect(last_response.status).to eq(200)
@@ -109,6 +109,9 @@ def plex_stub(clazz, method_stubs={})
109
109
  default_stubs = {
110
110
  key: '/some/key',
111
111
  title: 'Title',
112
+ summary: 'Summary',
113
+ duration: 100,
114
+ rating: 1,
112
115
  updated_at: Time.now.to_i,
113
116
  added_at: Time.now.to_i,
114
117
  refresh: nil
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
 
40
40
  # filesystem adapter dependencies, make optional?
41
41
  spec.add_dependency "listen"
42
- spec.add_dependency "streamio-ffmpeg"
42
+ spec.add_dependency "tivohmo-streamio-ffmpeg"
43
43
 
44
44
  # plex adapter dependencies, make optional?
45
45
  spec.add_dependency "plex-ruby"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tivohmo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Conway
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-12 00:00:00.000000000 Z
11
+ date: 2014-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -249,7 +249,7 @@ dependencies:
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
251
  - !ruby/object:Gem::Dependency
252
- name: streamio-ffmpeg
252
+ name: tivohmo-streamio-ffmpeg
253
253
  requirement: !ruby/object:Gem::Requirement
254
254
  requirements:
255
255
  - - ">="
@@ -415,3 +415,4 @@ test_files:
415
415
  - spec/cli_spec.rb
416
416
  - spec/server_spec.rb
417
417
  - spec/spec_helper.rb
418
+ has_rdoc: