mumbletune 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,148 @@
1
+ require 'uri'
2
+ require 'meta-spotify'
3
+ require 'text'
4
+
5
+
6
+ module Mumbletune
7
+ def self.resolve(argument)
8
+ Resolvers.workers.each do |r|
9
+ if r.matches?(argument)
10
+ return r.resolve(argument)
11
+ end
12
+ end
13
+ return false
14
+ end
15
+
16
+ module Resolvers
17
+ class << self
18
+ attr_accessor :workers
19
+ end
20
+
21
+ @workers = []
22
+
23
+ class Resolver
24
+ def matches?(arg); end
25
+ def resolve(arg); end
26
+ def self.inherited(subcl)
27
+ Resolvers.workers.push(subcl.new)
28
+ end
29
+ end
30
+
31
+ class SpotifyURIResolver < Resolver
32
+ def matches?(uri)
33
+ http_uris = URI.extract(uri, ['http', 'https'])
34
+ sp_uris = URI.extract(uri, 'spotify')
35
+ if http_uris.any?
36
+ parsed_uri = URI.parse(http_uris.join)
37
+ if parsed_uri.hostname =~ /(?:open|play)\.spotify\.com/i
38
+ true
39
+ else
40
+ false
41
+ end
42
+ elsif sp_uris.any?
43
+ true
44
+ else
45
+ false
46
+ end
47
+ end
48
+ def resolve(uri)
49
+ raise "Not a Spotify URI." unless matches?(uri)
50
+ regexp = /(?<type>track|artist|album)[\/|:](?<id>\w+)/i
51
+ matched = regexp.match(uri)
52
+ type = matched[:type]
53
+ id = matched[:id]
54
+ sp_uri = "spotify:#{type}:#{id}"
55
+
56
+ # behave according to URI type
57
+ case type
58
+ when "track" # Return this track
59
+ SpotifyTrack::track_from_uri(sp_uri)
60
+ when "album" # Return all tracks of the album to queue
61
+ SpotifyTrack::tracks_from_album(sp_uri)
62
+ when "artist" # Return 10 tracks for this artist
63
+ SpotifyTrack::tracks_from_artist(sp_uri)
64
+ end
65
+ end
66
+ end
67
+
68
+ class SpotifySearchResolver < Resolver
69
+ def matches?(query)
70
+ # basically we will search for anything that's not a URL
71
+ if URI.extract(query).any?
72
+ return false
73
+ else
74
+ return true
75
+ end
76
+ end
77
+ def resolve(query)
78
+ first_word = query.split.first
79
+
80
+ # if first word is a type to search for, it needs to be stripped
81
+ # from the query so we don't search for it (e.g. "track starships")
82
+ if first_word =~ /^(artist|album|track)$/i
83
+ query_a = query.split
84
+ query_a.delete_at 0
85
+ query = query_a.join(" ")
86
+ end
87
+
88
+ # used to check if tracks are playable in in region
89
+ region = Mumbletune.config["spotify"]["region"]
90
+
91
+ # determine result based on a type in the first word
92
+ if first_word =~ /^artist$/i
93
+ artist = Mumbletune.handle_sp_error { MetaSpotify::Artist.search(query) }
94
+ result = artist[:artists].first
95
+
96
+ elsif first_word =~ /^album$/i
97
+ album = Mumbletune.handle_sp_error { MetaSpotify::Album.search(query) }
98
+ album[:albums].select! { |a| a.available_territories.include? region }
99
+ result = album[:albums].first
100
+
101
+ elsif first_word =~ /^track$/i
102
+ track = Mumbletune.handle_sp_error { MetaSpotify::Track.search(query) }
103
+ track[:tracks].select! { |t| t.album.available_territories.include? region }
104
+ result = track[:tracks].first
105
+
106
+ else # determine intended result by similarity to the query
107
+ artist = Mumbletune.handle_sp_error { MetaSpotify::Artist.search(query) }
108
+ album = Mumbletune.handle_sp_error { MetaSpotify::Album.search(query) }
109
+ track = Mumbletune.handle_sp_error { MetaSpotify::Track.search(query) }
110
+
111
+ # searches now finished
112
+
113
+ # remove anything out-of-region
114
+ album[:albums].select! { |a| a.available_territories.include? region } if album[:albums].any?
115
+ track[:tracks].select! { |t| t.album.available_territories.include? region } if track[:tracks].any?
116
+
117
+ compare = []
118
+ compare.push track[:tracks].first if track[:tracks].any?
119
+ compare.push album[:albums].first if album[:albums].any?
120
+ compare.push artist[:artists].first if artist[:artists].any?
121
+
122
+ white = Text::WhiteSimilarity.new
123
+ compare.sort! do |a, b|
124
+ a_sim = white.similarity(query, a.name)
125
+ b_sim = white.similarity(query, b.name)
126
+ if a_sim > b_sim
127
+ -1
128
+ elsif b_sim > a_sim
129
+ 1
130
+ else
131
+ 0
132
+ end
133
+ end
134
+ result = compare.first
135
+ end
136
+
137
+ if result.class == MetaSpotify::Artist
138
+ SpotifyTrack.tracks_from_artist(result)
139
+ elsif result.class == MetaSpotify::Album
140
+ SpotifyTrack.tracks_from_album(result)
141
+ elsif result.class == MetaSpotify::Track
142
+ SpotifyTrack.track_from_uri(result)
143
+ end
144
+
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,42 @@
1
+ require 'sinatra/base'
2
+
3
+ module Mumbletune
4
+
5
+ module SPURIServer
6
+
7
+ class Server < Sinatra::Base
8
+ set :run, true
9
+ set :server, :thin
10
+ set :logging, false
11
+ set :app_file, __FILE__
12
+ set :bind, 'localhost'
13
+ set :port, 8081
14
+
15
+ get '/play/:uri' do
16
+ cred = Mumbletune.config['spotify']
17
+ sp = Mumbletune::Spotify.new(cred['username'], cred['password'])
18
+
19
+ track = sp.objectFromURI(params[:uri])
20
+ halt 404, "Could not find a track with that URI." if track == nil
21
+
22
+ url = track.getFileURL().to_s
23
+ halt 404, "Could not find a track URL for that URI." if track == nil
24
+
25
+ sp.logout
26
+ redirect url, 303
27
+ end
28
+ end
29
+
30
+ def self.url_for(uri)
31
+ bind = Server::bind
32
+ port = Server::port
33
+ "http://#{bind}:#{port}/play/#{uri}"
34
+ end
35
+
36
+ def self.sp_uri_for(url)
37
+ regexp = /(.+)(<sp_uri>spotify:\w+:\w+)/i
38
+ matched = regexp.match(url)
39
+ matched[:sp_uri]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ require 'uri'
2
+
3
+ module Mumbletune
4
+
5
+ class SpotifyTrack < Track
6
+ def initialize(params)
7
+ super
8
+
9
+ @uri = params[:uri]
10
+ @url = SPURIServer.url_for(@uri)
11
+ end
12
+
13
+ def self.track_from_uri(track)
14
+ track = MetaSpotify::Track.lookup(track) unless track.class == MetaSpotify::Track
15
+
16
+ # force track to be playable within region
17
+ unless track.album.available_territories.include? Mumbletune.config["spotify"]["region"]
18
+ raise "#{track.name}: Not available in this region."
19
+ end
20
+
21
+ song = SpotifyTrack.new({
22
+ :name => track.name,
23
+ :artist => track.artists.first.name,
24
+ :album => track.album.name,
25
+ :uri => track.uri
26
+ })
27
+
28
+ # Technically, a collection of one.
29
+ Collection.new(
30
+ :TRACK,
31
+ song,
32
+ "<b>#{song.name}</b> by <b>#{song.artist}</b>"
33
+ )
34
+ end
35
+
36
+ def self.tracks_from_album(album_ref)
37
+ album_uri = album_ref.uri if album_ref.class == MetaSpotify::Album
38
+ album = MetaSpotify::Album.lookup(album_uri, {:extras => "track"})
39
+
40
+ # force album to be playable in region
41
+ unless album.available_territories.include? Mumbletune.config["spotify"]["region"]
42
+ raise "#{album.name}: Not available in this region."
43
+ end
44
+
45
+ tracks = []
46
+ album.tracks.each do |track|
47
+ tracks.push SpotifyTrack.new({
48
+ :name => track.name,
49
+ :artist => track.artists.first.name,
50
+ :album => album.name,
51
+ :uri => track.uri
52
+ })
53
+ end
54
+
55
+ Collection.new(
56
+ :ALBUM,
57
+ tracks,
58
+ "the album <b>#{album.name}</b> by <b>#{album.artists.first.name}</b>"
59
+ )
60
+ end
61
+
62
+ def self.tracks_from_artist(artist)
63
+ artist = MetaSpotify::Artist.lookup(artist) unless artist.class == MetaSpotify::Artist
64
+
65
+ # spotify metadata api still error-prone
66
+ search_result = Mumbletune.handle_sp_error { MetaSpotify::Track.search("artist:\"#{artist.name}\"") }
67
+
68
+ # filter out tracks outside region
69
+ search_result[:tracks].select! do |track|
70
+ track.album.available_territories.include? Mumbletune.config["spotify"]["region"]
71
+ end
72
+
73
+ tracks = []
74
+ search_result[:tracks][0...10].each do |track|
75
+ tracks.push SpotifyTrack.new({
76
+ :name => track.name,
77
+ :artist => track.artists.first.name,
78
+ :album => track.album.name,
79
+ :uri => track.uri
80
+ })
81
+ end
82
+
83
+ Collection.new(
84
+ :ARTIST_TOP,
85
+ tracks,
86
+ "#{tracks.length} tracks by <b>#{artist.name}</b>"
87
+ )
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,100 @@
1
+ {{#unknown}}
2
+ <p>
3
+ I've got literally no idea what "{{command}}" means. Try one of these commands:
4
+ </p>
5
+ {{/unknown}}
6
+
7
+ <table>
8
+ <thead>
9
+ <tr class="header">
10
+ <th align="left">
11
+ Command
12
+ </th>
13
+ <th align="left">
14
+ Action
15
+ </th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <tr class="odd">
20
+ <td align="left">
21
+ play &lt;track&gt;
22
+ </td>
23
+ <td align="left">
24
+ Queue this song.
25
+ </td>
26
+ </tr>
27
+ <tr class="even">
28
+ <td align="left">
29
+ play &lt;artist&gt;
30
+ </td>
31
+ <td align="left">
32
+ Queue 10 tracks by this artist.
33
+ </td>
34
+ </tr>
35
+ <tr class="odd">
36
+ <td align="left">
37
+ play &lt;album&gt;
38
+ </td>
39
+ <td align="left">
40
+ Queue this entire album.
41
+ </td>
42
+ </tr>
43
+ <tr class="even">
44
+ <td align="left">
45
+ play &lt;something&gt; now
46
+ </td>
47
+ <td align="left">
48
+ Play this shit right now!
49
+ </td>
50
+ </tr>
51
+ <tr class="odd">
52
+ <td align="left">
53
+ what
54
+ </td>
55
+ <td align="left">
56
+ Gets what is playing, and what is queued.
57
+ </td>
58
+ </tr>
59
+ <tr class="even">
60
+ <td align="left">
61
+ next
62
+ </td>
63
+ <td align="left">
64
+ Jump to the next song in the queue.
65
+ </td>
66
+ </tr>
67
+ <tr class="odd">
68
+ <td align="left">
69
+ clear
70
+ </td>
71
+ <td align="left">
72
+ Clears the current queue.
73
+ </td>
74
+ </tr>
75
+ <tr class="even">
76
+ <td align="left">
77
+ volume?
78
+ </td>
79
+ <td align="left">
80
+ Get the current volume level.
81
+ </td>
82
+ </tr>
83
+ <tr class="odd">
84
+ <td align="left">
85
+ volume [0-100]
86
+ </td>
87
+ <td align="left">
88
+ Set the volume.
89
+ </td>
90
+ </tr>
91
+ <tr class="even">
92
+ <td align="left">
93
+ wisdom
94
+ </td>
95
+ <td align="left">
96
+ Obtain it.
97
+ </td>
98
+ </tr>
99
+ </tbody>
100
+ </table>
@@ -0,0 +1,41 @@
1
+ {{#anything?}}
2
+ <table>
3
+ <col width="29%">
4
+ <col width="13%">
5
+ <col width="19%">
6
+ <thead>
7
+ <tr class="header">
8
+ <th align="left"></th>
9
+ <th align="left">
10
+ Track
11
+ </th>
12
+ <th align="left">
13
+ Artist
14
+ </th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ {{/anything?}}
19
+
20
+ {{#queue}}
21
+ <tr class="odd">
22
+ <td align="left">
23
+ {{#playing?}} &#9658; {{/playing?}}
24
+ </td>
25
+ <td align="left">
26
+ {{name}}
27
+ </td>
28
+ <td align="left">
29
+ {{artist}}
30
+ </td>
31
+ </tr>
32
+ {{/queue}}
33
+
34
+ {{#anything?}}
35
+ </tbody>
36
+ </table>
37
+ {{/anything?}}
38
+
39
+ {{^anything?}}
40
+ Nothing is playing. :c
41
+ {{/anything?}}
@@ -0,0 +1,32 @@
1
+ module Mumbletune
2
+ class Track
3
+ attr_accessor :name, :artist, :album, :url, :mpd_id, :queue_pos
4
+
5
+ class << self
6
+ attr_accessor :store
7
+ end
8
+ self.store = []
9
+
10
+ def initialize(params)
11
+ @name = params[:name]
12
+ @artist = params[:artist]
13
+ @album = params[:album]
14
+ @url = params[:url]
15
+
16
+ Track.store.push self
17
+ end
18
+
19
+ def playing?
20
+ if self == Mumbletune.player.current_song
21
+ true
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ def self.retreive_from_mpd_id(id)
28
+ Track.store.select { |t| t.mpd_id == id }.first
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Mumbletune
2
+ VERSION = "0.1.0"
3
+ end
data/lib/mumbletune.rb ADDED
@@ -0,0 +1,79 @@
1
+ require 'mumbletune/mumble_client'
2
+ require 'mumbletune/mpd_client'
3
+ require 'mumbletune/messages'
4
+ require 'mumbletune/track'
5
+ require 'mumbletune/collection'
6
+ require 'mumbletune/sp_uri_server'
7
+ require 'mumbletune/spotify_track'
8
+ require 'mumbletune/resolver'
9
+ require 'mumbletune/handle_sp_error'
10
+
11
+ require 'optparse'
12
+ require 'yaml'
13
+ require 'eventmachine'
14
+ require 'rubypython'
15
+
16
+ module Mumbletune
17
+ class << self
18
+ attr_reader :player, :mumble, :uri_server, :config
19
+ end
20
+
21
+ # parse command line options
22
+ config_file = nil
23
+ OptionParser.new do |opts|
24
+ opts.banner = "Usage: mumbletune.rb [options]"
25
+ opts.on("-c", "--config FILE", "=MANDATORY", "Path to configuration file") do |file|
26
+ config_file = file
27
+ end
28
+ opts.on("-h", "--help", "This help message") do
29
+ puts opts.help()
30
+ exit
31
+ end
32
+ end.parse!
33
+ raise OptionParser::MissingArgument unless config_file
34
+
35
+ # load configuration file
36
+ @config = YAML.load_file(config_file)
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
42
+
43
+ # open URI server
44
+ uri_thread = Thread.new do
45
+ SPURIServer::Server.run!
46
+ end
47
+
48
+ # initialize player
49
+ play_thread = Thread.new do
50
+ @player = Player.new
51
+ end
52
+
53
+ # connect to mumble & start streaming
54
+ mumble_thread = Thread.new do
55
+ @mumble = MumbleClient.new
56
+ @mumble.connect
57
+ @mumble.stream
58
+ end
59
+
60
+ # shutdown code
61
+ def self.shutdown
62
+ puts "\nGoodbye forever. Exiting..."
63
+ exit
64
+ end
65
+
66
+ # exit when Ctrl-C pressed
67
+ EventMachine.schedule do
68
+ trap("INT") { Mumbletune.shutdown }
69
+ end
70
+
71
+ # testing
72
+ # sleep 3
73
+ # @player.command_test_load
74
+ # @player.command_play
75
+
76
+ Thread.stop # wake up to shut down
77
+ self.shutdown
78
+
79
+ end
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mumbletune/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mumbletune"
8
+ spec.version = Mumbletune::VERSION
9
+ spec.authors = ["Elliott Williams"]
10
+ spec.email = ["e@elliottwillia.ms"]
11
+ spec.description = "Mumbletune connects to a mumble server and allows users to"\
12
+ " interact with and play a queue of music. Currently plays"\
13
+ " from Spotify alone."
14
+ spec.summary = "A mumble server bot that plays music"
15
+ spec.homepage = "http://github.com/elliottwilliams/mumbletune"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files`.split($/)
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+
26
+ spec.add_runtime_dependency "mumble-ruby"
27
+ spec.add_runtime_dependency "ruby-mpd"
28
+ spec.add_runtime_dependency "sinatra"
29
+ spec.add_runtime_dependency "meta-spotify"
30
+ spec.add_runtime_dependency "rubypython"
31
+
32
+ spec.add_runtime_dependency "thin"
33
+ spec.add_runtime_dependency "eventmachine"
34
+ spec.add_runtime_dependency "text"
35
+ spec.add_runtime_dependency "mustache"
36
+ end