LOLastfm 0.0.1
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.
- 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: []
|