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,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