# # Author:: R.J.Sharp # Email:: robert(a)osburn-sharp.ath.cx # Copyright:: Copyright (c) 2013 # License:: Open Software Licence v3.0 # # This software is licensed for use under the Open Software Licence v. 3.0 # The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php # and in the file LICENCE. Under the terms of this licence, all derivative works # must themselves be licensed under the Open Software Licence v. 3.0 # # # [requires go here] # ffmpeg -itsoffset -4 -i test.avi -vcodec mjpeg -vframes 1 -an -f rawvideo -s 320x240 test.jpg # require 'jeeves/errors' require 'jeeves/utils' require 'nokogiri' require 'open-uri' # = Jetags # # Helper class to create and manipulate tag data to be included in or extracted from matroska files # module Jeeves class Tagger Fingerprint = "JeevesTags" # create a tags objects def initialize(options = {}) #@jeeves_tv_url = options[:jeeves_tv_url] || raise(MissingOption, "Need to have a jeeves tv url") @tag_data = nil @tag_hash = Hash.new @tag_hash[:fingerprint] = Fingerprint end # provided for backward compatibility - use new_from_remote_id def self.new_from_prog_id(prog_id, listing_url) tags = self.new tags.load_tags_from_prog(prog_id, listing_url) return tags end # get tags from a remote service with the id, route and url # #{url}/#{route}/#{id}.xml # which should provide an XML document with Simple nodes and # Name and String node contents. # # # # # name # value # # # ... # # # # def self.new_from_remote_id(id, route, url) tags = self.new tags.load_tags_from_remote(id, route, url) return tags end def self.new_from_file(pathname) tags = self.new tags.load_tags_from_file(pathname) return tags end # get tags for the given programme from jeeves-tv and # embed them in the given video file, which incidentally becomes an mkv # WARNING - mkvmerge, used here to tag stuff, makes a big hash of mpegts # and often seems to screw the audio up completely. Probably not worth trying this one! # def write_to_file(path) # check that the path exists and get its basename unless File.readable?(path) raise FileError, "Cannot read: #{path}" end dirname = File.expand_path(File.dirname(path)) bname = File.basename(path, File.extname(path)) mkv_path = File.join(dirname, bname + '.mkv') unless mkv_path != path raise FileError, "Source file is already matroska" end xml_path = File.join(dirname, bname + '.xml') # get the tags from jeeves-tv, wherever that might be and save to a hidden file cmd = "/usr/bin/wget #{@jeeves_tv_url}/pid/#{prog_id}.xml -O #{xml_path}" unless system(cmd) raise XMLError, "Failed to get xml data: #{$?}. Command line was: #{cmd}" end unless File.exists?(xml_path) raise XMLError, "XML file: #{xml_path} was not created" end # merge the tags with the video file to create a matroska file cmd = "/usr/bin/mkvmerge --global-tags #{xml_path} #{path} -o #{mkv_path}" unless system(cmd) raise MkvError, "Failed to create mkv: #{$?}, Command line was: #{cmd}" end return true end # provide all tag data as key, value pairs def each_tag(&block) @tag_hash.each do |tag, value| block.call(tag, value) end end # return all the tags as a string of key=value pairs def to_a tags_to_exclude = %w[priority tuner] tag_ary = Array.new @tag_hash.each do |tag, value| next if tags_to_exclude.include?(tag) tag_ary << tag.to_s + '=' + value end return tag_ary end def to_s strings = '' each_tag do |tag, value| strings << " #{tag}: #{value}\n" end return strings end alias :metadata :to_a # return the value of a given tag def [](tag) return @tag_hash[tag] end # create a new tag def []=(tag, value) @tag_hash[tag] = value end def delete(tag) @tag_hash.delete(tag) end def has_tag?(tag) @tag_hash.has_key?(tag) end def jeeves? @tag_hash.has_key?(:fingerprint) && @tag_hash[:fingerprint] == Fingerprint end #protected # get tags direct from programme listing def load_tags_from_prog(prog_id, url) @tag_data = String.new my_url = "#{url}/pid/#{prog_id}.xml" open(my_url) do |xml| xml.each_line {|line| @tag_data << line} end self.xml_to_hash #rescue #raise UrlError, "Url failed to open: #{my_url}" end # get tags from a remote service on the given url and route # #{url}/#{route}/#{id}.xml # # should replace load_tags_from_prog def load_tags_from_remote(id, route, url) @tag_data = String.new my_url = "#{url}/#{route}/#{id}.xml" #puts "URL: #{my_url}" open(my_url) do |xml| xml.each_line do |line| #puts line @tag_data << line end end self.xml_to_hash #puts @tag_hash.inspect end # extract tags from an mkv video and return as an xml string def load_tags_from_file(path) unless File.readable?(path) raise FileError, "Cannot read: #{path}" end dirname = File.expand_path(File.dirname(path)) bname = File.basename(path, File.extname(path)) xml_path = File.join(dirname, bname + '.xml') @tag_data = `/usr/bin/mkvextract tags #{path}` self.xml_to_hash # unless system(cmd) # raise MkvError, "Failed to extract tags: #{$?}, command line: #{cmd}" # end # # unless File.exists?(xml_path) # raise XMLError, "XML file: #{xml_path} was not created" # end # # @tag_data = File.readlines(xml_path) end def load_tags_from_imdb(url) doc = Nokogiri::HTML(open(url)) name = doc.at_css('*[itemprop="name"]').content @tag_hash[:programme] = name.gsub("\n", '').gsub(/\([0-9]+\)/, '') @tag_hash[:duration] = doc.at_css('*[itemprop="duration"]').content.to_i * 60 @tag_hash[:date] = doc.at_css('*[itemprop="datePublished"]').content.match(/.*([0-9]{4,4})/)[1] @tag_hash[:description] = doc.at_css('*[itemprop="description"]').content.gsub("\n", '') @tag_hash[:source] = 'IMDB' @tag_hash[:category] = 'Film' end # copy a file and apply the given tags to it def apply_tags_to_file(filename) if FileTest.exists?(filename) then source = funique(filename) FileUtils.mv(filename, source) dn, bn, en = fsplit(filename) target = File.join(dn, bn + ".mkv") ffargs = "-i #{source} -c:v copy -c:a copy -f matroska -y".split(' ') self.metadata.each do |tag| ffargs << "-metadata" ffargs << tag end ffargs << target system("/usr/bin/ffmpeg", *ffargs) end end # copy a bit of a file and apply the given tags to it def apply_to_proxy(filename) if FileTest.exists?(filename) then self['path'] = filename target = Jeeves.rename(filename) dn, bn, en = fsplit(target) target = File.join(dn, bn + ".mkv") ffargs = "-i #{filename} -v quiet -t 10 -f matroska -y".split(' ') self.metadata.each do |tag| ffargs << "-metadata" ffargs << tag end ffargs << target unless system("/usr/bin/ffmpeg", *ffargs) raise Jeeves::MkvError, "ffmpeg failed with args: #{ffargs.join(' ')}" end return target end end def xml_to_hash xml_tags = Nokogiri::XML(@tag_data) @tag_hash = Hash.new # ensure you are not assuming this comes from Jeeves xml_tags.xpath('//Simple').each do |tag| @tag_hash[tag.at_xpath('Name').content.downcase] = tag.at_xpath('String').content end end def funique(filename) dirname, basename, extname = fsplit(filename) newfilename = File.join(dirname, basename + extname) while FileTest.exists?(newfilename) # oh dear, got one already. need to add something to the name randname = (rand * 10000).to_i.to_s basename = basename + '_' + randname + extname newfilename = File.join(dirname, basename) end return newfilename end def fsplit(filename) dname = File.dirname(filename) extname = File.extname(filename) basename = File.basename(filename, extname) return [dname, basename, extname] end end end