lyricli 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/Gemfile.lock +63 -0
- data/README.md +69 -0
- data/bin/lrc +52 -0
- data/config/defaults.json +5 -0
- data/lib/lyricli/configuration.rb +90 -0
- data/lib/lyricli/exceptions/disable_source_error.rb +8 -0
- data/lib/lyricli/exceptions/enable_source_error.rb +8 -0
- data/lib/lyricli/exceptions/invalid_lyrics_error.rb +8 -0
- data/lib/lyricli/exceptions/lyrics_not_found_error.rb +8 -0
- data/lib/lyricli/exceptions/reset_source_error.rb +8 -0
- data/lib/lyricli/exceptions/source_configuration_error.rb +9 -0
- data/lib/lyricli/exceptions/start_source_error.rb +7 -0
- data/lib/lyricli/exceptions/unknown_source_error.rb +8 -0
- data/lib/lyricli/exceptions.rb +7 -0
- data/lib/lyricli/lyricli.rb +55 -0
- data/lib/lyricli/lyrics_engine.rb +41 -0
- data/lib/lyricli/source_manager.rb +136 -0
- data/lib/lyricli/sources/arguments.rb +39 -0
- data/lib/lyricli/sources/current_song.scpt +0 -0
- data/lib/lyricli/sources/itunes.rb +41 -0
- data/lib/lyricli/sources/rdio.rb +73 -0
- data/lib/lyricli/sources.rb +6 -0
- data/lib/lyricli/util.rb +36 -0
- data/lib/lyricli.rb +96 -0
- data/spec/bin/lrc_spec.rb +0 -0
- data/spec/lib/lyricli/configuration_spec.rb +0 -0
- data/spec/lib/lyricli/exceptions_spec.rb +0 -0
- data/spec/lib/lyricli/lyricli_spec.rb +0 -0
- data/spec/lib/lyricli/lyrics_engine_spec.rb +0 -0
- data/spec/lib/lyricli/source_manager_spec.rb +25 -0
- data/spec/lib/lyricli/sources/arguments.rb +0 -0
- data/spec/lib/lyricli/sources/itunes.rb +0 -0
- data/spec/lib/lyricli/sources/rdio.rb +0 -0
- data/spec/lib/lyricli/sources_spec.rb +0 -0
- data/spec/lib/lyricli/util_spec.rb +42 -0
- data/spec/lib/lyricli_spec.rb +0 -0
- metadata +212 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
lyricli (0.0.1)
|
5
|
+
launchy (~> 2.1.2)
|
6
|
+
multi_json (~> 1.3.6)
|
7
|
+
nokogiri (~> 1.5.5)
|
8
|
+
rdio (~> 0.1.0)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
addressable (2.3.2)
|
14
|
+
archive-tar-minitar (0.5.2)
|
15
|
+
columnize (0.3.6)
|
16
|
+
daemons (1.1.9)
|
17
|
+
diff-lcs (1.1.3)
|
18
|
+
eventmachine (1.0.0)
|
19
|
+
json (1.7.5)
|
20
|
+
launchy (2.1.2)
|
21
|
+
addressable (~> 2.3)
|
22
|
+
linecache19 (0.5.12)
|
23
|
+
ruby_core_source (>= 0.1.4)
|
24
|
+
multi_json (1.3.6)
|
25
|
+
nokogiri (1.5.5)
|
26
|
+
oauth (0.4.7)
|
27
|
+
rack (1.4.1)
|
28
|
+
rdio (0.1.0)
|
29
|
+
json
|
30
|
+
oauth (>= 0.3.0)
|
31
|
+
rspec (2.11.0)
|
32
|
+
rspec-core (~> 2.11.0)
|
33
|
+
rspec-expectations (~> 2.11.0)
|
34
|
+
rspec-mocks (~> 2.11.0)
|
35
|
+
rspec-core (2.11.1)
|
36
|
+
rspec-expectations (2.11.3)
|
37
|
+
diff-lcs (~> 1.1.3)
|
38
|
+
rspec-mocks (2.11.3)
|
39
|
+
ruby-debug-base19 (0.11.25)
|
40
|
+
columnize (>= 0.3.1)
|
41
|
+
linecache19 (>= 0.5.11)
|
42
|
+
ruby_core_source (>= 0.1.4)
|
43
|
+
ruby-debug19 (0.11.6)
|
44
|
+
columnize (>= 0.3.1)
|
45
|
+
linecache19 (>= 0.5.11)
|
46
|
+
ruby-debug-base19 (>= 0.11.19)
|
47
|
+
ruby_core_source (0.1.5)
|
48
|
+
archive-tar-minitar (>= 0.5.2)
|
49
|
+
thin (1.5.0)
|
50
|
+
daemons (>= 1.0.9)
|
51
|
+
eventmachine (>= 0.12.6)
|
52
|
+
rack (>= 1.0.0)
|
53
|
+
yard (0.8.2.1)
|
54
|
+
|
55
|
+
PLATFORMS
|
56
|
+
ruby
|
57
|
+
|
58
|
+
DEPENDENCIES
|
59
|
+
lyricli!
|
60
|
+
rspec (~> 2.11.0)
|
61
|
+
ruby-debug19 (~> 0.11.6)
|
62
|
+
thin (~> 1.5.0)
|
63
|
+
yard (~> 0.8.2.1)
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Lyricli #
|
2
|
+
## The command line client for lyrics ##
|
3
|
+
|
4
|
+
This is a quick introduction for Lyricli. Right now it's in really early
|
5
|
+
stages of development, so it's lacking in a lot of stuff (mainly tests
|
6
|
+
and documentation) ... But it generally works and here's a tutorial to
|
7
|
+
see how to get it working.
|
8
|
+
|
9
|
+
### Installing ###
|
10
|
+
|
11
|
+
1. Clone this Repo
|
12
|
+
2. `gem build lyricli.gemspec`
|
13
|
+
3. `gem install lyricli-0.0.1.gem`
|
14
|
+
4. Voila!
|
15
|
+
|
16
|
+
### Usage ###
|
17
|
+
|
18
|
+
Lyricli can be invoked with the command `lrc` and there are three basic
|
19
|
+
ways of using it:
|
20
|
+
|
21
|
+
`lrc`
|
22
|
+
|
23
|
+
When you run it without arguments, it will look in the available sources
|
24
|
+
to try to find a playing song and extract the lyrics.
|
25
|
+
|
26
|
+
`lrc artist song`
|
27
|
+
|
28
|
+
When you run it with arguments, it will use them to search for the
|
29
|
+
lyrics. This won't work if you manually disable the arguments source in
|
30
|
+
your configuration file.
|
31
|
+
|
32
|
+
#### Commands ####
|
33
|
+
|
34
|
+
The third way to use it is by passing it one of the following special
|
35
|
+
commands:
|
36
|
+
|
37
|
+
* `lrc -l` or `lrc --list-sources` lists the available sources.
|
38
|
+
* `lrc -e` or `lrc --enable SOURCE` enable a source from the list.
|
39
|
+
* `lrc -d` or `lrc --disable SOURCE` disable a source from the list.
|
40
|
+
* `lrc -r` or `lrc --reset SOURCE` reset all configuration for a source.
|
41
|
+
* `lrc -v` or `lrc --version` show the installed version of lyricli.
|
42
|
+
* `lrc -h` or `lrc --help` display some help
|
43
|
+
|
44
|
+
### Roadmap ###
|
45
|
+
|
46
|
+
There is not much defined right now as a roadmap, but this needs to be
|
47
|
+
done:
|
48
|
+
|
49
|
+
* Specs for all the components
|
50
|
+
* YARD documentation for all the components
|
51
|
+
|
52
|
+
And the first thing I want to work on after that is done is separating
|
53
|
+
the Lyrics Engines so we can add/remove lyrics engines in a similar way to how
|
54
|
+
we currently add/remove sources.
|
55
|
+
|
56
|
+
Also, I want to add the last song to the configuration, so you can check
|
57
|
+
that. This would let us "watch" lyricli without hammering the lyrics
|
58
|
+
wiki api
|
59
|
+
|
60
|
+
Also, during the enable phase, sources like iTunes should check for
|
61
|
+
proper OS and stop if they're not in their home turf.
|
62
|
+
|
63
|
+
### Leave Feedback Please! ###
|
64
|
+
|
65
|
+
If you decide to use or hack away at Lyricly, please don't forget to
|
66
|
+
post any issues you find.
|
67
|
+
|
68
|
+
### License ###
|
69
|
+
Licensed under 3-clause-BSD.
|
data/bin/lrc
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'lyricli'
|
5
|
+
|
6
|
+
|
7
|
+
options = {}
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = %{Usage:
|
10
|
+
lrc [options]
|
11
|
+
lrc artist song
|
12
|
+
lrc You must enable other sources for this
|
13
|
+
|
14
|
+
Options:
|
15
|
+
}
|
16
|
+
|
17
|
+
opts.on("-e", "--enable SOURCE", "Enable SOURCE") do |source|
|
18
|
+
Lyricli.enable(source)
|
19
|
+
puts "#{source} has been enabled"
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-l", "--list-sources", "List all available Sources") do
|
24
|
+
puts Lyricli.sources
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-d", "--disable SOURCE", "Disable SOURCE") do |source|
|
29
|
+
Lyricli.disable(source)
|
30
|
+
puts "#{source} has been disabled"
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-r", "--reset SOURCE", "Reset the configuration of SOURCE") do |source|
|
35
|
+
Lyricli.reset(source)
|
36
|
+
puts "#{source} has been disabled and all its configuration reset"
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on("-h", "--help", "Shows this message") do
|
41
|
+
puts opts
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-v", "--version", "Show version") do
|
46
|
+
puts Lyricli.version
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
end.parse!
|
50
|
+
|
51
|
+
|
52
|
+
puts Lyricli.lyrics
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Lyricli
|
2
|
+
|
3
|
+
# This class handles the configuration of Lyricli
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
# Defines the paths to the default and user configuration files
|
7
|
+
def initialize
|
8
|
+
@config_path = "~/.lyricli.conf"
|
9
|
+
@defaults_path = "defaults.json"
|
10
|
+
@config = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
@@instance = Configuration.new
|
14
|
+
|
15
|
+
# Ensure this is only called once. Only use the instance class variable
|
16
|
+
# to access this method, as its constructor is private.
|
17
|
+
def self.instance
|
18
|
+
@@instance
|
19
|
+
end
|
20
|
+
|
21
|
+
# Access configuration properties, loads config if needed beforehand.
|
22
|
+
#
|
23
|
+
# @param [String] key the configuration key to access
|
24
|
+
# @return [String, Hash, Array] the value of the configuration key.
|
25
|
+
def [](key)
|
26
|
+
load_config unless @config
|
27
|
+
@config[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Assigns a new value to a configuration key, loads config if needed and
|
31
|
+
# saves it after updating.
|
32
|
+
#
|
33
|
+
# @param [String] key the configuration key to set
|
34
|
+
# @param [Object] value the value for the configuration key, can be any
|
35
|
+
# object as long as it can be converted to JSON
|
36
|
+
def []=(key, value)
|
37
|
+
load_config unless @config
|
38
|
+
@config[key] = value
|
39
|
+
save_config
|
40
|
+
end
|
41
|
+
|
42
|
+
# Deletes a key from the configuration, loads config if needed and saves
|
43
|
+
# it after deleting.
|
44
|
+
#
|
45
|
+
# @param [String] key the key to delete
|
46
|
+
def delete(key)
|
47
|
+
load_config unless @config
|
48
|
+
@config.delete(key)
|
49
|
+
save_config
|
50
|
+
end
|
51
|
+
|
52
|
+
private_class_method :new
|
53
|
+
|
54
|
+
# Loads the configuration from the user file, attempts to create it from
|
55
|
+
# defaults if it's not present. sets the `@config` instance variable.
|
56
|
+
def load_config
|
57
|
+
path = File.expand_path(@config_path)
|
58
|
+
|
59
|
+
if File.exists?(path)
|
60
|
+
file = File.new(path, "r")
|
61
|
+
@config = MultiJson.decode(file.read)
|
62
|
+
else
|
63
|
+
load_default_config
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Serializes the `@config` Hash to JSON and saves it to a file.
|
68
|
+
def save_config
|
69
|
+
path = File.expand_path(@config_path)
|
70
|
+
file = File.new(path, "w")
|
71
|
+
file.print(MultiJson.encode(@config))
|
72
|
+
file.close
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Loads the default configuration from a JSON file
|
78
|
+
def load_default_config
|
79
|
+
# Load the default
|
80
|
+
path = File.join(::Lyricli.root, "config", @defaults_path)
|
81
|
+
|
82
|
+
if File.exists?(path)
|
83
|
+
file = File.new(path, "r")
|
84
|
+
@config = MultiJson.decode(file.read)
|
85
|
+
else
|
86
|
+
@config = {}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Lyricli
|
2
|
+
|
3
|
+
# This class has the basic logic for extracting the lyrics and controlling the
|
4
|
+
# application
|
5
|
+
class Lyricli
|
6
|
+
|
7
|
+
# Constructor, initializes `@source_manager`
|
8
|
+
def initialize
|
9
|
+
@source_manager = SourceManager.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raises an InvalidLyricsError which means we did not get any valid
|
13
|
+
# artist/song from any of the sources
|
14
|
+
#
|
15
|
+
# @raise [Lyricli::Exceptions::InvalidLyricsError] because we found nothing
|
16
|
+
def exit_with_error
|
17
|
+
raise Exceptions::InvalidLyricsError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Extracts the current track, validates it and requests the lyrics from our
|
21
|
+
# LyricsEngine
|
22
|
+
#
|
23
|
+
# @return [String] the found lyrics, or a string indicating none were found
|
24
|
+
def get_lyrics
|
25
|
+
|
26
|
+
begin
|
27
|
+
set_current_track
|
28
|
+
check_params
|
29
|
+
rescue Exceptions::InvalidLyricsError
|
30
|
+
return "No Artist/Song could be found :("
|
31
|
+
end
|
32
|
+
|
33
|
+
engine = LyricsEngine.new(@current_track[:artist], @current_track[:song])
|
34
|
+
|
35
|
+
begin
|
36
|
+
return engine.get_lyrics
|
37
|
+
rescue Exceptions::LyricsNotFoundError
|
38
|
+
return "Lyrics not found :("
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set the `@current_track` instance variable by asking the SourceManager for
|
43
|
+
# its current track
|
44
|
+
def set_current_track
|
45
|
+
@current_track = @source_manager.current_track
|
46
|
+
end
|
47
|
+
|
48
|
+
# Exits with error when there is an empty field from the current track.
|
49
|
+
def check_params
|
50
|
+
self.exit_with_error unless @current_track
|
51
|
+
self.exit_with_error if @current_track[:artist].nil? or @current_track[:artist].empty?
|
52
|
+
self.exit_with_error if @current_track[:song].nil? or @current_track[:song].empty?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Lyricli
|
2
|
+
|
3
|
+
# This class gets the lyrics according to a given artist and song name.
|
4
|
+
class LyricsEngine
|
5
|
+
|
6
|
+
include Util
|
7
|
+
|
8
|
+
# Starts a new instance of LyricsEngine
|
9
|
+
#
|
10
|
+
# @param [String] artist the artist
|
11
|
+
# @param [String] song the song to look for
|
12
|
+
def initialize(artist, song)
|
13
|
+
@provider = URI("http://lyrics.wikia.com/api.php?artist=#{sanitize_param artist}&song=#{sanitize_param song}&fmt=realjson")
|
14
|
+
end
|
15
|
+
|
16
|
+
# Asks Lyrics Wiki for the lyrics, also cleans up the output a little.
|
17
|
+
#
|
18
|
+
# @return [String] the lyrics
|
19
|
+
def get_lyrics
|
20
|
+
begin
|
21
|
+
response = Net::HTTP.get(@provider)
|
22
|
+
response = MultiJson.decode(response)
|
23
|
+
|
24
|
+
doc = Nokogiri::HTML(open(response['url']))
|
25
|
+
node = doc.search(".lyricbox").first
|
26
|
+
rescue
|
27
|
+
raise Exceptions::LyricsNotFoundError
|
28
|
+
end
|
29
|
+
|
30
|
+
node.search(".rtMatcher").each do |n|
|
31
|
+
n.remove
|
32
|
+
end
|
33
|
+
|
34
|
+
node.search("br").each do |br|
|
35
|
+
br.replace "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
node.inner_text
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Lyricli
|
2
|
+
|
3
|
+
# Manages the different sources. SourceManager is in charge of enabling and
|
4
|
+
# disabling them, as well as getting the current track.
|
5
|
+
class SourceManager
|
6
|
+
|
7
|
+
include Util
|
8
|
+
|
9
|
+
# Creates a new instance of SourceManager
|
10
|
+
def initialize
|
11
|
+
@enabled_sources = []
|
12
|
+
@config = Configuration.instance
|
13
|
+
@config["enabled_sources"].each do |source|
|
14
|
+
if klass = parse_class(camelize(source))
|
15
|
+
current_source = klass.new
|
16
|
+
@enabled_sources << current_source
|
17
|
+
else
|
18
|
+
raise Exceptions::StartSourceError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Enables a source. This runs the source's enable method and adds it to the
|
24
|
+
# `enabled_sources` configuration key. It will only enable sources that
|
25
|
+
# are "available" (see #available_sources)
|
26
|
+
#
|
27
|
+
# @param [String] source_name the name of the source to enable
|
28
|
+
def enable(source_name)
|
29
|
+
if available_sources.include?(source_name)
|
30
|
+
if klass = parse_class(camelize(source_name))
|
31
|
+
klass.enable
|
32
|
+
@config["enabled_sources"] << klass.name
|
33
|
+
@config["enabled_sources"].uniq!
|
34
|
+
@config.save_config
|
35
|
+
else
|
36
|
+
raise Exceptions::EnableSourceError
|
37
|
+
end
|
38
|
+
else
|
39
|
+
raise Exceptions::UnknownSourceError
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Disables a source. This only removes the source from the `enabled_sources`
|
44
|
+
# configuration key.
|
45
|
+
#
|
46
|
+
# @param [String] source_name the name of the source to disable
|
47
|
+
def disable(source_name)
|
48
|
+
if available_sources.include?(source_name)
|
49
|
+
if klass = parse_class(camelize(source_name))
|
50
|
+
@config["enabled_sources"].delete(klass.name)
|
51
|
+
@config.save_config
|
52
|
+
else
|
53
|
+
raise Exceptions::DisableSourceError
|
54
|
+
end
|
55
|
+
else
|
56
|
+
raise Exceptions::UnknownSourceError
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Resets a source. This runs the source's reset method. It will also disable
|
61
|
+
# them.
|
62
|
+
#
|
63
|
+
# @param [String] source_name the name of the source to reset.
|
64
|
+
def reset(source_name)
|
65
|
+
if available_sources.include?(source_name)
|
66
|
+
if klass = parse_class(camelize(source_name))
|
67
|
+
klass.reset
|
68
|
+
disable(source_name)
|
69
|
+
else
|
70
|
+
raise Exceptions::ResetSourceError
|
71
|
+
end
|
72
|
+
else
|
73
|
+
raise Exceptions::UnknownSourceError
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Iterates over every source to attempt to retrieve the current song.
|
78
|
+
#
|
79
|
+
# @return [Hash] the current track, has an `:artist` and `:song` key.
|
80
|
+
def current_track
|
81
|
+
track = nil
|
82
|
+
lock = false
|
83
|
+
@enabled_sources.each do |source|
|
84
|
+
begin
|
85
|
+
current_track = source.current_track
|
86
|
+
|
87
|
+
# This is a special thing for arguments. The thing is, they need to
|
88
|
+
# be inputted manually. So, if they are present they won't allow
|
89
|
+
# anyone else to give results. Makes sense, yet a bit hacky.
|
90
|
+
unless current_track[:artist].nil? || current_track[:artist].empty? || current_track[:song].nil? || current_track[:song].empty?
|
91
|
+
track = current_track unless lock
|
92
|
+
lock = true if source.class.name == "arguments"
|
93
|
+
end
|
94
|
+
rescue
|
95
|
+
raise Exceptions::SourceConfigurationError
|
96
|
+
end
|
97
|
+
end
|
98
|
+
track
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns an array with the available sources. Optionally formats the result
|
102
|
+
# so active sources are identified by an appended *
|
103
|
+
#
|
104
|
+
# @param [Boolean] format whether or not to render the stars for active
|
105
|
+
# sources.
|
106
|
+
# @return [Array] the names of the currently available sources.
|
107
|
+
def available_sources(format = false)
|
108
|
+
path_root = File.expand_path(File.dirname(__FILE__))
|
109
|
+
sources = Dir[path_root+"/sources/*.rb"].map{ |s|
|
110
|
+
name = s.split("/").last.gsub(/\.rb/, "")
|
111
|
+
name
|
112
|
+
}
|
113
|
+
|
114
|
+
# Remove arguments (Hack?) We don't want anybody to touch tihs one.
|
115
|
+
sources.delete("arguments")
|
116
|
+
if format
|
117
|
+
# Add a star to denote enabled sources
|
118
|
+
format_sources(sources)
|
119
|
+
else
|
120
|
+
sources
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Adds a star to all members of the array that correspond to an active
|
125
|
+
# source
|
126
|
+
#
|
127
|
+
# @param [Array] sources the array of sources to format
|
128
|
+
# @return [Array] the formatted array
|
129
|
+
def format_sources(sources)
|
130
|
+
sources.map{ |s|
|
131
|
+
s << "*" if @config["enabled_sources"].include?(s)
|
132
|
+
s
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Lyricli
|
2
|
+
module Sources
|
3
|
+
# The arguments source. This one is special since it expects two
|
4
|
+
# arguments. It is treated specially by the SourceManager.
|
5
|
+
class Arguments
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :name
|
9
|
+
end
|
10
|
+
|
11
|
+
@name = "arguments"
|
12
|
+
|
13
|
+
# The enable method should run all of the tasks needed to validate
|
14
|
+
# the source. In the case of Rdio it has to authenticate with OAuth.
|
15
|
+
def self.enable
|
16
|
+
# Nothing to do.
|
17
|
+
end
|
18
|
+
|
19
|
+
# Instantiates everything it needs to run.
|
20
|
+
def initialize
|
21
|
+
# Nothing to do.
|
22
|
+
end
|
23
|
+
|
24
|
+
# The current_track method should return the name of the current
|
25
|
+
# artist and song.
|
26
|
+
# @return [Hash] A hash containing the current `:song` and `:artist`.
|
27
|
+
def current_track
|
28
|
+
artist = ARGV[0]
|
29
|
+
song = ARGV[1]
|
30
|
+
{:artist => artist, :song => song}
|
31
|
+
end
|
32
|
+
|
33
|
+
# The reset method resets any configurations it may have
|
34
|
+
def self.reset
|
35
|
+
# Reset Code
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
Binary file
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Lyricli
|
2
|
+
module Sources
|
3
|
+
# The source for iTunes
|
4
|
+
class Itunes
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_accessor :name
|
8
|
+
end
|
9
|
+
|
10
|
+
@name = "itunes"
|
11
|
+
|
12
|
+
# The enable method should run all of the tasks needed to validate
|
13
|
+
# the source. In the case of Rdio it has to authenticate with OAuth.
|
14
|
+
def self.enable
|
15
|
+
# Nothing to do
|
16
|
+
end
|
17
|
+
|
18
|
+
# Instantiates everything it needs to run.
|
19
|
+
def initialize
|
20
|
+
@config = Configuration.instance
|
21
|
+
@script = "current_song.scpt"
|
22
|
+
end
|
23
|
+
|
24
|
+
# The current_track method should return the name of the current
|
25
|
+
# artist and song.
|
26
|
+
# @return [Hash] A hash containing the current `:song` and `:artist`.
|
27
|
+
def current_track
|
28
|
+
path_root = File.expand_path(File.dirname(__FILE__))
|
29
|
+
path = File.join(path_root, @script)
|
30
|
+
current = `osascript #{path}`
|
31
|
+
current = current.split("<-SEP->")
|
32
|
+
{:artist => current[0], :song => current[1]}
|
33
|
+
end
|
34
|
+
|
35
|
+
# The reset method resets any configurations it may have
|
36
|
+
def self.reset
|
37
|
+
# Nothing to do
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Lyricli
|
2
|
+
module Sources
|
3
|
+
|
4
|
+
# This is the Source for rdio
|
5
|
+
class Rdio
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :name
|
9
|
+
end
|
10
|
+
|
11
|
+
@name = "rdio"
|
12
|
+
|
13
|
+
# The enable method should run all of the tasks needed to validate
|
14
|
+
# the source. In the case of Rdio it has to authenticate with OAuth.
|
15
|
+
def self.enable
|
16
|
+
# Validation Code
|
17
|
+
@config = Configuration.instance
|
18
|
+
unless @config["rdio_auth_token"] && !@config["rdio_auth_token"].empty?
|
19
|
+
create_auth_token
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "***"
|
23
|
+
puts "Hello, rdio tends to be a bit aggressive and tends to have trouble with other sources. If you're having trouble, you can disable it temporarily. You will not have to reauthenticate."
|
24
|
+
puts "***"
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
# Instantiates everything it needs to run.
|
29
|
+
def initialize
|
30
|
+
@name = 'rdio'
|
31
|
+
@config = Configuration.instance
|
32
|
+
@rdio = ::Rdio::SimpleRdio.new([@config["rdio_key"], @config["rdio_secret"]], @config["rdio_auth_token"])
|
33
|
+
end
|
34
|
+
|
35
|
+
# The current_track method should return the name of the current
|
36
|
+
# artist and song.
|
37
|
+
#
|
38
|
+
# @return [Hash] A hash containing the current `:song` and `:artist`.
|
39
|
+
def current_track
|
40
|
+
response = @rdio.call('currentUser', {'extras' => 'lastSongPlayed'})
|
41
|
+
artist = response["result"]["lastSongPlayed"]["artist"]
|
42
|
+
song = response["result"]["lastSongPlayed"]["name"]
|
43
|
+
{:artist => artist, :song => song}
|
44
|
+
end
|
45
|
+
|
46
|
+
# The reset method resets any configurations it may have
|
47
|
+
def self.reset
|
48
|
+
@config = Configuration.instance
|
49
|
+
@config.delete("rdio_auth_token")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Signs in to rdio with our credentials and requests access for a new auth
|
53
|
+
# token.
|
54
|
+
def self.create_auth_token
|
55
|
+
rdio = ::Rdio::SimpleRdio.new([@config["rdio_key"], @config["rdio_secret"]], @config["rdio_auth_token"])
|
56
|
+
|
57
|
+
# Request Authorization
|
58
|
+
puts "Follow this URL to authorize lyricli:"
|
59
|
+
auth_url = rdio.begin_authentication('oob')
|
60
|
+
puts auth_url
|
61
|
+
::Launchy.open(auth_url)
|
62
|
+
|
63
|
+
# Request Code, Obtain Token
|
64
|
+
print "Please type the authorization code: "
|
65
|
+
auth_code = gets.chomp
|
66
|
+
token = rdio.complete_authentication(auth_code)
|
67
|
+
|
68
|
+
@config["rdio_auth_token"] = token
|
69
|
+
token
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/lyricli/util.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Lyricli
|
2
|
+
# This module contains several utility functions.
|
3
|
+
module Util
|
4
|
+
|
5
|
+
# Transforms a string from snake_case to UpperCamelCase
|
6
|
+
#
|
7
|
+
# @param [String] str the string that will be Camelized
|
8
|
+
# @return [String] the Camelized string.
|
9
|
+
def camelize(str)
|
10
|
+
str.split('_').map {|w| w.capitalize}.join
|
11
|
+
end
|
12
|
+
|
13
|
+
# Takes a class name in snake_case and attempts to find the corresponding
|
14
|
+
# class from the sources.
|
15
|
+
#
|
16
|
+
# @param [String] class_name the snake_case name of the class to search for.
|
17
|
+
# @return [Class,nil] the found class or nil
|
18
|
+
def parse_class(class_name)
|
19
|
+
begin
|
20
|
+
path = "Sources::#{class_name}"
|
21
|
+
return eval(path)
|
22
|
+
rescue NameError
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Simply escapes a param and substitutes spaces and escaped plus signs for
|
28
|
+
# plus signs.
|
29
|
+
#
|
30
|
+
# @param [String] p the parameter to be sanitized
|
31
|
+
# @return [String] the sanitized parameter
|
32
|
+
def sanitize_param(p)
|
33
|
+
CGI.escape(p.gsub(/ /, "+")).gsub("%2B", "+")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/lyricli.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/http'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'launchy'
|
8
|
+
|
9
|
+
# This shit causes a lot of warnings. Quick Hack.
|
10
|
+
original_verbosity = $VERBOSE
|
11
|
+
$VERBOSE = nil
|
12
|
+
require 'rdio'
|
13
|
+
$VERBOSE = original_verbosity
|
14
|
+
|
15
|
+
# Local Dependencies
|
16
|
+
require "lyricli/util"
|
17
|
+
require "lyricli/configuration"
|
18
|
+
require "lyricli/lyricli"
|
19
|
+
require "lyricli/lyrics_engine"
|
20
|
+
require "lyricli/source_manager"
|
21
|
+
require "lyricli/sources"
|
22
|
+
require "lyricli/sources/arguments"
|
23
|
+
require "lyricli/sources/rdio"
|
24
|
+
require "lyricli/sources/itunes"
|
25
|
+
require "lyricli/exceptions"
|
26
|
+
require "lyricli/exceptions/disable_source_error"
|
27
|
+
require "lyricli/exceptions/enable_source_error"
|
28
|
+
require "lyricli/exceptions/invalid_lyrics_error"
|
29
|
+
require "lyricli/exceptions/lyrics_not_found_error"
|
30
|
+
require "lyricli/exceptions/reset_source_error"
|
31
|
+
require "lyricli/exceptions/source_configuration_error"
|
32
|
+
require "lyricli/exceptions/start_source_error"
|
33
|
+
require "lyricli/exceptions/unknown_source_error"
|
34
|
+
|
35
|
+
# The Lyricli module allows you to easily search for lyrics by looking for
|
36
|
+
# song and artist data from diverse sources.
|
37
|
+
module Lyricli
|
38
|
+
# Creates a new Lyricli instance and returns lyrics by going through the
|
39
|
+
# sources.
|
40
|
+
# @return [String] the fetched lyrics
|
41
|
+
def self.lyrics
|
42
|
+
@lyricli = Lyricli.new
|
43
|
+
@lyricli.get_lyrics
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the version of the library
|
47
|
+
# @return [String] the version
|
48
|
+
def self.version
|
49
|
+
Gem.loaded_specs["lyricli"].version
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a list of the available sources to enable or disable
|
53
|
+
# @return [String] the list of available sources. Enabled sources have
|
54
|
+
# a star appended.
|
55
|
+
def self.sources
|
56
|
+
source_manager = SourceManager.new
|
57
|
+
source_manager.available_sources(true).join(", ")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Enables a source via the Source Manager
|
61
|
+
def self.enable(source_name)
|
62
|
+
source_manager = SourceManager.new
|
63
|
+
begin
|
64
|
+
source_manager.enable(source_name)
|
65
|
+
rescue Exceptions::UnknownSourceError
|
66
|
+
"There is no such Source"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Disables a source via the Source Manager
|
71
|
+
def self.disable(source_name)
|
72
|
+
source_manager = SourceManager.new
|
73
|
+
begin
|
74
|
+
source_manager.disable(source_name)
|
75
|
+
rescue Exceptions::UnknownSourceError
|
76
|
+
"There is no such Source"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Resets all configuration for a source via the Source Manager
|
81
|
+
def self.reset(source_name)
|
82
|
+
source_manager = SourceManager.new
|
83
|
+
begin
|
84
|
+
source_manager.reset(source_name)
|
85
|
+
rescue Exceptions::UnknownSourceError
|
86
|
+
"There is no such Source"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the root of the Gem.
|
91
|
+
#
|
92
|
+
# @return [String] the root path for this gem
|
93
|
+
def self.root
|
94
|
+
File.expand_path('../..',__FILE__)
|
95
|
+
end
|
96
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "lyricli"
|
3
|
+
|
4
|
+
describe Lyricli::SourceManager do
|
5
|
+
before :each do
|
6
|
+
# Stub the configuration
|
7
|
+
@example_configuration = {"enabled_sources" => "test_class"}
|
8
|
+
Configuration.stub(:'[]').and_return(@example_configuration)
|
9
|
+
Configuration.stub(:delete)
|
10
|
+
|
11
|
+
# Stub the test class.
|
12
|
+
module Lyricli
|
13
|
+
module Sources
|
14
|
+
module TestClass
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@example_artist = {:artist => "The Shins", :song => "Know Your Onion"}
|
20
|
+
|
21
|
+
Lyricli::Sources::TestClass.stub(:enable)
|
22
|
+
Lyricli::Sources::TestClass.stub(:reset)
|
23
|
+
Lyricli::Sources::TestClass.stub(:current_track).and_return(@example_artist)
|
24
|
+
end
|
25
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "lyricli"
|
3
|
+
|
4
|
+
describe Lyricli::Util do
|
5
|
+
before :each do
|
6
|
+
class TestClass
|
7
|
+
include Lyricli::Util
|
8
|
+
end
|
9
|
+
|
10
|
+
module Lyricli
|
11
|
+
module Sources
|
12
|
+
class ParsedClass
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@c = TestClass.new
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#camelize" do
|
21
|
+
it "should convert snake_case to CamelCase" do
|
22
|
+
expect(@c.camelize("test_string")).to eq("TestString")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#parse_class" do
|
27
|
+
it "should parse classes under the Source namespace" do
|
28
|
+
expect(@c.parse_class("ParsedClass")).to eq(Lyricli::Sources::ParsedClass)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return nil for nonexistent classes" do
|
32
|
+
expect(@c.parse_class("non_existent_class")).to eq(nil)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#sanitize_param" do
|
37
|
+
it "should escape weird characters, but conserve the +" do
|
38
|
+
str = "one two+three /?&"
|
39
|
+
expect(@c.sanitize_param(str)).to eq("one+two+three+%2F%3F%26")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lyricli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ben Beltran
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: nokogiri
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.5.5
|
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: 1.5.5
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: multi_json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.3.6
|
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: 1.3.6
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rdio
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.1.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.1.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: launchy
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.1.2
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.1.2
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: ruby-debug19
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.11.6
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.11.6
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: yard
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.8.2.1
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.8.2.1
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: thin
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.5.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.5.0
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rspec
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ~>
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: 2.11.0
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 2.11.0
|
142
|
+
description: Lyricli is an awesome CLI tool to read the lyrics of your currently playing
|
143
|
+
song right in the command line
|
144
|
+
email: ben@nsovocal.com
|
145
|
+
executables:
|
146
|
+
- lrc
|
147
|
+
extensions: []
|
148
|
+
extra_rdoc_files: []
|
149
|
+
files:
|
150
|
+
- lib/lyricli/configuration.rb
|
151
|
+
- lib/lyricli/exceptions/disable_source_error.rb
|
152
|
+
- lib/lyricli/exceptions/enable_source_error.rb
|
153
|
+
- lib/lyricli/exceptions/invalid_lyrics_error.rb
|
154
|
+
- lib/lyricli/exceptions/lyrics_not_found_error.rb
|
155
|
+
- lib/lyricli/exceptions/reset_source_error.rb
|
156
|
+
- lib/lyricli/exceptions/source_configuration_error.rb
|
157
|
+
- lib/lyricli/exceptions/start_source_error.rb
|
158
|
+
- lib/lyricli/exceptions/unknown_source_error.rb
|
159
|
+
- lib/lyricli/exceptions.rb
|
160
|
+
- lib/lyricli/lyricli.rb
|
161
|
+
- lib/lyricli/lyrics_engine.rb
|
162
|
+
- lib/lyricli/source_manager.rb
|
163
|
+
- lib/lyricli/sources/arguments.rb
|
164
|
+
- lib/lyricli/sources/itunes.rb
|
165
|
+
- lib/lyricli/sources/rdio.rb
|
166
|
+
- lib/lyricli/sources.rb
|
167
|
+
- lib/lyricli/util.rb
|
168
|
+
- lib/lyricli.rb
|
169
|
+
- bin/lrc
|
170
|
+
- Gemfile
|
171
|
+
- Gemfile.lock
|
172
|
+
- README.md
|
173
|
+
- spec/bin/lrc_spec.rb
|
174
|
+
- spec/lib/lyricli/configuration_spec.rb
|
175
|
+
- spec/lib/lyricli/exceptions_spec.rb
|
176
|
+
- spec/lib/lyricli/lyricli_spec.rb
|
177
|
+
- spec/lib/lyricli/lyrics_engine_spec.rb
|
178
|
+
- spec/lib/lyricli/source_manager_spec.rb
|
179
|
+
- spec/lib/lyricli/sources/arguments.rb
|
180
|
+
- spec/lib/lyricli/sources/itunes.rb
|
181
|
+
- spec/lib/lyricli/sources/rdio.rb
|
182
|
+
- spec/lib/lyricli/sources_spec.rb
|
183
|
+
- spec/lib/lyricli/util_spec.rb
|
184
|
+
- spec/lib/lyricli_spec.rb
|
185
|
+
- config/defaults.json
|
186
|
+
- lib/lyricli/sources/current_song.scpt
|
187
|
+
homepage: http://nsovocal.com/lyricli
|
188
|
+
licenses: []
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
none: false
|
195
|
+
requirements:
|
196
|
+
- - ! '>='
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '0'
|
199
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
200
|
+
none: false
|
201
|
+
requirements:
|
202
|
+
- - ! '>='
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 1.8.24
|
208
|
+
signing_key:
|
209
|
+
specification_version: 3
|
210
|
+
summary: Lyricli is an awesome lyric client for your Command Line
|
211
|
+
test_files: []
|
212
|
+
has_rdoc:
|