mumbletune 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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