I’ve expanded on my previous podcast downloader program so that it gets torrents as well. I was running Automatic but it seemed silly to have two programs doing more or less the same thing.
I also didn’t like the configuration of Automatic, which has a global set of regexps it applies to all feeds to decide whether to download an item. My program has separate regexps for each feed1 which I think is a bit easier to deal with. In order to make the config file easy to parse, it is actually another Ruby file, which is read with instance_eval. By having methods defined as feed and matches the config file can just use those and look relatively sane:
feed 'http://some.feed.url/blah.rss' matches 'title.to.match.*\d+'
(Some people would call this a DSL, but I think that’s stretching a point)
To make the torrents get into Transmission, the torrent downloader we’re using, I just call the transmission-remote command. Sometimes it’s easier to use what works.
So here’s the full program (see my previous post for how to set things up on the DNS-323):
#!/ffp/bin/env ruby require 'open-uri' require 'rss' CONFFILE='/ffp/etc/mypodder2.conf' PODCASTDIR='/mnt/HD_a2/podcasts' TORRENTDIR='/mnt/HD_a2/torrents' TORRENT_UPLOAD='/ffp/bin/transmission-remote "127.0.0.1:9091" -a "%s"' class ConfigParser def initialize(feeds) @feeds = feeds end def parse(file) instance_eval(File.read(file), file) end def feed(url) @current = Feed.new(url) @feeds << @current end def matches(pattern) @current.add_pattern(pattern) if @current end alias :or :matches end class Feed def initialize(url) @url = url @patterns = [] end def uri @url end def add_pattern(pattern) if(pattern && pattern.length > 0) @patterns << Regexp.new(pattern, true) end end def allow?(item) icompare = item.title @patterns.empty? || @patterns.find {|p| p =~ icompare} end end class Main def initialize(options = {}) @options = {:config => CONFFILE, :verbose => false}.merge(options) @feeds = [] read_config end def read_config ConfigParser.new(@feeds).parse(@options[:config]) end def vprintln(sometext) if(@options[:verbose]) puts sometext end end def torrent?(item) item.enclosure.type =~ /.*torrent/ end def run() @feeds.each do |feed| vprintln "checking feed #{feed.uri}" rss_content = "" open(feed.uri) do |f| rss_content = f.read end rss = RSS::Parser.parse(rss_content, false) rss.items.each do |item| vprintln " + checking item #{item.title}" if(item.enclosure && feed.allow?(item)) iurl = item.enclosure.url vprintln " + + allowed item #{iurl}" ctitle = rss.channel.title.gsub(/\W/, '_') base = PODCASTDIR base = TORRENTDIR if torrent?(item) system("mkdir -p #{File.join(base, ctitle)}") system("chmod a+rw #{File.join(base, ctitle)}") ifile = File.join(base, ctitle, File.basename(iurl)) unless File.exists?(ifile) puts "About to download #{iurl} as #{ifile}" system("wget -O #{ifile} #{iurl}") if(torrent?(item) && File.exists?(ifile)) system(sprintf(TORRENT_UPLOAD, ifile)) end else vprintln " + + file exists" end end end end end end if __FILE__ == $0 require 'optparse' options = {} opts = OptionParser.new do |op| op.on("-f CONFIG", "--config CONFIG", "Specify config file (default #{CONFFILE})") do |f| options[:config] = f end op.on("-v", "--verbose", "Output messages") do options[:verbose] = true end # No argument, shows at tail. This will print an options summary. op.on_tail("-h", "--help", "Show this message") do puts opts exit end end opts.parse(ARGV) Main.new(options).run end
1 At the moment, it ‘OR’s together multiple regexps on a single feed, but I’m thinking I may change that so it’s easier to exclude certain matches as well as include.
