mumbletune 0.1.3 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -18,3 +18,6 @@ tmp
18
18
  .DS_Store
19
19
  conf.yml
20
20
  conf.yaml
21
+
22
+ spotify_appkey.key
23
+ tmp/
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in mumbletune.gemspec
4
4
  gemspec
5
+
6
+ gem "mumble-ruby", :github => "elliottwilliams/mumble-ruby", :branch => "master"
7
+ gem "hallon-queue-output", :github => "elliottwilliams/hallon-queue-output", :branch => "master"
@@ -1,74 +1,67 @@
1
+ GIT
2
+ remote: git://github.com/elliottwilliams/hallon-queue-output.git
3
+ revision: 19883c7b06a8b2bb7256bbef2579e715f359ab9e
4
+ branch: master
5
+ specs:
6
+ hallon-queue-output (0.0.2)
7
+
8
+ GIT
9
+ remote: git://github.com/elliottwilliams/mumble-ruby.git
10
+ revision: 8a557f55d2bace410e9ca046469e7ab7344a51de
11
+ branch: master
12
+ specs:
13
+ mumble-ruby (0.0.3)
14
+ activesupport
15
+ celt-ruby
16
+ ruby-libsamplerate
17
+ ruby_protobuf
18
+
1
19
  PATH
2
20
  remote: .
3
21
  specs:
4
- mumbletune (0.1.2)
5
- eventmachine
6
- meta-spotify
22
+ mumbletune (0.2.0)
23
+ daemons
24
+ ffi (~> 1.3.0)
25
+ hallon
7
26
  mumble-ruby
8
27
  mustache
9
- ruby-mpd
10
- rubypython
11
- sinatra
12
28
  text
13
- thin
14
29
 
15
30
  GEM
16
31
  remote: https://rubygems.org/
17
32
  specs:
18
- activesupport (3.2.12)
19
- i18n (~> 0.6)
33
+ activesupport (3.2.13)
34
+ i18n (= 0.6.1)
20
35
  multi_json (~> 1.0)
21
- blankslate (3.1.2)
22
36
  celt-ruby (0.0.1)
23
37
  ffi
24
- columnize (0.3.6)
25
38
  daemons (1.1.9)
26
- debugger (1.5.0)
27
- columnize (>= 0.3.1)
28
- debugger-linecache (~> 1.2.0)
29
- debugger-ruby_core_source (~> 1.2.0)
30
- debugger-linecache (1.2.0)
31
- debugger-ruby_core_source (1.2.0)
32
- eventmachine (1.0.3)
33
- ffi (1.4.0)
34
- httparty (0.10.2)
35
- multi_json (~> 1.0)
36
- multi_xml (>= 0.5.2)
37
- i18n (0.6.4)
38
- meta-spotify (0.3.0)
39
- httparty (> 0.8)
40
- multi_json (1.6.1)
41
- multi_xml (0.5.3)
42
- mumble-ruby (0.0.3)
43
- activesupport
44
- celt-ruby
45
- ruby_protobuf
39
+ ffi (1.3.1)
40
+ hallon (0.18.1)
41
+ spotify (~> 12.3.0)
42
+ weak_observable (~> 1.0)
43
+ i18n (0.6.1)
44
+ libspotify (12.1.51.3)
45
+ multi_json (1.7.2)
46
46
  mustache (0.99.4)
47
- rack (1.5.2)
48
- rack-protection (1.5.0)
49
- rack
50
- rake (10.0.3)
51
- ruby-mpd (0.2.1)
47
+ rake (10.0.4)
48
+ ref (1.0.4)
49
+ ruby-libsamplerate (0.0.1)
50
+ ffi
52
51
  ruby_protobuf (0.4.11)
53
- rubypython (0.6.3)
54
- blankslate (>= 2.1.2.3)
55
- ffi (>= 1.0.7)
56
- sinatra (1.4.1)
57
- rack (~> 1.5, >= 1.5.2)
58
- rack-protection (~> 1.4)
59
- tilt (~> 1.3, >= 1.3.4)
52
+ spotify (12.3.0)
53
+ ffi (~> 1.0, >= 1.0.11)
54
+ libspotify (~> 12.1.51)
60
55
  text (1.2.1)
61
- thin (1.5.0)
62
- daemons (>= 1.0.9)
63
- eventmachine (>= 0.12.6)
64
- rack (>= 1.0.0)
65
- tilt (1.3.5)
56
+ weak_observable (1.0.1)
57
+ ref (~> 1.0.0)
66
58
 
67
59
  PLATFORMS
68
60
  ruby
69
61
 
