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,69 @@
1
+ require 'socket'
2
+
3
+ module TivoHMO
4
+
5
+ # Provides a mechanism for broadcasting the presence of a
6
+ # TivoHMO::Server to Tivo dvrs on the local network
7
+ class Beacon
8
+ include GemLogger::LoggerSupport
9
+
10
+ def initialize(service_port, limit: -1, interval: 60)
11
+ @interval = interval
12
+ @limit = limit
13
+ @uid = SecureRandom.uuid
14
+ @socket = UDPSocket.new(Socket::AF_INET)
15
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
16
+ @services = ['TiVoMediaServer:%s/http' % service_port]
17
+ @running = false
18
+ end
19
+
20
+ def start
21
+ if ! @running
22
+ logger.info "Starting beacon(limit=#@limit, interval=#@interval) for #{@services.inspect}"
23
+ @running = true
24
+ @broadcast_thread = Thread.new do
25
+ while @running
26
+ broadcast
27
+ sleep(@interval)
28
+ @limit = @limit - 1
29
+ break if @limit == 0
30
+ end
31
+ @running = false
32
+ logger.info "Beacon thread exiting"
33
+ end
34
+ end
35
+ end
36
+
37
+ def stop
38
+ logger.info "Stopping beacon"
39
+ @running = false
40
+ end
41
+
42
+ def join
43
+ @broadcast_thread.join
44
+ end
45
+
46
+ def beacon_data(method)
47
+ payload = {
48
+ tivoconnect: 1,
49
+ method: method,
50
+ identity: "{#{@uid}}",
51
+ machine: Socket.gethostname,
52
+ platform: 'pc/tivohmo',
53
+ services: @services.join(';'),
54
+ swversion: TivoHMO::VERSION
55
+ }
56
+ data = payload.collect {|k,v| "#{k}=#{v}" }.join("\n") << "\n"
57
+ data
58
+ end
59
+
60
+ def broadcast
61
+ bcast_ip = '<broadcast>' #'255.255.255.255'
62
+ bcast_port = 2190
63
+ packet = beacon_data('broadcast')
64
+ logger.debug "Sending beacon packet: #{packet}"
65
+ @socket.send(packet, 0, bcast_ip, bcast_port)
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,182 @@
1
+ require 'clamp'
2
+ require 'yaml'
3
+ require 'active_support/core_ext/string'
4
+ require 'sigdump/setup'
5
+ require 'tivohmo'
6
+ require 'open-uri'
7
+
8
+ module TivoHMO
9
+
10
+ # The command line interface to tivohmo
11
+ class CLI < Clamp::Command
12
+ include GemLogger::LoggerSupport
13
+
14
+ def self.description
15
+ desc = <<-DESC
16
+ Runs a HMO server. Specify one or more applications to show up as top level
17
+ shares in the TiVo Now Playing view. The application, identifier,
18
+ transcoder, metadata options can be given in groups to apply the transcoder
19
+ and metadata to each application - uses the application's default if not given
20
+
21
+ e.g.
22
+
23
+ tivohmo -a TivoHMO::Adapters::Filesystem::Application -i ~/Video/Movies \\
24
+ -a TivoHMO::Adapters::Filesystem::Application -i ~/Video/TV
25
+
26
+ to run two top level filesystem video serving apps for different dirs,
27
+ or
28
+
29
+ tivohmo -a TivoHMO::Adapters::Filesystem::Application -t Vids -i ~/Video
30
+
31
+ to run the single filesystem app with a custom title
32
+ DESC
33
+ desc.split("\n").collect(&:strip).join("\n")
34
+ end
35
+
36
+ option ["-d", "--debug"],
37
+ :flag, "debug output\n",
38
+ default: false
39
+
40
+ option ["-r", "--preload"],
41
+ :flag, "Preloads all lazy container listings\n",
42
+ default: false
43
+
44
+ option ["-l", "--logfile"],
45
+ "FILE", "log to given file\n"
46
+
47
+ option ["-p", "--port"],
48
+ "PORT", "run server using PORT\n",
49
+ default: 9032 do |s|
50
+ Integer(s)
51
+ end
52
+
53
+ option ["-f", "--configuration"],
54
+ "FILE", "load configuration from given filename\n"
55
+
56
+ option ["-a", "--application"],
57
+ "CLASSNAME", "use the given application class\n",
58
+ multivalued: true
59
+
60
+ option ["-i", "--identifier"],
61
+ "IDENTIFIER", "use the given application identifier\n" +
62
+ "a string that has meaning to the application\n",
63
+ multivalued: true
64
+
65
+ option ["-t", "--title"],
66
+ "TITLE", "use the given title for the application\n",
67
+ multivalued: true
68
+
69
+ option ["-T", "--transcoder"],
70
+ "CLASSNAME", "override the application's transcoder class\n",
71
+ multivalued: true
72
+
73
+ option ["-M", "--metadata"],
74
+ "CLASSNAME", "override the application's metadata class\n",
75
+ multivalued: true
76
+
77
+ option ["-b", "--beacon"],
78
+ "LIMIT:INTERVAL", "configure beacon limit and/or interval\n"
79
+
80
+ def execute
81
+ setup_logging
82
+
83
+ if configuration
84
+ config = YAML.load_file(configuration)
85
+
86
+ # allow cli option to override config file
87
+ set_if_default(:port, config['port'].to_i)
88
+ end
89
+
90
+ signal_usage_error "at least one application is required" unless application_list.present?
91
+ signal_usage_error "an initializer is needed for each application" unless
92
+ application_list.size == identifier_list.size
93
+
94
+ (application_list + transcoder_list + metadata_list).each do |c|
95
+ if c && c.starts_with?('TivoHMO::Adapters::')
96
+ path = c.downcase.split('::')[0..-2].join('/')
97
+ require path
98
+ end
99
+ end
100
+
101
+ server = TivoHMO::API::Server.new
102
+
103
+ apps_with_config = application_list.zip(identifier_list,
104
+ title_list,
105
+ transcoder_list,
106
+ metadata_list)
107
+
108
+ apps_with_config.each do |app_classname, identifier, title, transcoder, metadata|
109
+ app_class = app_classname.constantize
110
+ app = app_class.new(identifier)
111
+
112
+ if title
113
+ app.title = title
114
+ else
115
+ app.title = "#{app.title} on #{server.title}"
116
+ end
117
+
118
+ app.transcoder_class = transcoder.constantize if transcoder
119
+ app.metadata_class = metadata.constantize if metadata
120
+ server.add_child(app)
121
+ end
122
+
123
+ preload_containers(server) if preload?
124
+
125
+ opts = {}
126
+ if beacon.present?
127
+ limit, interval = beacon.split(":")
128
+ opts[:limit] = limit.to_i if limit.present?
129
+ opts[:interval] = interval.to_i if interval.present?
130
+ end
131
+ notifier = TivoHMO::Beacon.new(port, **opts)
132
+
133
+ TivoHMO::Server.start(server, port) do |s|
134
+ wait_for_server { notifier.start }
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def setup_logging
141
+ Logging.logger.root.level = :debug if debug?
142
+
143
+ if logfile.present?
144
+ Logging.logger.root.appenders = Logging.appenders.file(
145
+ logfile,
146
+ layout: Logging.layouts.pattern(
147
+ pattern: Logging.appenders.stdout.layout.pattern
148
+ )
149
+ )
150
+ end
151
+ end
152
+
153
+ def set_if_default(attr, new_value)
154
+ self.send("#{attr}=", new_value) if self.send(attr) == self.send("default_#{attr}")
155
+ end
156
+
157
+ def preload_containers(server)
158
+ logger.info "Preloading lazily cached containers"
159
+ queue = server.children.dup
160
+ queue.each do |i|
161
+ logger.debug("Loading children for #{i.title_path}")
162
+ queue.concat(i.children)
163
+ end
164
+ logger.info "Preload complete"
165
+ end
166
+
167
+ def wait_for_server
168
+ Thread.new do
169
+ while true
170
+ begin
171
+ open("http://localhost:#{port}/TiVoConnect?Command=QueryServer") {}
172
+ yield
173
+ break
174
+ rescue Exception => e
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ end
181
+
182
+ end
@@ -0,0 +1,50 @@
1
+ require 'gem_logger'
2
+ require 'logging'
3
+ require 'active_support/concern'
4
+
5
+ Logging.format_as :inspect
6
+ Logging.backtrace true
7
+
8
+ Logging.color_scheme(
9
+ 'bright',
10
+ levels: {
11
+ info: :green,
12
+ warn: :yellow,
13
+ error: :red,
14
+ fatal: [:white, :on_red]
15
+ },
16
+ date: :blue,
17
+ logger: :cyan,
18
+ message: :magenta
19
+ )
20
+
21
+ Logging.appenders.stdout(
22
+ 'stdout',
23
+ layout: Logging.layouts.pattern(
24
+ pattern: '[%d] %-5l %c{2} %m\n',
25
+ color_scheme: 'bright'
26
+ )
27
+ )
28
+
29
+ Logging.logger.root.appenders = Logging.appenders.stdout
30
+ Logging.logger.root.level = :info
31
+
32
+ module TivoHMO
33
+ module LoggingConcern
34
+ extend ActiveSupport::Concern
35
+
36
+ def logger
37
+ Logging.logger[self.class]
38
+ end
39
+
40
+ module ClassMethods
41
+ def logger
42
+ Logging.logger[self]
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+ GemLogger.default_logger = Logging.logger.root
50
+ GemLogger.logger_concern = TivoHMO::LoggingConcern
@@ -0,0 +1,19 @@
1
+ xml.Item do
2
+
3
+ xml.Details do
4
+ xml.Title container.title
5
+ xml.ContentType container.content_type
6
+ xml.SourceFormat container.source_format
7
+ xml.UniqueId format_uuid(container.uuid)
8
+ xml.TotalItems container.children.size
9
+ xml.LastCaptureDate format_date(container.created_at) if container.created_at
10
+ end
11
+
12
+ xml.Links do
13
+ xml.Content do
14
+ xml.Url container_url(container)
15
+ xml.ContentType "x-tivo-container/folder"
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,57 @@
1
+ xml.Item do
2
+
3
+ md = item.metadata
4
+
5
+ xml.Details do
6
+ xml.Title item.title
7
+ xml.ContentType item.content_type
8
+ xml.SourceFormat item.source_format
9
+
10
+ if md
11
+ xml.SourceSize md.source_size if md.source_size
12
+ xml.Duration md.duration.to_i if md.duration
13
+ xml.Description md.description if md.description
14
+
15
+ xml.SourceChannel md.channel[:major_number] if md.channel
16
+ xml.SourceStation md.channel[:callsign] if md.channel
17
+
18
+ xml.SeriesId md.series_id
19
+ xml.ShowingBits md.showing_bits if md.showing_bits
20
+ # xml.CopyProtected 'Yes' if md.valid?
21
+
22
+ xml.EpisodeTitle md.episode_title if md.is_episode && md.episode_title
23
+ xml.EpisodeNumber md.episode_number if md.episode_number
24
+
25
+ xml.ProgramId md.program_id if md.program_id
26
+
27
+ xml.TvRating md.tv_rating[:name] if md.tv_rating
28
+ xml.MpaaRating md.mpaa_rating[:name] if md.mpaa_rating
29
+ end
30
+
31
+ xml.CaptureDate format_date(item.created_at) if item.created_at
32
+
33
+ end
34
+
35
+ xml.Links do
36
+ xml.Content do
37
+ xml.ContentType item.content_type
38
+ xml.Url item_url(item)
39
+ end
40
+
41
+ xml.CustomIcon do
42
+ xml.ContentType "image/*"
43
+ xml.AcceptsParams "No"
44
+ xml.Url "urn:tivo:image:save-until-i-delete-recording"
45
+ end
46
+
47
+ if md
48
+ xml.TiVoVideoDetails do
49
+ xml.ContentType "text/xml"
50
+ xml.AcceptsParams "No"
51
+ xml.Url item_detail_url(item)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,26 @@
1
+ xml.TiVoContainer do
2
+
3
+ paginated_children = children[item_start, item_count]
4
+
5
+ xml.ItemStart item_start
6
+ xml.ItemCount paginated_children.size
7
+
8
+ xml.Details do
9
+ xml.Title container.title_path
10
+ xml.ContentType "x-tivo-container/folder"
11
+ xml.SourceFormat "x-tivo-container/folder"
12
+ xml.TotalItems children.size
13
+ xml.UniqueId format_uuid(container.uuid)
14
+ end
15
+
16
+ paginated_children.each do |child|
17
+ if child.is_a?(TivoHMO::API::Container)
18
+ builder :_container, layout: false, locals: { xml: xml, container: child }
19
+ elsif child.is_a?(TivoHMO::API::Item)
20
+ builder :_item, layout: false, locals: { xml: xml, item: child }
21
+ else
22
+ raise "Invalid child, needs to be item or container"
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,153 @@
1
+ xml.TvBusMarshalledStruct :TvBusEnvelope,
2
+ 'xmlns:xs' => "http://www.w3.org/2001/XMLSchema-instance",
3
+ 'xmlns:TvBusMarshalledStruct' => "http://tivo.com/developer/xml/idl/TvBusMarshalledStruct",
4
+ 'xmlns:TvPgdRecording' => "http://tivo.com/developer/xml/idl/TvPgdRecording",
5
+ 'xmlns:TvBusDuration' => "http://tivo.com/developer/xml/idl/TvBusDuration",
6
+ 'xmlns:TvPgdShowing' => "http://tivo.com/developer/xml/idl/TvPgdShowing",
7
+ 'xmlns:TvDbShowingBit' => "http://tivo.com/developer/xml/idl/TvDbShowingBit",
8
+ 'xmlns:TvBusDateTime' => "http://tivo.com/developer/xml/idl/TvBusDateTime",
9
+ 'xmlns:TvPgdProgram' => "http://tivo.com/developer/xml/idl/TvPgdProgram",
10
+ 'xmlns:TvDbColorCode' => "http://tivo.com/developer/xml/idl/TvDbColorCode",
11
+ 'xmlns:TvPgdSeries' => "http://tivo.com/developer/xml/idl/TvPgdSeries",
12
+ 'xmlns:TvDbShowType' => "http://tivo.com/developer/xml/idl/TvDbShowType",
13
+ 'xmlns:TvPgdChannel' => "http://tivo.com/developer/xml/idl/TvPgdChannel",
14
+ 'xmlns:TvDbTvRating' => "http://tivo.com/developer/xml/idl/TvDbTvRating",
15
+ 'xmlns:TvDbRecordQuality' => "http://tivo.com/developer/xml/idl/TvDbRecordQuality",
16
+ 'xmlns:TvDbBitstreamFormat' => "http://tivo.com/developer/xml/idl/TvDbBitstreamFormat",
17
+ 'xs:schemaLocation' => "http://tivo.com/developer/xml/idl/TvBusMarshalledStruct TvBusMarshalledStruct.xsd http://tivo.com/developer/xml/idl/TvPgdRecording TvPgdRecording.xsd http://tivo.com/developer/xml/idl/TvBusDuration TvBusDuration.xsd http://tivo.com/developer/xml/idl/TvPgdShowing TvPgdShowing.xsd http://tivo.com/developer/xml/idl/TvDbShowingBit TvDbShowingBit.xsd http://tivo.com/developer/xml/idl/TvBusDateTime TvBusDateTime.xsd http://tivo.com/developer/xml/idl/TvPgdProgram TvPgdProgram.xsd http://tivo.com/developer/xml/idl/TvDbColorCode TvDbColorCode.xsd http://tivo.com/developer/xml/idl/TvPgdSeries TvPgdSeries.xsd http://tivo.com/developer/xml/idl/TvDbShowType TvDbShowType.xsd http://tivo.com/developer/xml/idl/TvPgdChannel TvPgdChannel.xsd http://tivo.com/developer/xml/idl/TvDbTvRating TvDbTvRating.xsd http://tivo.com/developer/xml/idl/TvDbRecordQuality TvDbRecordQuality.xsd http://tivo.com/developer/xml/idl/TvDbBitstreamFormat TvDbBitstreamFormat.xsd",
18
+ 'xs:type' => "TvPgdRecording:TvPgdRecording" do
19
+
20
+ md = item.metadata
21
+
22
+ xml.recordedDuration format_iso_duration(md.duration)
23
+ xml.vActualShowing md.actual_showing
24
+ xml.vBookmark md.bookmark
25
+ xml.recordingQuality md.recording_quality[:name], :value => md.recording_quality[:value]
26
+
27
+ xml.showing do
28
+
29
+ xml.showingBits :value => md.showing_bits
30
+ xml.time format_iso_date(md.time)
31
+ xml.duration format_iso_duration(md.duration)
32
+ if md.part_count and md.part_index
33
+ xml.partCount md.part_count
34
+ xml.partIndex md.part_index
35
+ end
36
+
37
+ xml.program do
38
+
39
+ xml.vActor do
40
+ Array(md.actors).each do |actor|
41
+ xml.element actor
42
+ end
43
+ end
44
+
45
+ xml.vAdvisory md.advisory
46
+
47
+ xml.vChoreographer do
48
+ Array(md.choreographers).each do |choreographer|
49
+ xml.element choreographer
50
+ end
51
+ end
52
+
53
+ xml.colorCode md.color_code.try(:[], :name),
54
+ :value => md.color_code.try(:[], :value)
55
+ xml.description md.description
56
+
57
+ xml.vDirector do
58
+ Array(md.directors).each do |director|
59
+ xml.element director
60
+ end
61
+ end
62
+
63
+ xml.episodeNumber md.episode_number if md.episode_number
64
+ xml.episodeTitle md.episode_title if md.is_episode && md.episode_title
65
+
66
+ xml.vExecProducer do
67
+ Array(md.executive_producers).each do |executive_producer|
68
+ xml.element executive_producer
69
+ end
70
+ end
71
+
72
+ xml.vProgramGenre do
73
+ Array(md.program_genres).each do |program_genre|
74
+ xml.element program_genre
75
+ end
76
+ end
77
+
78
+ xml.vGuestStar do
79
+ Array(md.guest_stars).each do |guest_star|
80
+ xml.element guest_star
81
+ end
82
+ end
83
+
84
+ xml.vHost do
85
+ Array(md.hosts).each do |host|
86
+ xml.element host
87
+ end
88
+ end
89
+
90
+ xml.isEpisode md.is_episode
91
+
92
+ if md.movie_year
93
+ xml.movieYear md.movie_year
94
+ else
95
+ xml.originalAirDate format_iso_date(md.original_air_date || md.time)
96
+ end
97
+
98
+ if md.mpaa_rating
99
+ xml.mpaaRating md.mpaa_rating[:name], :value => md.mpaa_rating[:value]
100
+ end
101
+
102
+ xml.vProducer do
103
+ Array(md.producers).each do |producer|
104
+ xml.element producer
105
+ end
106
+ end
107
+
108
+ xml.series do
109
+ xml.isEpisodic md.is_episode
110
+ xml.vSeriesGenre do
111
+ Array(md.series_genres).each do |series_genre|
112
+ xml.element series_genre
113
+ end
114
+ end
115
+ xml.seriesTitle md.series_title
116
+ xml.uniqueId md.series_id if md.series_id
117
+ end
118
+
119
+ xml.showType md.show_type.try(:[], :name),
120
+ :value => md.show_type.try(:[], :value)
121
+
122
+ if md.star_rating
123
+ xml.starRating md.star_rating[:name], :value => md.star_rating[:value]
124
+ end
125
+
126
+ xml.title md.series_title ? md.series_title : (md.title || item.title)
127
+
128
+ xml.vWriter do
129
+ Array(md.writers).each do |writer|
130
+ xml.element writer
131
+ end
132
+ end
133
+
134
+ xml.uniqueId md.program_id if md.program_id
135
+
136
+ end
137
+
138
+ xml.channel do
139
+ xml.displayMajorNumber md.channel.try(:[], :major_number)
140
+ xml.displayMinorNumber md.channel.try(:[], :minor_number)
141
+ xml.callsign md.channel.try(:[], :callsign)
142
+ end
143
+
144
+ if md.tv_rating
145
+ xml.tvRating md.tv_rating[:name], :value => md.tv_rating[:value]
146
+ end
147
+
148
+ end
149
+
150
+ xml.startTime format_iso_date(md.start_time)
151
+ xml.stopTime format_iso_date(md.stop_time)
152
+
153
+ end
@@ -0,0 +1,2 @@
1
+ xml.instruct! :xml, :version => '1.0'
2
+ xml << yield
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2
+ <html>
3
+ <head>
4
+ <title>TivoHMO</title>
5
+ <link rel="stylesheet" type="text/css" href="/main.css">
6
+ </head>
7
+ <body>
8
+ <%= yield %>
9
+ </body>
10
+ </html>
@@ -0,0 +1,18 @@
1
+ xml.TiVoContainer do
2
+
3
+ xml.Details do
4
+ xml.Title container.title
5
+ xml.ContentType container.content_type
6
+ xml.SourceFormat container.source_format
7
+ xml.TotalItems children.size
8
+ end
9
+
10
+ paginated_children = children[item_start, item_count]
11
+ paginated_children.each do |child|
12
+ builder :_container, layout: false, locals: { xml: xml, container: child }
13
+ end
14
+
15
+ xml.ItemStart item_start
16
+ xml.ItemCount paginated_children.size
17
+
18
+ end
@@ -0,0 +1,7 @@
1
+ xml.TiVoServer do
2
+ xml.Version "1.6"
3
+ xml.InternalName "TivoHMO"
4
+ xml.InternalVersion TivoHMO::VERSION
5
+ xml.Organization "TivoHMO Developers"
6
+ xml.Comment "https://github.com/wr0ngway/tivohmo"
7
+ end
@@ -0,0 +1,7 @@
1
+ <h3>Unsupported Command</h3>
2
+ <p>Query:</p>
3
+ <ul>
4
+ <% params.each do |key, value| %>
5
+ <li><%= key %>: <%= value %></li>
6
+ <% end %>
7
+ </ul>
@@ -0,0 +1,10 @@
1
+ #GET /TiVoConnect?Command=QueryFormats&SourceFormat=video%2Fx-tivo-mpeg HTTP/1.1.
2
+
3
+ xml.TiVoFormats do
4
+ formats.each do |format|
5
+ xml.Format do
6
+ xml.ContentType format
7
+ xml.Description ""
8
+ end
9
+ end
10
+ end