#!/usr/bin/ruby

=begin
Purpose:
  Automate channel selection for premium membership from
  Digitally Imported ( http://DI.fm/ ) radio
  and now also from SKY.fm ( http://sky.fm/ ) radio
Author: Wejn <box at wejn dot org>
License: MIT
Requires: Ruby >= 1.8, di.fm premium membership, mplayer,
          basic unix skills to operate the script ;)
TS: 20120102143200

Installation:
1) Copy this file to directory in your $PATH
2) Create ~/.difmrc containing your di.fm listen key
3) launch it without params to see list of channels
4) give channel name (regexp) as param to launch the stream via mplayer

Beware: Channel list gets cached (for 20 mins) as ~/.difm.cache*.pls
        to avoid hitting them everytime. If you absolutely need
        to refresh the list (after messing w/ your favs), remove
        the cache file.

        [this also means that this script sees just your faves as
        its channel list]
=end

require 'net/http'
require 'open-uri'
require 'timeout'
require 'pp'

# Class that loads channel list from DI.fm premium membership favorites page
class DIFM # {{{1
    API_KEY = 'api_key=53db7c9bf337fa071b4616a296d55157'
    BASEURL_DI = 'http://listen.di.fm/premium_high/favorites.pls?' + API_KEY
    BASEURL_SKY = 'http://listen.sky.fm/premium_high/favorites.pls?' + API_KEY
    CACHE_DI = File.join((ENV['HOME'] || '.'), '.difm.cache_difm.pls')
    CACHE_SKY = File.join((ENV['HOME'] || '.'), '.difm.cache_skyfm.pls')
    CACHE_VALIDITY = 120*60 # 120 minutes


    attr_reader :channels

    def initialize(listen_key) # {{{2
        content = fetch_pls(BASEURL_DI, CACHE_DI, listen_key)
        @channels = parse_pls(content)
        content = fetch_pls(BASEURL_SKY, CACHE_SKY, listen_key)
        @channels += parse_pls(content)
    end

    def search_channel(chan)
        out = []
        re = Regexp.new(chan, Regexp::IGNORECASE)
        $stderr.puts "Match1 via: #{re.inspect}" if $DEBUG || $VERBOSE
        @channels.each do |c|
            out << c if re =~ c.title
        end
        if out.empty?
            # match
            re = Regexp.new(chan.split(/\s+/).join(".*"), Regexp::IGNORECASE)
            $stderr.puts "Match2 via: #{re.inspect}" if $DEBUG || $VERBOSE
            @channels.each do |c|
                out << c if re =~ c.title
            end
        end
        out
    end # }}}2

    private

    def fetch_pls(url, cache, listen_key) # {{{2
        if url.index('?')
            url += '&listen_key=' + listen_key
        else
            url += '?listen_key=' + listen_key
        end
        content = nil

        timeout(20) do
            if ! FileTest.exists?(cache) ||
                    File::stat(cache).mtime < Time.now - CACHE_VALIDITY
                content = open(url).read

                unless content.index("[playlist]")
                    raise ArgumentError, "Wrong credentials!"
                end

                begin
                    File.open(cache, 'w') do |f|
                        f.write(content)
                    end
                end
            else
                content = File.open(cache, 'r').read
            end
        end

        content
    end # }}}2

    Channel = Struct.new(:title, :file)

    class Channel # {{{2
        def url
            file
        end

        def name
            title
        end
    end # }}}2

    def parse_pls(content) # {{{2
        channels = []
        entries = nil

        content = content.split(/\n+/)

        content.shift if content.first.index('[playlist]')

        content.each do |ln|
            ar = ln.split(/=/, 2)
            case ar.first
            when /^NumberOfEntries/
                entries = ar[1].to_i
            when /^(Title|File)(\d+)$/
                #puts "_OK_: " + ar.inspect
                (channels[$2.to_i - 1] ||= Channel.new).
                    send($1.downcase + '=', ar[1].strip.sub(/^.*?-\s+/, ''))
            when /^(Length\d+|Version)$/
                #puts "Skip: " + ar.inspect
            else
                raise "Unexpected pls line: " + ar.inspect
            end
        end

        unless (entries || channels.size) == channels.size
            raise "Number of channels doesn't match pls actual size!"
        end

        channels
    end # }}}2
end # }}}1

# Display DI.fm channels in a nice way
def channels_out(chans) # {{{1
    # to have the list unsorted remove ".sort" from next line:
    names = chans.map { |x| x.name }.sort
    maxn = names.map { |x| x.length }.max

    tsize = 80 # FIXME: too lazy to determine this ...
    count = tsize / (maxn + 2)
    out = []
    names.each_with_index do |n, i|
        out << "" if (i % count).zero?
        out.last << "  " + n.ljust(maxn)
    end
    out
end # }}}1

# Here we go ...

puts "DI.fm simple launcher by Wejn <box at wejn dot org>, version: 3"
puts "=-------------------------------------------------------------="
puts

creds = nil

# Fetch credentials from rcfile ...
begin # {{{1
    creds = File.open(File.join(ENV['HOME'] || '.', '.difmrc')).gets.strip
    raise "Wrong credentials" unless creds =~ /^[0-9a-f]{14,20}$/
rescue
    puts "Unable to load rc file (for whatever reason)!"
    puts "Make sure you have your listen key in ~/.difmrc"
    exit 1
end # }}}1

# Obtain channel list ...
begin # {{{1
    difm = DIFM.new(creds)
rescue
    puts "Unable to download channel list from DI.fm:"
    puts $!.to_s.gsub(/^/, '  ')
    exit 1
end # }}}1

# Perform requested action ...
if ARGV.empty? # {{{1
    puts "Available channels:"
    puts channels_out(difm.channels)
    puts
    puts "Give channel name (regex) on cmdline to launch it."
else
    res = difm.search_channel(ARGV.join(' '))
    case res.size
    when 0
        puts "No such channel '#{ARGV.join(' ')}' ... :("
    when 1
        puts "Channel selected: " + res.first.name
        puts
        exec "mplayer", res.first.url
    else
        puts "Multiple results:"
        puts channels_out(res)
    end
end # }}}1

# Yes, this will annoy the hell out of vim newbies:
# vim: set ft=ruby fdm=marker ts=4 :