70
62
  DEPENDENCIES
71
63
  bundler (~> 1.3)
72
- debugger
64
+ hallon-queue-output!
65
+ mumble-ruby!
73
66
  mumbletune!
74
67
  rake
data/README.md CHANGED
@@ -1,23 +1,24 @@
1
1
  # Mumbletune
2
2
 
3
- MUMBLETUNE is an amiable bot that connects to a mumble server and allows users to interact with and play a queue of music. It currently plays music through Spotify.
3
+ MUMBLETUNE is a nice bot that connects to a mumble server and allows users to listen to Spotify together.
4
4
 
5
5
  ## Installation
6
6
 
7
- First, install Hexxeh's [spotify-websocket-api](https://github.com/Hexxeh/spotify-websocket-api).
7
+ What you need:
8
+ - A Spotify Premium account. I wish it didn't have to be this way, but for API access, Premium's required
9
+ - A Spotify App Key. Spotify will issue you one [here][1].
8
10
 
9
- git clone git://github.com/Hexxeh/spotify-websocket-api.git
10
- cd spotify-websocket-api
11
- python setup.py build
12
- python setup.py install
11
+ Clone this repo and install its dependencies
13
12
 
14
- Then, install the gem:
15
-
16
- $ gem install mumbletune
13
+ $ git clone git://github.com/elliottwilliams/mumbletune.git
14
+ cd mumbletune
15
+ bundle install
17
16
 
18
17
  ## Usage
19
18
 
20
19
  1. Create a configuration file. See `conf.example.yaml` for help.
21
20
  2. Start Mumbletune, passing your config with `-c`.
22
21
 
23
- $ mumbletune -c config_file.yaml
22
+ $ mumbletune -c config_file.yaml
23
+
24
+ [1]: https://developer.spotify.com/technologies/libspotify/keys/
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "daemons"
4
+ require "optparse"
5
+
6
+ dir = File.expand_path File.dirname(__FILE__)
7
+ log = File.expand_path File.join(dir, "../logs")
8
+
9
+ # change -c paramater to full path (survives daemon)
10
+ ARGV.each_index do |i|
11
+ if ARGV[i] == "-c"
12
+ ARGV[i+1] = File.expand_path File.join(Dir.pwd, ARGV[i+1])
13
+ end
14
+ end
15
+
16
+ options = {
17
+ app_name: "mumbletune",
18
+ dir_mode: :normal,
19
+ dir: log,
20
+ log_output: true
21
+ }
22
+
23
+ Daemons.run(File.join(dir, "mumbletune"), options)
@@ -1,4 +1,5 @@
1
1
  spotify:
2
+ appkey: "/path/to/spotify_appkey.key"
2
3
  username: "sp_username"
3
4
  password: "sp_password"
4
5
  region: "us"
@@ -8,8 +9,6 @@ mumble:
8
9
  username: "mumbletune"
9
10
  password: "server_password"
10
11
  channel: "Music Room"
11
- mpd:
12
- host: "localhost"
13
- port: 6600
14
- fifo_out: "/tmp/mumble.fifo"
12
+ player:
15
13
  default_volume: 60
14
+ tracks_for_artist: 5
@@ -16,9 +16,12 @@
16
16
  <br>
17
17
  For example:
18
18
 
19
- play coldplay
20
- play nicki minaj starships
19
+ `play coldplay` plays the top 5 tracks by Coldplay.
21
20
 
22
- Mumbletune uses Spotify to find music.
21
+ `play nicki minaj starships` will play the (excellent) song Starships.
22
+
23
+ Mumbletune uses Spotify to find and play music. You can `play` a Spotify URL directly instead of searching, like
24
+
25
+ `play spotify:album:1HjSyGjmLNjRAKgT9t1cna`
23
26
 
24
27
  <!-- Generate HTML with: $ pandoc -f markdown -->
@@ -1,81 +1,86 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'mumbletune/version'
1
4
  require 'mumbletune/mumble_client'
2
- require 'mumbletune/mpd_client'
5
+ require 'mumbletune/hallon_player'
3
6
  require 'mumbletune/messages'
4
- require 'mumbletune/track'
5
7
  require 'mumbletune/collection'
6
- require 'mumbletune/sp_uri_server'
7
- require 'mumbletune/spotify_track'
8
8
  require 'mumbletune/resolver'
9
- require 'mumbletune/handle_sp_error'
9
+ require 'mumbletune/spotify_resolver'
10
10
 
11
11
  require 'optparse'
12
12
  require 'yaml'
13
- require 'eventmachine'
14
- require 'rubypython'
15
13
 
