#!/usr/bin/env ruby require 'douban.fm' require 'optparse' require 'ostruct' class DoubanFMCLI def parse options = OpenStruct.new options.verbose = false opts = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS]" opts.on('-u', '--user email', 'douban.fm account name, normally an email address', 'if not provided, will play anonymous playlist') do |email| options.email = email end opts.on('-p', '--password [password]', 'douban.fm account password', 'if not provided, will be asked') do |password| options.password = password end opts.on('-m', '--mpd', 'do not play by it own, send playlist to Music Player Daemon') do options.mpd = true end opts.on('-r', '--remote remote', 'mpd remote host, in format of :') do |remote| parts = remote.split(':') if parts.length != 2 puts opts exit end options.remote_host = parts[0] options.remote_port = parts[1].to_i end opts.on('-c', '--channel channel', 'which channel to play', 'if not provided, channel 0 will be selected but who knows what it is') do |channel| options.channel = channel.to_i end opts.on('-k', '--kbps kbps', 'set kbps', 'if not provided, 64kbps will be used as default', 'for non-pro user this option simply does not work') do |kbps| options.kbps = kbps.to_i end opts.on('-l', '--list', 'list all available channels') do options.list = true end opts.on('-f', '--favor [count]', 'list favorites songs') do |count| options.favor = true options.liked = count.to_i end opts.on('-i', '--interaction [port]', 'start an http server for interaction', 'if omit port, 3000 will be used by default', 'GET /channels to get all available channels', 'GET /now to get current playing channel', 'GET /channel/ to switch to channel of the specified id') do |port| options.interaction = true options.port = port end opts.on('-V', '--verbose', 'verbose mode') do options.verbose = true end opts.on('-v', '--version', 'show current version') do puts DoubanFM::VERSION exit end opts.on_tail('-h', '--help', 'show this message') do puts opts exit end end opts.parse! options end def main options = parse if not options.interaction.nil? and options.mpd.nil? puts 'only mpd mode supports interaction' exit end if options.verbose logger = DoubanFM::ConsoleLogger.new else logger = DoubanFM::DummyLogger.new end if options.list @douban_fm = DoubanFM::DoubanFM.new(logger) @douban_fm.fetch_channels @douban_fm.channels['channels'].sort_by { |i| i['channel_id'] }.each do |channel| channel_id = channel['channel_id'] puts "#{channel_id}.#{' ' * (4 - channel_id.to_s.length)}#{channel['name']}" end exit end if options.channel.nil? options.channel = 0 end if options.kbps.nil? options.kbps = 64 end if options.email.nil? @douban_fm = DoubanFM::DoubanFM.new(logger) logger.log('play anonymous playlist') else if options.password.nil? or options.password.empty? require 'highline/import' options.password = ask("Enter password: ") { |q| q.echo = false } end @douban_fm = DoubanFM::DoubanFM.new(logger, options.email, options.password) @douban_fm.login logger.log("login as user [#{options.email}]") end if options.favor @douban_fm.fetch_liked_songs(options.liked) @douban_fm.liked_songs['songs'].each_with_index do |song, index| puts "#{index}#{' '*(4 - index.to_s.length)}#{song['title']} - #{song['artist']}" end exit end @douban_fm.select_channel(options.channel) logger.log("select channel #{options.channel}") @douban_fm.set_kbps(options.kbps) logger.log("set kbps #{options.kbps}") if options.mpd.nil? play_proc = proc do |waiting| if waiting begin logger.log('fetch next playlist') @douban_fm.fetch_next_playlist rescue logger.log('session expired, relogin') @douban_fm.login logger.log('fetch next playlist') @douban_fm.fetch_next_playlist end logger.log('play current playlist') @douban_fm.play do |waiting| play_proc.call(waiting) end end end play_proc.call(true) else unless options.interaction.nil? require 'webrick' require 'webrick/httpstatus' require 'json' servletClass = Class.new(WEBrick::HTTPServlet::AbstractServlet) do class << self def remote_host=(host) @@remote_host = host end def remote_port=(port) @@remote_port = port end def douban_fm=(fm) @@douban_fm = fm end end @@remote_host = nil @@remote_port = nil @@douban_fm = nil def do_GET request, response path = request.path[1..-1].split('/') case path[0] when 'channels' @@douban_fm.fetch_channels response.body = JSON.generate(@@douban_fm.channels) raise WEBrick::HTTPStatus::OK when 'channel' unless path[1].nil? # there is a race but not a big deal @@douban_fm.select_channel(path[1].to_i) @@douban_fm.clear_mpd_playlist(@@remote_host, @@remote_port) raise WEBrick::HTTPStatus::OK else response.body = JSON.generate({'channel' => @@douban_fm.current_channel}) end when 'kbps' unless path[1].nil? # there is a race but not a big deal @@douban_fm.set_kbps(path[1].to_i) @@douban_fm.clear_mpd_playlist(@@remote_host, @@remote_port) raise WEBrick::HTTPStatus::OK else response.body = JSON.generate({'kbps' => @@douban_fm.kbps}) end when 'index', nil html_path = File.expand_path('../../index.html', __FILE__) response.body = File.open(html_path) { |f| f.read } when 'static' html_path = File.expand_path("../../#{request.path[1..-1]}", __FILE__) response.body = File.open(html_path) { |f| f.read } else raise WEBrick::HTTPStatus::NotFound end end end servletClass.remote_host = options.remote_host servletClass.remote_port = options.remote_port servletClass.douban_fm = @douban_fm Thread.new do port = options.port || 3000 server = WEBrick::HTTPServer.new(:Port => port, :DoNotReverseLookup => true) %w[INT TERM].each do |signal| trap(signal) do server.shutdown Thread.main.kill # die hard end end server.mount("/", servletClass) server.start end end # there are a lot more chances to get stack overflow in this mode, # so can't use the proc way count = 0 while true if not @douban_fm.add_to_mpd(options.remote_host, options.remote_port) count += 1 logger.log("playlist not updated, wait for 10 seconds, total waiting time #{count * 10} seconds") if count == 360 logger.log('playlist has been updated for around one hour, go clear it') @douban_fm.clear_mpd_playlist(options.remote_host, options.remote_port) count = 0 end else count = 0 end sleep 10 end end end end DoubanFMCLI.new.main sleep