LOLastfm 0.0.1 → 0.0.2

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 CHANGED
@@ -16,5 +16,4 @@ Gem::Specification.new {|s|
16
16
 
17
17
  s.add_dependency 'lastfm'
18
18
  s.add_dependency 'taglib-ruby'
19
- s.add_dependency 'call-me'
20
19
  }
data/README.md CHANGED
@@ -1,2 +1,51 @@
1
- LOLastfm - blablablabal
2
- =======================
1
+ LOLastfm - LOL a scrobbler
2
+ ==========================
3
+ LOLastfm is a modular and extensible last.fm scrobbler.
4
+
5
+ It supports a virtually infinite array of music players and has a cool server-client
6
+ interface that lets you manage music related features seamlessly.
7
+
8
+ For instance, you can ask it the current playing song without having to relay on client specific
9
+ shims, or by including the glyr extension you can ask it to give you the lyrics of the currently
10
+ playing song.
11
+
12
+ How to configure
13
+ ----------------
14
+ Before being able to use LOLastfm you have to allow access to it on last.fm, just run `LOLastfm -a`
15
+ and follow what it says, then create a file at `~/.LOLastfm/config` and put the session id it gave you
16
+ back.
17
+
18
+ You have to then tell LOLastfm what music player you want to be used, you can do it dynamically with
19
+ `LOLastfm-do --use player:path'.
20
+
21
+ ```ruby
22
+ session 'heregoesthesession'
23
+
24
+ use :moc
25
+ ```
26
+
27
+ The example above is if you're using moc.
28
+
29
+ You can also hook to certain events to, for example, change the attributes of a song before it's sent.
30
+
31
+ This allows for some neat features, like easily scrobbling from a radio, the following example shows
32
+ how to scrobble songs from trance.fm.
33
+
34
+ ```ruby
35
+ %w(listened love now_playing).each {|name|
36
+ on name do |song|
37
+ next if song.artist || !song.stream?
38
+
39
+ if song.comment && song.comment.include?('trance.fm')
40
+ next :stop if song.title.include?('trance.fm') || song.title.start_with?('Trance - FM-')
41
+
42
+ song.artist, song.title = song.title.split(/ - /, 2)
43
+ song.length = 60 * 1337
44
+
45
+ next
46
+ end
47
+
48
+ :stop
49
+ end
50
+ }
51
+ ```
data/bin/LOLastfm CHANGED
@@ -24,7 +24,7 @@ OptionParser.new do |o|
24
24
  end.parse!
25
25
 
26
26
  EM.run {
27
- d = LOLastfm.load(ARGV.first || '~/.LOLastfm.rc')
27
+ d = LOLastfm.load(ARGV.first || '~/.LOLastfm/config')
28
28
  d.start
29
29
 
30
30
  %w[INT KILL].each {|sig|
data/bin/LOLastfm-cmus CHANGED
@@ -4,7 +4,7 @@ require 'json'
4
4
 
5
5
  args = Hash[*ARGV]
6
6
 
7
- UNIXSocket.new(File.expand_path('~/.LOLastfm.socket')).tap {|socket|
7
+ UNIXSocket.new(File.expand_path('~/.LOLastfm/socket')).tap {|socket|
8
8
  if args['url']
9
9
  socket.puts [:hint, [:stream, args['title'], args['comment']]].to_json
10
10
  end
@@ -0,0 +1,22 @@
1
+ #! /usr/bin/env ruby
2
+ require 'json'
3
+ require 'socket'
4
+
5
+ socket = UNIXSocket.new(File.expand_path(ARGV.empty? ? '~/.LOLastfm/socket' : ARGV.first))
6
+ socket.puts [:commands, :glyr].to_json
7
+
8
+ if STDOUT.tty?
9
+ require 'ncursesw'
10
+
11
+ abort 'ncurses UI not implemented yet, redirect the output to something'
12
+ else
13
+ socket.puts [:lyrics?].to_json
14
+
15
+ response = JSON.parse(socket.readline)
16
+
17
+ if response.first
18
+ puts response.first['data']
19
+ else
20
+ exit 1
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ #! /usr/bin/env ruby
2
+ require 'json'
3
+ require 'socket'
4
+
5
+ socket = UNIXSocket.new(File.expand_path(ARGV.empty? ? '~/.LOLastfm/socket' : ARGV.first))
6
+ socket.puts [:now_playing?].to_json
7
+
8
+ response = JSON.parse(?[ + socket.readline + ?]).first
9
+
10
+ exit 1 if response.nil?
11
+
12
+ %w[track title artist album].each {|name|
13
+ puts "#{name} #{response[name]}" if response[name]
14
+ }
@@ -6,7 +6,7 @@ require 'json'
6
6
  options = {}
7
7
 
8
8
  OptionParser.new do |o|
9
- options[:socket] = '~/.LOLastfm.socket'
9
+ options[:socket] = '~/.LOLastfm/socket'
10
10
  options[:song] = {}
11
11
 
12
12
  o.on '--listened', 'enable listened sending' do
@@ -48,6 +48,14 @@ OptionParser.new do |o|
48
48
  o.on '-i', '--id ID', 'the MusicBrainz id of the song' do |value|
49
49
  options[:song][:id] = value
50
50
  end
51
+
52
+ o.on '-u', '--use USE', 'the string for the checker' do |value|
53
+ options[:use] = value
54
+ end
55
+
56
+ o.on '-u', '--commands COMMANDS', 'the string for the command library' do |value|
57
+ options[:commands] = value
58
+ end
51
59
  end.parse!
52
60
 
53
61
  if ARGV.first
@@ -56,12 +64,18 @@ end
56
64
 
57
65
  socket = UNIXSocket.new(File.expand_path(options[:socket]))
58
66
 
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
67
+ if options[:use]
68
+ socket.puts [:use, options[:use].split(':')].to_json
69
+ elsif options[:commands]
70
+ socket.puts [:commands, options[:commands].split(':')].to_json
63
71
  else
64
- socket.puts [:listened, options[:song]].to_json
72
+ if options[:now_playing]
73
+ socket.puts [:now_playing, options[:song]].to_json
74
+ elsif options[:love]
75
+ socket.puts [:love, options[:song].empty? ? nil : options[:song]].to_json
76
+ else
77
+ socket.puts [:listened, options[:song]].to_json
78
+ end
65
79
  end
66
80
 
67
81
  socket.flush
data/lib/LOLastfm.rb CHANGED
@@ -10,7 +10,6 @@
10
10
 
11
11
  require 'eventmachine'
12
12
  require 'lastfm'
13
- require 'call-me/memoize'
14
13
 
15
14
  class LOLastfm
16
15
  def self.load (path)
@@ -33,31 +32,45 @@ class LOLastfm
33
32
  commands[name.to_sym.downcase] = block
34
33
  end
35
34
 
36
- attr_reader :cache
35
+ attr_reader :path, :host, :port, :cache
37
36
 
38
37
  def initialize
39
38
  @session = Lastfm.new('5f7b134ba19b20536a5e29bc86ae64c9', '3b50e74d989795c3f4b3667c5a1c8e67')
40
39
  @cache = Cache.new(self)
41
40
  @events = Hash.new { |h, k| h[k] = [] }
42
41
 
43
- cache_at '~/.LOLastfm.cache'
42
+ cache_at '~/.LOLastfm/cache'
44
43
  end
45
44
 
46
45
  def load (path)
47
46
  instance_eval File.read(File.expand_path(path)), path
48
47
  end
49
48
 
49
+ def started?; !!@started; end
50
+
50
51
  def start
51
- @server = EM.start_unix_domain_server(@socket || File.expand_path('~/.LOLastfm.socket'), LOLastfm::Connection) {|conn|
52
- conn.fm = self
53
- }
52
+ return if started?
53
+
54
+ @server = if @host && @port
55
+ EM.start_server(host, port, LOLastfm::Connection) {|conn|
56
+ conn.fm = self
57
+ }
58
+ else
59
+ EM.start_unix_domain_server(path || File.expand_path('~/.LOLastfm/socket'), LOLastfm::Connection) {|conn|
60
+ conn.fm = self
61
+ }
62
+ end
54
63
 
55
64
  @timer = EM.add_periodic_timer 360 do
56
65
  save
57
66
  end
67
+
68
+ @checker.start
58
69
  end
59
70
 
60
71
  def stop
72
+ return unless started?
73
+
61
74
  EM.stop_server @server
62
75
  EM.cancel_timer @timer
63
76
 
@@ -82,8 +95,13 @@ class LOLastfm
82
95
  end
83
96
  end
84
97
 
85
- def socket (path)
86
- @socket = path
98
+ def server_at (host, port = nil)
99
+ if port
100
+ @host = host
101
+ @port = port
102
+ else
103
+ @path = host
104
+ end
87
105
  end
88
106
 
89
107
  def session (key)
@@ -96,6 +114,7 @@ class LOLastfm
96
114
 
97
115
  def now_playing (song)
98
116
  song = Song.new(song) unless song.is_a?(Song)
117
+ song = song.dup
99
118
 
100
119
  return false unless fire :now_playing, song
101
120
 
@@ -112,6 +131,7 @@ class LOLastfm
112
131
 
113
132
  def listened (song)
114
133
  song = Song.new(song) unless song.is_a?(Song)
134
+ song = song.dup
115
135
 
116
136
  return false unless fire :listened, song
117
137
 
@@ -138,6 +158,7 @@ class LOLastfm
138
158
  def love (song = nil)
139
159
  song = @last_played or return unless song
140
160
  song = Song.new(song) unless song.is_a? Song
161
+ song = song.dup
141
162
 
142
163
  return false unless fire :love, song
143
164
 
@@ -168,16 +189,26 @@ class LOLastfm
168
189
  @last_played
169
190
  end
170
191
 
171
- def use (*args, &block)
172
- if args.first.is_a?(String)
173
- return require args.first
192
+ def commands (name)
193
+ begin
194
+ require "LOLastfm/commands/#{name}"
195
+ rescue LoadError
196
+ require name.to_s
174
197
  end
198
+ end
175
199
 
176
- unless block
200
+ def use (*args, &block)
201
+ return if args.empty? && !block
202
+
203
+ unless args.first.respond_to?(:to_hash) || block
177
204
  name = args.shift.to_sym
178
205
 
179
- if args.first.is_a?(String)
180
- require args.shift
206
+ if args.first.is_a? String
207
+ require args.pop
208
+ elsif !self.class.checkers[name]
209
+ begin
210
+ require "LOLastfm/checkers/#{name}"
211
+ rescue LoadError; end
181
212
  end
182
213
 
183
214
  block = self.class.checkers[name]
@@ -187,9 +218,9 @@ class LOLastfm
187
218
  @checker.stop
188
219
  end
189
220
 
190
- @checker = Checker.new(self, name, args.shift, &block)
191
- @checker.start
192
- @checker
221
+ @checker = Checker.new(self, name, args.shift, &block).tap {|c|
222
+ c.start if started?
223
+ }
193
224
  end
194
225
 
195
226
  def hint (*args)
@@ -62,17 +62,21 @@ class Checker
62
62
  end
63
63
 
64
64
  def stop (&block)
65
- @timers.each {|timer|
66
- clear_timeout(timer)
67
- }
68
-
69
65
  if block
70
66
  @stop_block = block
71
67
  else
68
+ @stopped = true
69
+
70
+ @timers.each {|timer|
71
+ clear_timeout(timer)
72
+ }
73
+
72
74
  @stop_block.call if @stop_block
73
75
  end
74
76
  end
75
77
 
78
+ def stopped?; !!@stopped; end
79
+
76
80
  def hint (*args, &block)
77
81
  if block
78
82
  @hint_block = block
@@ -11,7 +11,6 @@
11
11
  require 'cmus'
12
12
 
13
13
  class Cmus::Controller::Status
14
- memoize
15
14
  def to_song
16
15
  return unless song
17
16
 
@@ -33,26 +32,37 @@ LOLastfm.define_checker :cmus do
33
32
  settings.default[:every] = 5
34
33
  settings.default[:timeout] = 0.005
35
34
 
36
- @cmus = Cmus::Controller.new(settings[:socket], settings[:timeout])
37
- @last = @cmus.status
35
+ if @cmus = Cmus::Controller.new(settings[:socket], settings[:timeout]) rescue false
36
+ @last = @cmus.status
38
37
 
39
- if @last == :play
40
- now_playing @last.to_song
38
+ if @last == :play
39
+ now_playing @last.to_song
40
+ end
41
41
  end
42
42
 
43
43
  set_interval settings[:every] do
44
+ unless @cmus
45
+ @cmus = Cmus::Controller.new(settings[:socket], settings[:timeout]) rescue next
46
+ @last = @cmus.status
47
+ end
48
+
49
+ unless status = @cmus.status rescue nil
50
+ @cmus = nil
51
+ next
52
+ end
53
+
44
54
  status = @cmus.status
45
55
 
46
56
  if status == :stop
47
- if @last != :stop && LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration)
57
+ if @last != :stop && (@last.to_song.stream? || LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration))
48
58
  listened @last.to_song
49
- else
50
- stopped_playing!
51
59
  end
60
+
61
+ stopped_playing!
52
62
  elsif status == :pause
53
63
  stopped_playing!
54
64
  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)
65
+ if @last != :stop && (@last.to_song != status.to_song || @last.song.position > status.song.position + 30) && (@last.to_song.stream? || LOLastfm::Song.is_scrobblable?(@last.song.position, @last.song.duration))
56
66
  listened @last.to_song
57
67
  end
58
68
 
@@ -10,23 +10,25 @@
10
10
 
11
11
  require 'moc'
12
12
 
13
- class Moc::Controller::Status
14
- memoize
13
+ class Moc::Controller::Status::Live
15
14
  def to_song
16
15
  return unless song
17
16
 
18
- if song.file.start_with?('http://') || song.file.start_with?('ftp://')
19
- comment = song.file
17
+ file = song.file
18
+ tags = song.tags
19
+
20
+ if file.start_with?('http://') || file.start_with?('ftp://')
21
+ comment = file
20
22
  else
21
- path = song.file
23
+ path = file
22
24
  end
23
25
 
24
26
  LOLastfm::Song.new(true,
25
- track: song.track,
26
- title: song.title,
27
- artist: song.artist,
28
- album: song.album,
29
- length: song.duration,
27
+ track: tags.track,
28
+ title: tags.title,
29
+ artist: tags.artist,
30
+ album: tags.album,
31
+ length: tags.time,
30
32
  comment: comment,
31
33
  path: path,
32
34
  stream: !!comment
@@ -38,34 +40,58 @@ LOLastfm.define_checker :moc do
38
40
  settings.default[:socket] = '~/.moc/socket2'
39
41
  settings.default[:every] = 5
40
42
 
41
- @moc = Moc::Controller.new(settings[:socket])
42
- @last = @moc.status
43
+ create = proc {
44
+ unless moc = Moc::Controller.new(settings[:socket]) rescue false
45
+ set_timeout settings[:every], &create unless stopped?
46
+ end
43
47
 
44
- if @last == :play
45
- now_playing @last.to_song
46
- end
48
+ Thread.new {
49
+ song, position = nil
47
50
 
48
- set_interval settings[:every] do
49
- status = @moc.status
51
+ begin
52
+ moc.loop {|e|
53
+ if e == :audio_stop
54
+ next unless song
50
55
 
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
56
+ if song.stream?
57
+ listened song
58
+ else
59
+ if LOLastfm::Song.is_scrobblable?(position, song.length)
60
+ listened song
61
+ else
62
+ stopped_playing!
63
+ end
64
+ end
65
+
66
+ song = nil
67
+ elsif e == :tags
68
+ elsif e == :audio_start
69
+ now_playing song = moc.status(true).to_song
70
+ elsif e == :state && moc.status(true) == :paused
71
+ stopped_playing!
72
+ elsif e == :ctime
73
+ unless song
74
+ now_playing song = moc.status(true).to_song
75
+ end
63
76
 
64
- if @last != :play || @last.to_song != status.to_song
65
- now_playing status.to_song
77
+ if song.stream?
78
+ if moc.status(true).song.title != song.title
79
+ listened song
80
+ now_playing song = moc.status(true).to_song
81
+ end
82
+ else
83
+ position = moc.status(true).song.position
84
+ end
85
+ end
86
+ }
87
+ rescue Exception => e
88
+ $stderr.puts e.message
89
+ $stderr.puts e.backtrace
66
90
  end
67
- end
68
91
 
69
- @last = status
70
- end
92
+ set_timeout settings[:every], &create unless stopped?
93
+ }
94
+ }
95
+
96
+ create.call
71
97
  end
@@ -9,19 +9,34 @@
9
9
  #++
10
10
 
11
11
  require 'glyr'
12
+ require 'tmpdir'
13
+ require 'fileutils'
12
14
 
13
- class Glyr::Result::Data
14
- def to_json
15
- { source: source, provider: provider, data: data }.to_json
15
+ module Glyr
16
+ class Source
17
+ def to_hash
18
+ { name: name, quality: quality, speed: speed, language_aware: language_aware? }
19
+ end
16
20
  end
21
+
22
+ class Result
23
+ def to_hash
24
+ { source: source, url: url, data: data }
25
+ end
26
+ end
27
+
28
+ File.join(Dir.tmpdir, rand.to_s).tap {|path|
29
+ FileUtils.mkpath(path)
30
+ cache_at path
31
+ }
17
32
  end
18
33
 
19
34
  LOLastfm.define_command :lyrics? do
20
35
  song = now_playing?
21
36
 
22
37
  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
38
+ Glyr.query(title: song.title, artist: song.artist, album: song.album).lyrics
39
+ }, -> results {
40
+ send_line results.map(&:to_hash).to_json
26
41
  }
27
42
  end
@@ -16,6 +16,10 @@ define_command :use do |*args|
16
16
  use *args
17
17
  end
18
18
 
19
+ define_command :commands do |*args|
20
+ commands *args
21
+ end
22
+
19
23
  define_command :listened do |song|
20
24
  listened song
21
25
  end
@@ -55,7 +59,7 @@ class Connection < EventMachine::Protocols::LineAndTextProtocol
55
59
  command, arguments = JSON.parse(line)
56
60
 
57
61
  if block = LOLastfm.commands[command.to_sym.downcase]
58
- instance_eval *arguments, &block
62
+ instance_exec *arguments, &block
59
63
  end
60
64
  rescue => e
61
65
  $stderr.puts e.to_s
data/lib/LOLastfm/song.rb CHANGED
@@ -17,6 +17,8 @@ class LOLastfm
17
17
 
18
18
  class Song
19
19
  def self.is_scrobblable? (position, duration)
20
+ return false unless position && duration
21
+
20
22
  return false if duration < 30
21
23
 
22
24
  return true if position > 240
@@ -10,6 +10,6 @@
10
10
 
11
11
  class LOLastfm
12
12
  def self.version
13
- '0.0.1'
13
+ '0.0.2'
14
14
  end
15
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: LOLastfm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-23 00:00:00.000000000 Z
12
+ date: 2012-06-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: lastfm
@@ -43,28 +43,14 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
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
46
  description:
63
47
  email: meh@paranoici.org
64
48
  executables:
65
49
  - LOLastfm
66
50
  - LOLastfm-cmus
67
- - LOLastfm-do
51
+ - LOLastfm-lyrics
52
+ - LOLastfm-now-playing
53
+ - LOLastfm-send
68
54
  extensions: []
69
55
  extra_rdoc_files: []
70
56
  files:
@@ -72,7 +58,9 @@ files:
72
58
  - README.md
73
59
  - bin/LOLastfm
74
60
  - bin/LOLastfm-cmus
75
- - bin/LOLastfm-do
61
+ - bin/LOLastfm-lyrics
62
+ - bin/LOLastfm-now-playing
63
+ - bin/LOLastfm-send
76
64
  - lib/LOLastfm.rb
77
65
  - lib/LOLastfm/cache.rb
78
66
  - lib/LOLastfm/checker.rb