16
14
  module Mumbletune
17
15
  class << self
18
- attr_reader :player, :mumble, :uri_server, :config
16
+ attr_reader :player, :mumble, :uri_server, :config, :verbose
19
17
  end
20
18
 
19
+ # defaults
20
+ @verbose = false
21
+ config_file = nil
22
+
21
23
  # parse command line options
22
- config_file = nil
23
- OptionParser.new do |opts|
24
+ opts = OptionParser.new do |opts|
24
25
  opts.banner = "Usage: mumbletune.rb [options]"
25
26
  opts.on("-c", "--config FILE", "=MANDATORY", "Path to configuration file") do |file|
26
27
  config_file = file
27
28
  end
29
+ opts.on("-v", "--verbose", "Verbose output") do
30
+ @verbose = true
31
+ end
28
32
  opts.on("-h", "--help", "This help message") do
29
- puts opts.help()
33
+ puts opts.help
30
34
  exit
31
35
  end
32
- end.parse!
33
- raise OptionParser::MissingArgument unless config_file
34
-
35
- # load configuration file
36
- @config = YAML.load_file(config_file)
36
+ end
37
37
 
38
- # load spotify-websocket-api
39
- puts ">> Loading Spotify APIs..."
40
- RubyPython.start(:python_exe => 'python2.7')
41
- Spotify = RubyPython.import('spotify_web.friendly').Spotify
38
+ opts.parse!
42
39
 
43
- # open URI server
44
- uri_thread = Thread.new do
45
- SPURIServer::Server.run!
40
+ unless config_file
41
+ puts opts.help
42
+ exit
46
43
  end
47
44
 
45
+ # load configuration file
46
+ @config = YAML.load_file(config_file)
47
+
48
48
  # initialize player
49
49
  play_thread = Thread.new do
50
- @player = Player.new
50
+ @player = HallonPlayer.new
51
+ @player.connect
52
+ puts ">> Connected to Spotify."
51
53
  end
52
54
 
53
55
  # connect to mumble & start streaming
56
+ sleep 0.1 until @player && @player.ready
54
57
  mumble_thread = Thread.new do
55
58
  @mumble = MumbleClient.new
56
59
  @mumble.connect
60
+ puts ">> Connected to Mumble server at #{self.config['mumble']['host']}."
57
61
  @mumble.stream
58
62
  end
59
63
 
60
64
  # shutdown code
61
65
  def self.shutdown
66
+ Thread.new do
67
+ sleep 5 # timeout
68
+ puts "Timeout. Forcing exit."
69
+ exit!
70
+ end
71
+ print "\n>> Exiting... "
62
72
  self.mumble.disconnect
73
+ print "Disconnected from Mumble... "
63
74
  self.player.disconnect
64
- puts "\nGoodbye forever. Exiting..."
75
+ print "Disconnected from Spotify... "
76
+ puts "\nGoodbye forever."
65
77
  exit
66
78
  end
67
79
 
68
80
  # exit when Ctrl-C pressed
69
- EventMachine.schedule do
70
- trap("INT") { Mumbletune.shutdown }
81
+ Signal.trap("INT") do
82
+ Mumbletune.shutdown
71
83
  end
72
84
 
73
- # testing
74
- # sleep 3
75
- # @player.command_test_load
76
- # @player.command_play
77
-
78
- Thread.stop # wake up to shut down
79
- self.shutdown
80
-
85
+ Thread.stop # we're done here
81
86
  end
@@ -1,17 +1,37 @@
1
+ require 'forwardable'
2
+
1
3
  module Mumbletune
2
4
  class Collection
3
- attr_accessor :type, :tracks, :description, :user
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ attr_accessor :type, :description, :user, :tracks, :history, :current_track
9
+
10
+ def_delegators :@tracks, :length, :first, :last, :each, :any?, :empty?
4
11
 
5
12
  def initialize(type, tracks, description)
6
13
  @type = type
7
14
  @description = description
15
+ @tracks = [tracks].flatten
16
+ @history = Array.new
17
+ end
8
18
 
9
- if tracks.class == Array
10
- @tracks = tracks
11
- else
12
- @tracks = [tracks]
19
+ def next
20
+ if @current_track
21
+ @history << @current_track
22
+ @tracks.delete @current_track
13
23
  end
24
+ @current_track = @tracks.first
14
25
  end
15
26
 
27
+ def empty?
28
+ without_current = @tracks.dup
29
+ without_current.delete_if { |t| t == @current_track }
30
+ without_current.empty?
31
+ end
32
+
33
+ def any?
34
+ !empty?
35
+ end
16
36
  end
