Advertisement
patronanejo

Error at 365: undefined method 'success'

Jan 15th, 2013
168
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.71 KB | None | 0 0
  1. # 4od-dl version 0.3. https://github.com/robwatkins/4od-dl
  2.  
  3. require 'rubygems'
  4. require 'logger'
  5. require 'nokogiri'
  6. require 'crypt/blowfish'
  7. require 'base64'
  8. require 'open-uri'
  9. require 'optparse'
  10.  
  11. @log = Logger.new(STDOUT)
  12. @log.sev_threshold = Logger::INFO
  13.  
  14. class FourODProgramDownloader
  15. def initialize(program_id, logger, out_path, remux)
  16. #Search range determines how far before/after the program ID to search for a MP4 file when the original program ID resolves to a f4m.
  17. @search_range = 10
  18. @out_dir = out_path
  19. @program_id = program_id
  20. @mp4_program_id = nil
  21. @out_file = nil
  22. @log = logger
  23. @metadata = Hash.new
  24. @remux = remux
  25. end
  26.  
  27. def download_image(url, out_file_name)
  28. @log.debug "Downloading image from #{url} to #{out_file_name}"
  29. open(url) {|f|
  30. File.open(out_file_name,"wb") do |file|
  31. file.puts f.read
  32. end
  33. }
  34. end
  35.  
  36.  
  37. def download_data(url)
  38. begin
  39. doc = open(url) { |f| Nokogiri(f) }
  40. return doc
  41. rescue OpenURI::HTTPError => the_error
  42. raise "Cannot download from url #{url}. Error is: #{the_error.message}"
  43. end
  44. end
  45.  
  46. #AIS data - used to get stream data
  47. def download_ais(prog_id)
  48. return download_data("http://ais.channel4.com/asset/#{prog_id}")
  49. end
  50.  
  51. #asset info used for episode related information
  52. def download_asset(prog_id)
  53. return download_data("http://www.channel4.com/programmes/asset/#{prog_id}")
  54. end
  55.  
  56. #Program guide used for synopsis
  57. def download_progguide(prog_guide_url)
  58. return download_data("http://www.channel4.com#{prog_guide_url}")
  59. end
  60.  
  61. #read all the program metadata in one go - used for tagging and file name generation
  62. def get_metadata
  63. doc = download_ais(@program_id)
  64. streamUri = (doc/"//streamuri").text
  65. @metadata[:fileType] = streamUri[-3..-1]
  66. @metadata[:programName] = (doc/"//brandtitle").text
  67. @metadata[:episodeId] = (doc/"//programmenumber").text
  68.  
  69. assetInfo = download_asset(@program_id)
  70. @metadata[:episodeNumber] = (assetInfo/"//episodenumber").text
  71. @metadata[:seriesNumber] = (assetInfo/"//seriesnumber").text
  72. @metadata[:episodeInfo] = (assetInfo/"//episodeinfo").text
  73. @metadata[:episodeTitle] = (assetInfo/"//episodetitle").text
  74. @metadata[:brandTitle] = (assetInfo/"//brandtitle").text
  75. @metadata[:epId] = (assetInfo/"//programmeid").text
  76. @metadata[:imagePath] = (assetInfo/"//imagepath").text
  77.  
  78. @metadata[:title1] = (assetInfo/"//title1").text
  79. @metadata[:title2] = (assetInfo/"//title2").text
  80.  
  81. #progGuideUrl is used to pull out metadata from the CH4 website
  82. progGuideUrl = (assetInfo/"//episodeguideurl").text
  83.  
  84. begin
  85. #read program guide to get additional metadata
  86. seriesInfo = download_progguide(progGuideUrl)
  87.  
  88. synopsisElem = seriesInfo.at("//meta[@name='synopsis']")
  89. @metadata[:description] = synopsisElem.nil? ? "" : synopsisElem['content']
  90. rescue
  91. @log.error "Unable to read program guide data - the video file will not be fully tagged"
  92. @log.debug "Program Guide URL: #{progGuideUrl}"
  93. end
  94. end
  95.  
  96.  
  97. #check that the AIS data for the program ID points to a MP4 file. If not, search nearby program IDs for a MP4 version of this program.
  98. #set @mp4_program_id to the MP4 data. This will be used for downloading with rtmpdump
  99. def check_prog_id
  100. if (@metadata[:fileType] == "mp4")
  101. @log.info "AIS data for Program ID #{@program_id} resolves to a MP4"
  102. @mp4_program_id = @program_id
  103. return
  104. elsif (@metadata[:fileType] == "f4m")
  105. @log.info "AIS data for program ID #{@program_id} returns F4M file. Searching for a MP4 version..."
  106. for i in ((@program_id.to_i - @search_range)..(@program_id.to_i + @search_range))
  107. if i != @program_id.to_i and (search_prog_id(i, @metadata[:programName], @metadata[:episodeId]))
  108. @log.info "Found MP4 match: program ID #{i}"
  109. @mp4_program_id = i
  110. return
  111. end
  112. end
  113. end
  114.  
  115. #Either can't find a mp4 to download or wrong asset ID given
  116. raise "Unable to find a MP4 version of the program to download :-("
  117. end
  118.  
  119.  
  120. #Search for an alternative program ID. Will return true if it finds a matching program (on episode ID and Program Name)
  121. def search_prog_id(prog_id, programName, episodeId)
  122. begin
  123. @log.debug "Trying Program ID #{prog_id}"
  124. doc = download_ais(prog_id)
  125. streamUri = (doc/"//streamuri").text
  126. fileType = streamUri[-3..-1]
  127. match_programName = (doc/"//brandtitle").text
  128. match_episodeId = (doc/"//programmenumber").text
  129. @log.debug "found program #{match_programName} and #{match_episodeId}, type #{streamUri[-3..-1]}"
  130.  
  131. return (fileType == "mp4" and programName == match_programName and episodeId == match_episodeId)
  132. rescue
  133. return false
  134. end
  135.  
  136. end
  137.  
  138. #build filename based on metadata, using title1/title2 tag in AIS data and the episode title (if there is one)
  139. def generate_filename
  140. #if episodeTitle != brandTitle (brandTitle looks to be the name of the program) then use this in the filename
  141. if @metadata[:episodeTitle] != @metadata[:brandTitle]
  142. out_file = "#{@metadata[:title1]}__#{@metadata[:title2]}__#{@metadata[:episodeTitle]}"
  143. else #otherwise just use title1/2
  144. out_file = "#{@metadata[:title1]}__#{@metadata[:title2]}"
  145. end
  146. out_file.gsub!(/[^0-9A-Za-z.\-]/, '_') #replace non alphanumerics with underscores
  147.  
  148. @out_file = File.join(@out_dir, out_file)
  149. end
  150.  
  151.  
  152. #Download the program to a given directory
  153. def download
  154. get_metadata
  155. check_prog_id
  156. generate_filename
  157. download_stream
  158. ffmpeg
  159. tag
  160. cleanup
  161. end
  162.  
  163.  
  164. #download the stream using RTMPDump
  165. def download_stream
  166.  
  167. #Read the AIS data from C4. This gives the info required to get the flv via rtmpdump
  168. doc = download_ais(@mp4_program_id)
  169.  
  170. #Parse it - the inspiration for this comes from http://code.google.com/p/nibor-xbmc-repo/ too.
  171. token = (doc/"//token").text
  172. epid = (doc/"//e").text
  173. cdn = (doc/"//cdn").text
  174. streamUri = (doc/"//streamuri").text
  175. decoded_token = decode_token(token)
  176.  
  177. if cdn == 'll'
  178. file = streamUri.split("/e1/")[1]
  179. out_file = file.split("/")[1].gsub(".mp4",".flv")
  180. auth = "e=#{epid}&h=#{decoded_token}"
  181.  
  182. rtmpUrl = "rtmpe://ll.securestream.channel4.com/a4174/e1"
  183. app = "a4174/e1"
  184. playpath = "/#{file}?#{auth}"
  185.  
  186. else
  187. file = streamUri.split("/4oD/")[1]
  188. fingerprint = (doc/"//fingerprint").text
  189. slist = (doc/"//slist").text
  190. auth = "auth=#{decoded_token}&aifp=#{fingerprint}&slist=#{slist}"
  191.  
  192. rtmpUrl = streamUri.match('(.*?)mp4:')[1].gsub(".com/",".com:1935/")
  193. rtmpUrl += "?ovpfv=1.1&" + auth
  194.  
  195. app = streamUri.match('.com/(.*?)mp4:')[1]
  196. app += "?ovpfv=1.1&" + auth
  197.  
  198. playpath = streamUri.match('.*?(mp4:.*)')[1]
  199. playpath += "?" + auth
  200.  
  201. end
  202.  
  203. @log.debug "rtmpUrl: #{rtmpUrl} app: #{app} playpath: #{playpath}"
  204.  
  205. #build rtmpdump command
  206. command = "rtmpdump --rtmp \"#{rtmpUrl}\" "
  207. command += "--app \"#{app}\" "
  208. command += "--playpath \"#{playpath}\" "
  209. command += "-o \"#{@out_file}.flv\" "
  210. command += '-C O:1 -C O:0 '
  211. command += '--flashVer "WIN 10,3,183,7" '
  212. command += '--swfVfy "http://www.channel4.com/static/programmes/asset/flash/swf/4odplayer-11.34.1.swf" '
  213. @log.debug command
  214.  
  215. @log.info "Downloading file for Program ID #{@mp4_program_id} - saving to #{@out_file}.flv"
  216. success = system(command)
  217.  
  218. if not success
  219. raise "Something went wrong running rtmpdump :(. Your file may not have downloaded."
  220. end
  221.  
  222. @log.info "Download complete."
  223.  
  224. end
  225.  
  226.  
  227. #Run ffmpeg to convert to MP4 - There is an annoying bug in later versions of ffmpeg related to
  228. #playing MP4s on a PS3 - during playback the video skips and has no sound so is completely unwatchable.
  229. #Remapping the audio codec to AAC fixes it. I tested this with ffmpeg 0.10.3
  230. def ffmpeg
  231. @log.info "Running ffmpeg to convert to MP4"
  232. ffmpegOutOptions = "-strict experimental -vcodec copy -acodec aac"
  233. if @remux
  234. ffmpegOutOptions = "-vcodec copy -acodec copy"
  235. end
  236. ffmpeg_command ="ffmpeg -y -i \"#{@out_file}.flv\" #{ffmpegOutOptions} \"#{@out_file}.mp4\""
  237. success = system(ffmpeg_command)
  238.  
  239. if not success
  240. raise "Something went wrong running ffmpeg :(. Your file may not have converted properly."
  241. end
  242.  
  243. @log.info "File converted"
  244. end
  245.  
  246. #Tag with AtomicParsley using the metadata retrieved earlier
  247. def tag
  248. @log.info "Tagging file...."
  249. if @metadata[:episodeNumber] != ""
  250. fullTitle = "#{@metadata[:episodeNumber]}. #{@metadata[:episodeTitle]}"
  251. else
  252. fullTitle = "#{@metadata[:episodeTitle]} - #{@metadata[:episodeInfo]}"
  253. end
  254. atp_command = "AtomicParsley \"#{@out_file}.mp4\" --TVNetwork \"Channel4/4od\" --TVShowName \"#{@metadata[:brandTitle]}\" --stik \"TV Show\" --description \"#{@metadata[:description]}\" --TVEpisode \"#{@metadata[:epId]}\" --title \"#{fullTitle}\" --overWrite"
  255.  
  256. if @metadata[:seriesNumber] != ""
  257. atp_command += " --TVSeasonNum #{@metadata[:seriesNumber]}"
  258. end
  259. if @metadata[:episodeNumber] != ""
  260. atp_command += " --TVEpisodeNum #{@metadata[:episodeNumber]}"
  261. end
  262.  
  263. #If it exists, download the image and store in metadata
  264. if @metadata[:imagePath] != ""
  265. begin
  266. image_path = File.join(@out_dir,File.basename(@metadata[:imagePath]))
  267. download_image("http://www.channel4.com#{@metadata[:imagePath]}", image_path)
  268. atp_command += " --artwork \"#{image_path}\""
  269. rescue
  270. @log.warn "Error downloading thumbnail - video will be tagged without thumbnail"
  271. end
  272. end
  273.  
  274. @log.debug "#{atp_command}"
  275. success = system(atp_command)
  276.  
  277. if @metadata[:imagePath] != "" && File.exists?(image_path)
  278. File.delete(image_path)
  279.  
  280. end
  281.  
  282. if not success
  283. raise "Something went wrong running AtomicParsley :(. Your file may not be properly tagged."
  284. end
  285. end
  286.  
  287. #Remove the FLV file
  288. def cleanup
  289. @log.debug "Deleting #{@out_file}.flv"
  290. if File.exists?("#{@out_file}.flv")
  291. File.delete("#{@out_file}.flv")
  292. end
  293. end
  294.  
  295. #Method to decode an auth token for use with rtmpdump
  296. #Idea mostly taken from http://code.google.com/p/nibor-xbmc-repo/source/browse/trunk/plugin.video.4od/fourOD_token_decoder.py
  297. #Thanks to nibor for writing this in the first place!
  298. def decode_token(token)
  299. encryptedBytes = Base64.decode64(token)
  300. key = "STINGMIMI"
  301. blowfish = Crypt::Blowfish.new(key)
  302.  
  303. position = 0
  304. decrypted_token = ''
  305.  
  306. while position < encryptedBytes.length
  307. decrypted_token += blowfish.decrypt_block(encryptedBytes[position..position + 7]);
  308. position += 8
  309. end
  310.  
  311. npad = decrypted_token.slice(-1)
  312. if (npad > 0 && npad < 9)
  313. decrypted_token = decrypted_token.slice(0, decrypted_token.length-npad)
  314. end
  315.  
  316. return decrypted_token
  317. end
  318.  
  319. end
  320.  
  321.  
  322. #Parse parameters (only -p is required)
  323. hash_options = {}
  324. optparse = OptionParser.new do |opts|
  325. opts.banner = "Usage: 4od-dl [options]"
  326. opts.on('-p', '--programids ID1,ID2,ID3', "Program IDs to download - this is the 7 digit program ID that you find after the hash in the URL (e.g. 3333316)") do |v|
  327. hash_options[:pids] = v
  328. end
  329. hash_options[:outdir] = Dir.pwd
  330. opts.on('-o', '--outdir PATH', "Directory to save files to (default = pwd)") do |v|
  331. hash_options[:outdir] = v
  332. end
  333. opts.on('-r', '--remux', "Copy video/audio streams from FLV to MP4 - do not transcode audio") do |v|
  334. hash_options[:remux] = v
  335. end
  336. opts.on('-v', '--version', 'Display version information') do
  337. puts "4od-dl version 0.3 (16-Dec-2012)"
  338. exit
  339. end
  340. opts.on('-d', '--debug', 'Show advanced debugging information') do
  341. @log.sev_threshold = Logger::DEBUG
  342. end
  343. opts.on('-h', '--help', 'Display this help') do
  344. puts opts
  345. exit
  346. end
  347. end
  348.  
  349. optparse.parse!
  350.  
  351. if hash_options[:pids].nil?
  352. puts "Mandatory parameter -p not specified."
  353. puts optparse
  354. exit 1
  355. end
  356.  
  357. if !File.directory?(hash_options[:outdir])
  358. @log.error "Cannot find given output directory #{hash_options[:outdir]}. Exiting."
  359. exit 1
  360. end
  361.  
  362. #Given valid arguments. Check for pre-reqs
  363. @log.debug "looking for rtmpdump"
  364. #`which rtmpdump`
  365. if not $?.success?
  366. @log.error "Cannot find rtmpdump on your path. Please install and try again (I downloaded mine from http://trick77.com/2011/07/30/rtmpdump-2-4-binaries-for-os-x-10-7-lion/)"
  367. exit 1
  368. end
  369.  
  370. @log.debug "looking for ffmpeg"
  371. #`which ffmpeg`
  372. if not $?.success?
  373. @log.error "Cannot find ffmpeg on your path. Please install and try again (http://ffmpegmac.net). After extracting copy it to somewhere in your path"
  374. exit 1
  375. end
  376.  
  377. @log.debug "looking for AtomicParsley"
  378. #`which AtomicParsley`
  379. if not $?.success?
  380. @log.error "Cannot find AtomicParskey on your path. Please install and try again (http://atomicparsley.sourceforge.net/). After extracting copy it to somewhere in your path"
  381. exit 1
  382. end
  383.  
  384. #Download!
  385. hash_options[:pids].split(",").each do |prog_id|
  386. begin #first check it is a valid integer prog_id
  387. Integer(prog_id)
  388. rescue
  389. @log.error "Cannot parse program ID #{prog_id}. Is it a valid program ID?"
  390. end
  391.  
  392. #now download
  393. begin
  394. #Attempt to get a program ID which resolves to a MP4 file for this program, then download the file
  395. @log.info "Downloading program #{prog_id}..."
  396. fourOD = FourODProgramDownloader.new(prog_id, @log,hash_options[:outdir],hash_options[:remux])
  397. fourOD.download
  398. rescue Exception => e
  399. @log.error "Error downloading program: #{e.message}"
  400. @log.error "#{e.backtrace.join("\n")}"
  401. end
  402. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement