#!/usr/bin/ruby

=begin
Purpose: 
  Automate channel selection for premium membershi from
  Digitally Imported ( http://DI.fm/ ) radio
Author: Wejn <wejn at box dot cz>
License: GPLv2 (without the "latter" option)
Requires: Ruby >= 1.8, di.fm premium membership, mplayer,
          basic unix skills to operate the script ;)
TS: 20061217000000

Installation:
1) Copy this file to directory in your $PATH
2) Create ~/.difmrc containing your di.fm access credentials
   in format: username:password
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.
=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
    BASEURL = 'http://www.di.fm/listencustom/192k.pls'
    CACHE = File.join((ENV['HOME'] || '.'), '.difm.cache.pls')
    CACHE_VALIDITY = 20*60 # 20 minutes


    attr_reader :channels

    def initialize(username, password) # {{{2
        content = fetch_pls(username, password)

        @channels = parse_pls(content)
    end

    def search_channel(chan)
        out = []
        @channels.each do |c|
            out << c if Regexp.new(chan, Regexp::IGNORECASE) =~ c.title
        end
        out
    end # }}}2

    private

    def fetch_pls(username, password) # {{{2
        url = BASEURL + '?user=' + username + '&pass=' + password
        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])
            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: 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 <wejn at box dot cz>, version: 1"
puts "=------------------------------------------------------------="
puts

creds = nil

# Fetch credentials from rcfile ...
begin # {{{1
    rc = File.open(File.join(ENV['HOME'] || '.', '.difmrc')).gets.chomp
    creds = rc.split(/:/, 2)
    raise "Wrong credentials" unless creds.size == 2
rescue
    puts "Unable to load rc file (for whatever reason)!"
    puts "Make sure you have your credentials in ~/.difmrc in following"
    puts "  format: username:password"
    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 :