17
37
  end
@@ -0,0 +1,162 @@
1
+ require "hallon"
2
+ require "hallon-queue-output"
3
+ require "thread"
4
+
5
+ module Mumbletune
6
+
7
+ class HallonPlayer
8
+
9
+ attr_accessor :ready, :play_history, :add_history, :queue, :current_track, :audio_queue
10
+
11
+ def initialize
12
+ conf = Mumbletune.config
13
+
14
+ @play_history = Array.new
15
+ @add_history = Array.new
16
+ @queue = Array.new
17
+ @prev_id = 0
18
+ @ready = false
19
+ @audio_queue = Queue.new
20
+
21
+ @ready = true
22
+ end
23
+
24
+ def connect
25
+ conf = Mumbletune.config
26
+
27
+ @session = Hallon::Session.initialize(IO.read(conf["spotify"]["appkey"]))
28
+ @session.login!(conf["spotify"]["username"], conf["spotify"]["password"])
29
+
30
+ @player = Hallon::Player.new(Hallon::QueueOutput) do
31
+ # Set driver options
32
+ @driver.queue = Mumbletune.player.audio_queue
33
+ @driver.verbose = true if Mumbletune.verbose
34
+
35
+ # Define callbacks
36
+ on(:end_of_track) { Mumbletune.player.next }
37
+ on(:streaming_error) { |s, e| raise "Spotify connection error: #{e}" }
38
+ on(:play_token_lost) { Mumbletune.player.play_token_lost }
39
+ end
40
+
41
+ @ready = true
42
+
43
+ start_event_loop
44
+ end
45
+
46
+ def disconnect
47
+ @logging_out = true
48
+ @event_loop_thread.kill
49
+ @session.logout!
50
+ end
51
+
52
+ # Status methods
53
+ def playing?
54
+ @player.status == :playing
55
+ end
56
+
57
+ def paused?
58
+ @player.status == :paused
59
+ end
60
+
61
+ def stopped?
62
+ @player.status == :stopped
63
+ end
64
+
65
+ def any?
66
+ cols_with_more = @queue.map { |col| col.any? }
67
+ cols_with_more.delete_if { |m| m == false }
68
+ cols_with_more.any?
69
+ end
70
+
71
+ def empty?
72
+ !any?
73
+ end
74
+
75
+ # Queue Control
76
+ def add_collection(col, now=false)
77
+ only_track = empty?
78
+
79
+ # add to the queue
80
+ if now
81
+ @queue.unshift col
82
+ else
83
+ @queue.push col
84
+ end
85
+
86
+ # record in additions history
87
+ @add_history.push col
88
+
89
+ # play it, if this is the first track or if the user specified `now`
90
+ self.next if now || only_track
91
+ end
92
+
93
+ def undo
94
+ removed = @add_history.pop
95
+ @queue.delete_if { |col| col == removed }
96
+ self.next if removed.current_track
97
+ removed
98
+ end
99
+
100
+ def clear_queue
101
+ @queue.clear
102
+ self.stop
103
+ end
104
+
105
+ # Playback Commands
106
+ def play
107
+ if stopped?
108
+ self.next
109
+ elsif paused?
110
+ @player.play
111
+ end
112
+ end
113
+
114
+ def pause
115
+ if playing?
116
+ @player.pause
117
+ elsif paused?
118
+ @player.play
119
+ end
120
+ end
121
+
122
+ def next
123
+ # move the collection to play_history if it has played all its tracks
124
+ @play_history << @queue.shift if @queue.first && @queue.first.empty?
125
+
126
+ return stop unless any?
127
+
128
+ track = @queue.first.next
129
+
130
+ return stop unless track
131
+
132
+ # play that shit!
133
+ @current_track = track
134
+ @player.play track
135
+
136
+ puts "\u266B #{track.name} - #{track.artist.name}" unless Mumbletune.verbose
137
+ end
138
+
139
+ def stop
140
+ @player.stop
141
+ end
142
+
143
+ # Callback Handling
144
+ def play_token_lost
145
+ Mumbletune.mumble.broadcast %w{Mumbletune was just paused because this
146
+ Spotify account is being used elsewhere. Type <code>unpause
147
+ </code> to regain control and keep playing.} * " "
148
+ end
149
+
150
+ private
151
+
152
+ def start_event_loop
153
+ @event_loop_thread = Thread.new do
154
+ loop do
155
+ @session.process_events unless @session.disconnected?
156
+ sleep 1
157
+ end
158
+ end
159
+ end
160
+
161
+ end
162
+ end