LOLastfm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LOLastfm.gemspec +20 -0
- data/README.md +2 -0
- data/bin/LOLastfm +39 -0
- data/bin/LOLastfm-cmus +14 -0
- data/bin/LOLastfm-do +68 -0
- data/lib/LOLastfm.rb +232 -0
- data/lib/LOLastfm/cache.rb +72 -0
- data/lib/LOLastfm/checker.rb +103 -0
- data/lib/LOLastfm/checkers/cmus.rb +80 -0
- data/lib/LOLastfm/checkers/moc.rb +71 -0
- data/lib/LOLastfm/checkers/process.rb +34 -0
- data/lib/LOLastfm/commands/glyr.rb +27 -0
- data/lib/LOLastfm/connection.rb +84 -0
- data/lib/LOLastfm/song.rb +143 -0
- data/lib/LOLastfm/version.rb +15 -0
- metadata +110 -0
data/LOLastfm.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Kernel.load 'lib/LOLastfm/version.rb'
|
2
|
+
|
3
|
+
Gem::Specification.new {|s|
|
4
|
+
s.name = 'LOLastfm'
|
5
|
+
s.version = LOLastfm.version
|
6
|
+
s.author = 'meh.'
|
7
|
+
s.email = 'meh@paranoici.org'
|
8
|
+
s.homepage = 'http://github.com/meh/LOLastfm'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = 'LOL a scrobbler.'
|
11
|
+
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.require_paths = ['lib']
|
16
|
+
|
17
|
+
s.add_dependency 'lastfm'
|
18
|
+
s.add_dependency 'taglib-ruby'
|
19
|
+
s.add_dependency 'call-me'
|
20
|
+
}
|
data/README.md
ADDED
data/bin/LOLastfm
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'LOLastfm'
|
4
|
+
|
5
|
+
OptionParser.new do |o|
|
6
|
+
o.on '-a', '--auth', 'authorize LOLastfm to work' do
|
7
|
+
lastfm = Lastfm.new('5f7b134ba19b20536a5e29bc86ae64c9', '3b50e74d989795c3f4b3667c5a1c8e67')
|
8
|
+
token = lastfm.auth.get_token
|
9
|
+
|
10
|
+
puts "Go to the following URL, allow LOLastfm and press Enter to continue..."
|
11
|
+
puts ""
|
12
|
+
puts " http://www.last.fm/api/auth/?api_key=5f7b134ba19b20536a5e29bc86ae64c9&token=#{token}"
|
13
|
+
|
14
|
+
gets
|
15
|
+
|
16
|
+
puts "Use #{lastfm.auth.get_session(token)['key']} as session in your LOLastfm configuration."
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
|
20
|
+
o.on '-v', '--version', 'show version and exit' do
|
21
|
+
puts "LOLastfm #{LOLastfm.version}"
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
end.parse!
|
25
|
+
|
26
|
+
EM.run {
|
27
|
+
d = LOLastfm.load(ARGV.first || '~/.LOLastfm.rc')
|
28
|
+
d.start
|
29
|
+
|
30
|
+
%w[INT KILL].each {|sig|
|
31
|
+
trap sig do
|
32
|
+
puts 'LOLastfm stopping...'
|
33
|
+
|
34
|
+
d.stop
|
35
|
+
|
36
|
+
EM.stop_event_loop
|
37
|
+
end
|
38
|
+
}
|
39
|
+
}
|
data/bin/LOLastfm-cmus
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
require 'socket'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
args = Hash[*ARGV]
|
6
|
+
|
7
|
+
UNIXSocket.new(File.expand_path('~/.LOLastfm.socket')).tap {|socket|
|
8
|
+
if args['url']
|
9
|
+
socket.puts [:hint, [:stream, args['title'], args['comment']]].to_json
|
10
|
+
end
|
11
|
+
|
12
|
+
socket.flush
|
13
|
+
socket.close
|
14
|
+
}
|
data/bin/LOLastfm-do
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'socket'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
|
8
|
+
OptionParser.new do |o|
|
9
|
+
options[:socket] = '~/.LOLastfm.socket'
|
10
|
+
options[:song] = {}
|
11
|
+
|
12
|
+
o.on '--listened', 'enable listened sending' do
|
13
|
+
options[:listened] = true
|
14
|
+
end
|
15
|
+
|
16
|
+
o.on '--now-playing', 'enable now playing sending' do
|
17
|
+
options[:now_playing] = true
|
18
|
+
end
|
19
|
+
|
20
|
+
o.on '--love', 'enable love sending' do
|
21
|
+
options[:love] = true
|
22
|
+
end
|
23
|
+
|
24
|
+
o.on '-t', '--title TITLE', 'title of the song' do |value|
|
25
|
+
options[:song][:title] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
o.on '-a', '--artist ARTIST', 'the artist of the song' do |value|
|
29
|
+
options[:song][:artist] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
o.on '-A', '--album ALBUM', 'the album of the song' do |value|
|
33
|
+
options[:song][:album] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on '-l', '--length LENGTH', Integer, 'the length of the song in seconds' do |value|
|
37
|
+
options[:song][:length] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
o.on '-L', '--listened-at DATE', 'the time when the song has started playing' do |value|
|
41
|
+
options[:song][:listened_at] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on '-p', '--path PATH', 'the path of the song' do |value|
|
45
|
+
options[:song][:path] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
o.on '-i', '--id ID', 'the MusicBrainz id of the song' do |value|
|
49
|
+
options[:song][:id] = value
|
50
|
+
end
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
if ARGV.first
|
54
|
+
options[:socket] = ARGV.first
|
55
|
+
end
|
56
|
+
|
57
|
+
socket = UNIXSocket.new(File.expand_path(options[:socket]))
|
58
|
+
|
59
|
+
if options[:now_playing]
|
60
|
+
socket.puts [:now_playing, options[:song]].to_json
|
61
|
+
elsif options[:love]
|
62
|
+
socket.puts [:love, options[:song].empty? ? nil : options[:song]].to_json
|
63
|
+
else
|
64
|
+
socket.puts [:listened, options[:song]].to_json
|
65
|
+
end
|
66
|
+
|
67
|
+
socket.flush
|
68
|
+
socket.close
|
data/lib/LOLastfm.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'eventmachine'
|
12
|
+
require 'lastfm'
|
13
|
+
require 'call-me/memoize'
|
14
|
+
|
15
|
+
class LOLastfm
|
16
|
+
def self.load (path)
|
17
|
+
new.tap { |o| o.load(path) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.checkers
|
21
|
+
@checkers ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.define_checker (name, &block)
|
25
|
+
checkers[name.to_sym.downcase] = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.commands
|
29
|
+
@commands ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.define_command (name, &block)
|
33
|
+
commands[name.to_sym.downcase] = block
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :cache
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@session = Lastfm.new('5f7b134ba19b20536a5e29bc86ae64c9', '3b50e74d989795c3f4b3667c5a1c8e67')
|
40
|
+
@cache = Cache.new(self)
|
41
|
+
@events = Hash.new { |h, k| h[k] = [] }
|
42
|
+
|
43
|
+
cache_at '~/.LOLastfm.cache'
|
44
|
+
end
|
45
|
+
|
46
|
+
def load (path)
|
47
|
+
instance_eval File.read(File.expand_path(path)), path
|
48
|
+
end
|
49
|
+
|
50
|
+
def start
|
51
|
+
@server = EM.start_unix_domain_server(@socket || File.expand_path('~/.LOLastfm.socket'), LOLastfm::Connection) {|conn|
|
52
|
+
conn.fm = self
|
53
|
+
}
|
54
|
+
|
55
|
+
@timer = EM.add_periodic_timer 360 do
|
56
|
+
save
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
EM.stop_server @server
|
62
|
+
EM.cancel_timer @timer
|
63
|
+
|
64
|
+
@checker.stop if @checker
|
65
|
+
ensure
|
66
|
+
save
|
67
|
+
end
|
68
|
+
|
69
|
+
def cache_at (path)
|
70
|
+
@cache_at = File.expand_path(path)
|
71
|
+
|
72
|
+
if File.exists? @cache_at
|
73
|
+
@cache.load(@cache_at)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def save
|
78
|
+
if @cache_at
|
79
|
+
File.open(@cache_at, 'w') {|f|
|
80
|
+
f.write(@cache.to_yaml)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def socket (path)
|
86
|
+
@socket = path
|
87
|
+
end
|
88
|
+
|
89
|
+
def session (key)
|
90
|
+
@session.session = key
|
91
|
+
end
|
92
|
+
|
93
|
+
def is_authenticated?
|
94
|
+
!!@session.session
|
95
|
+
end
|
96
|
+
|
97
|
+
def now_playing (song)
|
98
|
+
song = Song.new(song) unless song.is_a?(Song)
|
99
|
+
|
100
|
+
return false unless fire :now_playing, song
|
101
|
+
|
102
|
+
@now_playing = nil
|
103
|
+
|
104
|
+
return false if song.nil?
|
105
|
+
|
106
|
+
@now_playing = song
|
107
|
+
|
108
|
+
@session.track.update_now_playing(song.artist, song.title, song.album, song.track, song.id, song.length)
|
109
|
+
rescue
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
def listened (song)
|
114
|
+
song = Song.new(song) unless song.is_a?(Song)
|
115
|
+
|
116
|
+
return false unless fire :listened, song
|
117
|
+
|
118
|
+
@cache.flush!
|
119
|
+
@now_playing = nil
|
120
|
+
|
121
|
+
return false if song.nil?
|
122
|
+
|
123
|
+
unless listened! song
|
124
|
+
@cache.listened(song)
|
125
|
+
end
|
126
|
+
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def listened! (song)
|
131
|
+
@session.track.scrobble(song.artist, song.title, song.listened_at.to_time.to_i, song.album, song.track, song.id, song.length).tap {
|
132
|
+
@last_played = song
|
133
|
+
}
|
134
|
+
rescue
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
def love (song = nil)
|
139
|
+
song = @last_played or return unless song
|
140
|
+
song = Song.new(song) unless song.is_a? Song
|
141
|
+
|
142
|
+
return false unless fire :love, song
|
143
|
+
|
144
|
+
return false if song.nil?
|
145
|
+
|
146
|
+
unless love! song
|
147
|
+
@cache.love(song)
|
148
|
+
end
|
149
|
+
|
150
|
+
true
|
151
|
+
end
|
152
|
+
|
153
|
+
def love! (song)
|
154
|
+
@session.track.love(song.artist, song.title)
|
155
|
+
rescue
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
159
|
+
def stopped_playing!
|
160
|
+
@now_playing = nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def now_playing?
|
164
|
+
@now_playing
|
165
|
+
end
|
166
|
+
|
167
|
+
def last_played?
|
168
|
+
@last_played
|
169
|
+
end
|
170
|
+
|
171
|
+
def use (*args, &block)
|
172
|
+
if args.first.is_a?(String)
|
173
|
+
return require args.first
|
174
|
+
end
|
175
|
+
|
176
|
+
unless block
|
177
|
+
name = args.shift.to_sym
|
178
|
+
|
179
|
+
if args.first.is_a?(String)
|
180
|
+
require args.shift
|
181
|
+
end
|
182
|
+
|
183
|
+
block = self.class.checkers[name]
|
184
|
+
end
|
185
|
+
|
186
|
+
if @checker
|
187
|
+
@checker.stop
|
188
|
+
end
|
189
|
+
|
190
|
+
@checker = Checker.new(self, name, args.shift, &block)
|
191
|
+
@checker.start
|
192
|
+
@checker
|
193
|
+
end
|
194
|
+
|
195
|
+
def hint (*args)
|
196
|
+
return unless @checker
|
197
|
+
|
198
|
+
@checker.hint(*args)
|
199
|
+
end
|
200
|
+
|
201
|
+
def on (event, &block)
|
202
|
+
@events[event.to_sym] << block
|
203
|
+
end
|
204
|
+
|
205
|
+
def fire (event, *args)
|
206
|
+
delete = []
|
207
|
+
stopped = false
|
208
|
+
|
209
|
+
@events[event.to_sym].each {|block|
|
210
|
+
result = block.call(*args)
|
211
|
+
|
212
|
+
case result
|
213
|
+
when :delete
|
214
|
+
delete << event
|
215
|
+
|
216
|
+
when :stop
|
217
|
+
stopped = true
|
218
|
+
break
|
219
|
+
end
|
220
|
+
}
|
221
|
+
|
222
|
+
@events[event.to_sym] -= delete
|
223
|
+
|
224
|
+
return !stopped
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
require 'LOLastfm/version'
|
229
|
+
require 'LOLastfm/cache'
|
230
|
+
require 'LOLastfm/connection'
|
231
|
+
require 'LOLastfm/song'
|
232
|
+
require 'LOLastfm/checker'
|
@@ -0,0 +1,72 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'yaml'
|
12
|
+
|
13
|
+
class LOLastfm
|
14
|
+
|
15
|
+
class Cache
|
16
|
+
attr_reader :fm
|
17
|
+
|
18
|
+
def initialize (fm)
|
19
|
+
@fm = fm
|
20
|
+
|
21
|
+
@listened = []
|
22
|
+
@loved = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def listened (song)
|
26
|
+
return if @loved.member? song
|
27
|
+
|
28
|
+
@listened << song
|
29
|
+
end
|
30
|
+
|
31
|
+
def love (song)
|
32
|
+
return if @loved.member? song
|
33
|
+
|
34
|
+
@loved << song
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty?
|
38
|
+
@listened.empty? && @loved.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def flush!
|
42
|
+
until @listened.empty?
|
43
|
+
break unless fm.listened! @listened.first
|
44
|
+
|
45
|
+
@listened.shift
|
46
|
+
end
|
47
|
+
|
48
|
+
until @loved.empty?
|
49
|
+
break unless fm.love! @loved.first
|
50
|
+
|
51
|
+
@loved.shift
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load (path)
|
56
|
+
data = YAML.parse_file(path).transform
|
57
|
+
|
58
|
+
data['listened'].each {|song|
|
59
|
+
listened(Song.new(song))
|
60
|
+
}
|
61
|
+
|
62
|
+
data['loved'].each {|song|
|
63
|
+
love(Song.new(song))
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_yaml
|
68
|
+
{ 'listened' => @listened.map(&:to_hash), 'loved' => @loved.map(&:to_hash) }.to_yaml
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
class LOLastfm
|
12
|
+
|
13
|
+
class Checker
|
14
|
+
class Settings
|
15
|
+
attr_reader :default
|
16
|
+
|
17
|
+
def initialize (checker, data)
|
18
|
+
@data = data.to_hash
|
19
|
+
@default = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond_to_missing? (id)
|
23
|
+
@data.respond_to?(id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing (id, *args, &block)
|
27
|
+
if @default.respond_to? id
|
28
|
+
return @default.merge(@data).__send__ id, *args, &block
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :fm, :name, :settings
|
36
|
+
|
37
|
+
def initialize (fm, name, settings = nil, &block)
|
38
|
+
raise LocalJumpError, 'no block given' unless block
|
39
|
+
|
40
|
+
@fm = fm
|
41
|
+
@name = name
|
42
|
+
@settings = Settings.new(self, settings || {})
|
43
|
+
@block = block
|
44
|
+
|
45
|
+
@timers = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def respond_to_missing? (id)
|
49
|
+
@fm.respond_to?(id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing (id, *args, &block)
|
53
|
+
if @fm.respond_to? id
|
54
|
+
return @fm.__send__ id, *args, &block
|
55
|
+
end
|
56
|
+
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def start
|
61
|
+
instance_eval &@block
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop (&block)
|
65
|
+
@timers.each {|timer|
|
66
|
+
clear_timeout(timer)
|
67
|
+
}
|
68
|
+
|
69
|
+
if block
|
70
|
+
@stop_block = block
|
71
|
+
else
|
72
|
+
@stop_block.call if @stop_block
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def hint (*args, &block)
|
77
|
+
if block
|
78
|
+
@hint_block = block
|
79
|
+
else
|
80
|
+
@hint_block.call(*args) if @hint_block
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_timeout (*args, &block)
|
85
|
+
EM.schedule {
|
86
|
+
@timers << EM.add_timer(*args, &block)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_interval (*args, &block)
|
91
|
+
EM.schedule {
|
92
|
+
@timers << EM.add_periodic_timer(*args, &block)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def clear_timeout (what)
|
97
|
+
EM.schedule {
|
98
|
+
EM.cancel_timer(what)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'cmus'
|
12
|
+
|
13
|
+
class Cmus::Controller::Status
|
14
|
+
memoize
|
15
|
+
def to_song
|
16
|
+
return unless song
|
17
|
+
|
18
|
+
LOLastfm::Song.new(true,
|
19
|
+
track: song.track,
|
20
|
+
title: song.title,
|
21
|
+
artist: song.artist,
|
22
|
+
album: song.album,
|
23
|
+
length: song.duration,
|
24
|
+
comment: song.tags.comment,
|
25
|
+
path: song.file,
|
26
|
+
stream: !song.file
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
LOLastfm.define_checker :cmus do
|
32
|
+
settings.default[:socket] = '~/.cmus/socket'
|
33
|
+
settings.default[:every] = 5
|
34
|
+
settings.default[:timeout] = 0.005
|
35
|
+
|
36
|
+
@cmus = Cmus::Controller.new(settings[:socket], settings[:timeout])
|
37
|
+
@last = @cmus.status
|
38
|
+
|
39
|
+
if @last == :play
|
40
|
+
now_playing @last.to_song
|
41
|
+
end
|
42
|
+
|
43
|
+
set_interval settings[:every] do
|
44
|
+
status = @cmus.status
|
45
|
+
|
46
|
+
if status == :stop
|
47
|
+
if @last != :stop && LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration)
|
48
|
+
listened @last.to_song
|
49
|
+
else
|
50
|
+
stopped_playing!
|
51
|
+
end
|
52
|
+
elsif status == :pause
|
53
|
+
stopped_playing!
|
54
|
+
else
|
55
|
+
if @last != :stop && (@last.to_song != status.to_song || @last.song.position > status.song.position) && LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration)
|
56
|
+
listened @last.to_song
|
57
|
+
end
|
58
|
+
|
59
|
+
if @last != :play || @last.to_song != status.to_song
|
60
|
+
now_playing status.to_song
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
@last = status
|
65
|
+
end
|
66
|
+
|
67
|
+
hint do |type, *args|
|
68
|
+
if type == 'stream'
|
69
|
+
title, comment = args
|
70
|
+
|
71
|
+
if @hint && title != @hint
|
72
|
+
next unless listened title: @hint, comment: comment || @cmus.status.song.comment, stream: true
|
73
|
+
end
|
74
|
+
|
75
|
+
next unless now_playing title: title, comment: comment || @cmus.status.song.comment, stream: true
|
76
|
+
|
77
|
+
@hint = title
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'moc'
|
12
|
+
|
13
|
+
class Moc::Controller::Status
|
14
|
+
memoize
|
15
|
+
def to_song
|
16
|
+
return unless song
|
17
|
+
|
18
|
+
if song.file.start_with?('http://') || song.file.start_with?('ftp://')
|
19
|
+
comment = song.file
|
20
|
+
else
|
21
|
+
path = song.file
|
22
|
+
end
|
23
|
+
|
24
|
+
LOLastfm::Song.new(true,
|
25
|
+
track: song.track,
|
26
|
+
title: song.title,
|
27
|
+
artist: song.artist,
|
28
|
+
album: song.album,
|
29
|
+
length: song.duration,
|
30
|
+
comment: comment,
|
31
|
+
path: path,
|
32
|
+
stream: !!comment
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
LOLastfm.define_checker :moc do
|
38
|
+
settings.default[:socket] = '~/.moc/socket2'
|
39
|
+
settings.default[:every] = 5
|
40
|
+
|
41
|
+
@moc = Moc::Controller.new(settings[:socket])
|
42
|
+
@last = @moc.status
|
43
|
+
|
44
|
+
if @last == :play
|
45
|
+
now_playing @last.to_song
|
46
|
+
end
|
47
|
+
|
48
|
+
set_interval settings[:every] do
|
49
|
+
status = @moc.status
|
50
|
+
|
51
|
+
if status == :stop
|
52
|
+
if @last != :stop && LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration)
|
53
|
+
listened @last.to_song
|
54
|
+
else
|
55
|
+
stopped_playing!
|
56
|
+
end
|
57
|
+
elsif status == :pause
|
58
|
+
stopped_playing!
|
59
|
+
else
|
60
|
+
if @last != :stop && (@last.to_song != status.to_song || @last.song.position > status.song.position) && LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration)
|
61
|
+
listened @last.to_song
|
62
|
+
end
|
63
|
+
|
64
|
+
if @last != :play || @last.to_song != status.to_song
|
65
|
+
now_playing status.to_song
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@last = status
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
LOLastfm.define_checker :process do
|
12
|
+
settings.default[:every] = 5
|
13
|
+
|
14
|
+
set_interval settings[:every] do
|
15
|
+
if status == :stopped
|
16
|
+
if @last == :playing
|
17
|
+
|
18
|
+
end
|
19
|
+
else
|
20
|
+
end
|
21
|
+
|
22
|
+
@last = status
|
23
|
+
end
|
24
|
+
|
25
|
+
hint do |path|
|
26
|
+
if @hint && path != @hint
|
27
|
+
next unless listened path: path
|
28
|
+
end
|
29
|
+
|
30
|
+
next unless now_playing path: path
|
31
|
+
|
32
|
+
@hint = path
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'glyr'
|
12
|
+
|
13
|
+
class Glyr::Result::Data
|
14
|
+
def to_json
|
15
|
+
{ source: source, provider: provider, data: data }.to_json
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
LOLastfm.define_command :lyrics? do
|
20
|
+
song = now_playing?
|
21
|
+
|
22
|
+
EM.defer -> {
|
23
|
+
Glyr.query.title(song.title).artist(song.artist).album(song.album).lyrics
|
24
|
+
}, -> result {
|
25
|
+
send_line result.first.to_str.to_json
|
26
|
+
}
|
27
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'json'
|
12
|
+
|
13
|
+
class LOLastfm
|
14
|
+
|
15
|
+
define_command :use do |*args|
|
16
|
+
use *args
|
17
|
+
end
|
18
|
+
|
19
|
+
define_command :listened do |song|
|
20
|
+
listened song
|
21
|
+
end
|
22
|
+
|
23
|
+
define_command :now_playing do |song|
|
24
|
+
now_playing song
|
25
|
+
end
|
26
|
+
|
27
|
+
define_command :stopped_playing do
|
28
|
+
stopped_playing!
|
29
|
+
end
|
30
|
+
|
31
|
+
define_command :love do |song|
|
32
|
+
love song
|
33
|
+
end
|
34
|
+
|
35
|
+
define_command :hint do |*args|
|
36
|
+
hint *args
|
37
|
+
end
|
38
|
+
|
39
|
+
define_command :now_playing? do
|
40
|
+
send_line now_playing?.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
define_command :next? do
|
44
|
+
on :now_playing do |song|
|
45
|
+
send_line song.to_json
|
46
|
+
|
47
|
+
:delete
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Connection < EventMachine::Protocols::LineAndTextProtocol
|
52
|
+
attr_writer :fm
|
53
|
+
|
54
|
+
def receive_line (line)
|
55
|
+
command, arguments = JSON.parse(line)
|
56
|
+
|
57
|
+
if block = LOLastfm.commands[command.to_sym.downcase]
|
58
|
+
instance_eval *arguments, &block
|
59
|
+
end
|
60
|
+
rescue => e
|
61
|
+
$stderr.puts e.to_s
|
62
|
+
$stderr.puts e.backtrace
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_line (line)
|
66
|
+
raise ArgumentError, 'the line already has a newline character' if line.include? "\n"
|
67
|
+
|
68
|
+
send_data "#{line}\r\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
def respond_to_missing? (id)
|
72
|
+
@fm.respond_to?(id)
|
73
|
+
end
|
74
|
+
|
75
|
+
def method_missing (id, *args, &block)
|
76
|
+
if @fm.respond_to? id
|
77
|
+
return @fm.__send__ id, *args, &block
|
78
|
+
end
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'taglib'
|
12
|
+
require 'json'
|
13
|
+
require 'yaml'
|
14
|
+
require 'date'
|
15
|
+
|
16
|
+
class LOLastfm
|
17
|
+
|
18
|
+
class Song
|
19
|
+
def self.is_scrobblable? (position, duration)
|
20
|
+
return false if duration < 30
|
21
|
+
|
22
|
+
return true if position > 240
|
23
|
+
|
24
|
+
return true if position >= duration / 2
|
25
|
+
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :track, :title, :album, :artist, :comment, :length, :listened_at, :path, :id
|
30
|
+
|
31
|
+
def initialize (no_fill = false, data)
|
32
|
+
data = Hash[data.map { |key, value| [key.to_sym, value] }]
|
33
|
+
|
34
|
+
@track = data[:track] && data[:track].to_i
|
35
|
+
@title = data[:title]
|
36
|
+
@album = data[:album]
|
37
|
+
@artist = data[:artist]
|
38
|
+
@length = data[:length] && data[:length].to_i
|
39
|
+
@comment = data[:comment]
|
40
|
+
@listened_at = data[:listened_at]
|
41
|
+
@path = data[:path]
|
42
|
+
@id = data[:id]
|
43
|
+
|
44
|
+
fill! unless no_fill
|
45
|
+
|
46
|
+
if data[:stream] || (@length && @length < 0)
|
47
|
+
stream!
|
48
|
+
|
49
|
+
@length = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
if @listened_at
|
53
|
+
@listened_at = @listened_at.is_a?(String) ? DateTime.parse(@listened_at) : @listened_at.to_datetime
|
54
|
+
elsif @length
|
55
|
+
@listened_at = (Time.now - @length).to_datetime
|
56
|
+
else
|
57
|
+
@listened_at = DateTime.now
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def fill!
|
62
|
+
return unless @path
|
63
|
+
|
64
|
+
return if @track && @title && @album && @artist && @comment && @length && @id
|
65
|
+
|
66
|
+
TagLib::FileRef.open(@path) {|f|
|
67
|
+
if f.tag
|
68
|
+
@track = f.tag.track && f.tag.track.to_i unless @track
|
69
|
+
@title = f.tag.title unless @title
|
70
|
+
@album = f.tag.album unless @album
|
71
|
+
@artist = f.tag.artist unless @artist
|
72
|
+
@comment = f.tag.comment unless @comment
|
73
|
+
end
|
74
|
+
|
75
|
+
if f.audio_properties
|
76
|
+
@length = f.audio_properties.length.to_i unless @length
|
77
|
+
end
|
78
|
+
}
|
79
|
+
|
80
|
+
unless @id
|
81
|
+
TagLib::MPEG::File.new(@path).tap {|file|
|
82
|
+
next unless tag = file.id3v2_tag
|
83
|
+
|
84
|
+
if ufid = tag.frame_list('UFID').find { |u| u.owner == 'http://musicbrainz.org' }
|
85
|
+
@id = ufid.identifier
|
86
|
+
end
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
unless @id
|
91
|
+
TagLib::FLAC::File.new(@path).tap {|file|
|
92
|
+
next unless tag = file.xiph_comment
|
93
|
+
|
94
|
+
@id = tag.field_list_map['MUSICBRAINZ_TRACKID']
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
unless @id
|
99
|
+
TagLib::Ogg::Vorbis::File.new(@path).tap {|file|
|
100
|
+
next unless tag = file.tag
|
101
|
+
|
102
|
+
@id = tag.field_list_map['MUSICBRAINZ_TRACKID']
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def stream?; !!@stream; end
|
108
|
+
def stream!; @stream = true; end
|
109
|
+
|
110
|
+
def nil?
|
111
|
+
title.nil? || artist.nil?
|
112
|
+
end
|
113
|
+
|
114
|
+
def hash
|
115
|
+
[title, artist].hash
|
116
|
+
end
|
117
|
+
|
118
|
+
def == (other)
|
119
|
+
return true if super
|
120
|
+
|
121
|
+
title == other.title && artist == other.artist
|
122
|
+
end
|
123
|
+
|
124
|
+
alias eql? ==
|
125
|
+
|
126
|
+
def to_hash
|
127
|
+
{
|
128
|
+
track: track, title: title, album: album, artist: artist, comment: comment,
|
129
|
+
length: length, listened_at: listened_at, path: path, id: id,
|
130
|
+
stream: stream?
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_json
|
135
|
+
to_hash.to_json
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_yaml
|
139
|
+
to_hash.to_yaml
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
class LOLastfm
|
12
|
+
def self.version
|
13
|
+
'0.0.1'
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: LOLastfm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- meh.
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: lastfm
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: taglib-ruby
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: call-me
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description:
|
63
|
+
email: meh@paranoici.org
|
64
|
+
executables:
|
65
|
+
- LOLastfm
|
66
|
+
- LOLastfm-cmus
|
67
|
+
- LOLastfm-do
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- LOLastfm.gemspec
|
72
|
+
- README.md
|
73
|
+
- bin/LOLastfm
|
74
|
+
- bin/LOLastfm-cmus
|
75
|
+
- bin/LOLastfm-do
|
76
|
+
- lib/LOLastfm.rb
|
77
|
+
- lib/LOLastfm/cache.rb
|
78
|
+
- lib/LOLastfm/checker.rb
|
79
|
+
- lib/LOLastfm/checkers/cmus.rb
|
80
|
+
- lib/LOLastfm/checkers/moc.rb
|
81
|
+
- lib/LOLastfm/checkers/process.rb
|
82
|
+
- lib/LOLastfm/commands/glyr.rb
|
83
|
+
- lib/LOLastfm/connection.rb
|
84
|
+
- lib/LOLastfm/song.rb
|
85
|
+
- lib/LOLastfm/version.rb
|
86
|
+
homepage: http://github.com/meh/LOLastfm
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.24
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: LOL a scrobbler.
|
110
|
+
test_files: []
|