Skip to content

Instantly share code, notes, and snippets.

@inferiorhumanorgans
Last active December 25, 2015 02:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inferiorhumanorgans/6900411 to your computer and use it in GitHub Desktop.
Save inferiorhumanorgans/6900411 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'optparse'
require 'ostruct'
CHANNEL_URL = 'http://web.mobilerider.com/clients/hsb/api/media/?filter={"channel":{},"query":""}'
STREAMS = {}
$options = OpenStruct.new
$options.debug = false
$options.format = '.mp4'
$options.ffmpeg = ENV['ffmpeg'] || './ffmpeg'
def parse_cmdline
opts = OptionParser.new do |opts|
opts.on('-f', '--format EXTENSION', 'Specify an ffmpeg supported container') do |ext|
$options.format = ext
end
opts.on('-d', '--debug', 'Enable debugging nonsense') do
$options.debug = true
end
opts.on('-r', '--resolution RESOLUTION', 'Desired resolution') do |resolution|
$options.resolution = resolution
end
opts.on('-x', '--executable EXECUTABLE', 'Path to ffmpeg executable') do |exe|
$opts.ffmpeg = exe
end
opts.on('-i', '--ids STREAM_IDS', 'Comma separated list of stream ids') do |ids|
$options.ids = ids.split(',')
end
opts.on('-l' , '--list', 'List the available streams and exit') do
$options.list_only = true
end
end
opts.parse(ARGV)
end
def get_url(url)
STDERR.puts "Fetching: #{url}" if $options.debug
escaped = URI.escape(url)
return Net::HTTP.get(URI(escaped))
end
def get_json (url)
STDERR.puts "JSON decoding: #{url}" if $options.debug
return JSON.parse(get_url(url))
end
def get_streams
streams = get_json(CHANNEL_URL)
unless streams.include? 'objects'
STDERR.puts "Malformed channel info"
exit(-1)
end
STREAMS['62665'] = {title: '2012-10-05', vendor_id: 2409}
STREAMS['62672'] = {title: '2012-10-06', vendor_id: 2409}
STREAMS['62680'] = {title: '2012-10-07', vendor_id: 2409}
streams['objects'].sort{|a,b| a['title'] <=> b['title']}.each do |stream|
# Presumably the <STAGE NAME> LIVE streams are just the stage for the whole day
next if stream['title'] =~ /Live/ unless defined? INCLUDE_FULL_DAY_STREAMS_TOO
STREAMS[stream['id']] = {title: stream['title'], vendor_id: 3050}
end
end
def show_streams
puts "%5s\t%s" % ['ID', 'Artist']
STREAMS.each do |stream_id, stream|
puts "%5d\t%s" % [stream_id, stream[:title]]
end
end
def choose_stream
show_streams
STDERR.printf "Pick a stream ID: "
STDERR.flush
input = STDIN.gets.strip
unless STREAMS.include? input
STDERR.puts "Couldn't find that stream, sorry."
exit(-1)
end
input
end
def get_resolutions(stream_id)
stream = get_json("http://web.mobilerider.com/api2/#{STREAMS[stream_id][:vendor_id]}/media/#{stream_id}.json")
# So I guess we could get different playlists here, but for now let's just take the first playlist we find.
m3u = get_url(stream['files'].values.first).split("\n")
STREAMS[stream_id][:resolutions] = {}
m3u.each_with_index do |line, idx|
next unless line =~ /#EXT-X-STREAM-INF/
# Gross, but good enough for HSB
resolution = line.sub(/.*RESOLUTION=/, '').sub(/,.*/, '')
STREAMS[stream_id][:resolutions][resolution] = m3u[idx+1]
end
if STREAMS[stream_id][:resolutions].empty?
STREAMS[stream_id][:master_playlist] = stream['files'].values.first
end
end
def choose_resolution(stream_id)
return nil if STREAMS[stream_id][:resolutions].empty?
STREAMS[stream_id][:resolutions].keys.each_with_index do |resolution, idx|
STDERR.puts '%2d.) %s' % [idx, resolution]
end
STDERR.printf 'Pick a resolution: '
STDERR.flush
resolution = STDIN.gets.strip
resolution = STREAMS[stream_id][:resolutions].keys[resolution.to_i] unless resolution =~ /x/
unless STREAMS[stream_id][:resolutions].include? resolution
STDERR.puts "Couldn't find that resolution, sorry"
exit(-1)
end
resolution
end
def get_mux_cmd(stream_id=nil, resolution=nil)
get_streams if STREAMS.empty?
stream_id = choose_stream if stream_id.nil?
get_resolutions(stream_id) unless STREAMS[stream_id].include? :resolutions
resolution = choose_resolution(stream_id) if resolution.nil? or resolution.empty?
if resolution
"#{$options.ffmpeg} -i '#{STREAMS[stream_id][:resolutions][resolution]}' -c copy -bsf:a aac_adtstoasc '#{STREAMS[stream_id][:title]}#{$options.format}'"
else
"#{$options.ffmpeg} -i '#{STREAMS[stream_id][:master_playlist]}' -c copy -bsf:a aac_adtstoasc '#{STREAMS[stream_id][:title]}#{$options.format}'"
end
end
parse_cmdline
if $options.list_only == true
get_streams if STREAMS.empty?
show_streams
exit 0
end
if $options.ids
$options.ids.each do |id|
system(get_mux_cmd(id, $options.resolution))
end
else
system(get_mux_cmd(nil, $options.resolution))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment