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.
- checksums.yaml +7 -0
 - data/.coveralls.yml +1 -0
 - data/.gitignore +17 -0
 - data/.travis.yml +10 -0
 - data/Gemfile +7 -0
 - data/LICENSE.txt +514 -0
 - data/README.md +50 -0
 - data/Rakefile +7 -0
 - data/bin/tivohmo +8 -0
 - data/contrib/tivohmo.conf +17 -0
 - data/contrib/tivohmo.plist +22 -0
 - data/lib/tivohmo/adapters/filesystem/application.rb +23 -0
 - data/lib/tivohmo/adapters/filesystem/file_item.rb +29 -0
 - data/lib/tivohmo/adapters/filesystem/folder_container.rb +105 -0
 - data/lib/tivohmo/adapters/filesystem.rb +3 -0
 - data/lib/tivohmo/adapters/plex/application.rb +41 -0
 - data/lib/tivohmo/adapters/plex/category.rb +72 -0
 - data/lib/tivohmo/adapters/plex/episode.rb +26 -0
 - data/lib/tivohmo/adapters/plex/metadata.rb +24 -0
 - data/lib/tivohmo/adapters/plex/movie.rb +26 -0
 - data/lib/tivohmo/adapters/plex/qualified_category.rb +51 -0
 - data/lib/tivohmo/adapters/plex/season.rb +39 -0
 - data/lib/tivohmo/adapters/plex/section.rb +48 -0
 - data/lib/tivohmo/adapters/plex/show.rb +39 -0
 - data/lib/tivohmo/adapters/plex/transcoder.rb +19 -0
 - data/lib/tivohmo/adapters/plex.rb +11 -0
 - data/lib/tivohmo/adapters/streamio/metadata.rb +26 -0
 - data/lib/tivohmo/adapters/streamio/transcoder.rb +239 -0
 - data/lib/tivohmo/adapters/streamio.rb +17 -0
 - data/lib/tivohmo/api/application.rb +35 -0
 - data/lib/tivohmo/api/container.rb +31 -0
 - data/lib/tivohmo/api/item.rb +32 -0
 - data/lib/tivohmo/api/metadata.rb +98 -0
 - data/lib/tivohmo/api/node.rb +115 -0
 - data/lib/tivohmo/api/server.rb +24 -0
 - data/lib/tivohmo/api/transcoder.rb +39 -0
 - data/lib/tivohmo/api.rb +7 -0
 - data/lib/tivohmo/beacon.rb +69 -0
 - data/lib/tivohmo/cli.rb +182 -0
 - data/lib/tivohmo/logging.rb +50 -0
 - data/lib/tivohmo/server/views/_container.builder +19 -0
 - data/lib/tivohmo/server/views/_item.builder +57 -0
 - data/lib/tivohmo/server/views/container.builder +26 -0
 - data/lib/tivohmo/server/views/item_details.builder +153 -0
 - data/lib/tivohmo/server/views/layout.builder +2 -0
 - data/lib/tivohmo/server/views/layout.erb +10 -0
 - data/lib/tivohmo/server/views/server.builder +18 -0
 - data/lib/tivohmo/server/views/server_info.builder +7 -0
 - data/lib/tivohmo/server/views/unsupported.erb +7 -0
 - data/lib/tivohmo/server/views/video_formats.builder +10 -0
 - data/lib/tivohmo/server.rb +306 -0
 - data/lib/tivohmo/version.rb +3 -0
 - data/lib/tivohmo.rb +5 -0
 - data/spec/adapters/filesystem/application_spec.rb +19 -0
 - data/spec/adapters/filesystem/file_item_spec.rb +33 -0
 - data/spec/adapters/filesystem/folder_container_spec.rb +115 -0
 - data/spec/adapters/plex/application_spec.rb +20 -0
 - data/spec/adapters/plex/category_spec.rb +66 -0
 - data/spec/adapters/plex/episode_spec.rb +22 -0
 - data/spec/adapters/plex/metadata_spec.rb +24 -0
 - data/spec/adapters/plex/movie_spec.rb +22 -0
 - data/spec/adapters/plex/qualified_category_spec.rb +51 -0
 - data/spec/adapters/plex/season_spec.rb +22 -0
 - data/spec/adapters/plex/section_spec.rb +38 -0
 - data/spec/adapters/plex/show_spec.rb +22 -0
 - data/spec/adapters/plex/transcoder_spec.rb +27 -0
 - data/spec/adapters/streamio/metadata_spec.rb +34 -0
 - data/spec/adapters/streamio/transcoder_spec.rb +42 -0
 - data/spec/api/application_spec.rb +63 -0
 - data/spec/api/container_spec.rb +34 -0
 - data/spec/api/item_spec.rb +53 -0
 - data/spec/api/metadata_spec.rb +59 -0
 - data/spec/api/node_spec.rb +178 -0
 - data/spec/api/server_spec.rb +24 -0
 - data/spec/api/transcoder_spec.rb +25 -0
 - data/spec/beacon_spec.rb +87 -0
 - data/spec/cli_spec.rb +227 -0
 - data/spec/server_spec.rb +458 -0
 - data/spec/spec_helper.rb +123 -0
 - data/tivohmo.gemspec +46 -0
 - 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
         
     |