subcl 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cce8d7f636cf3a5898c7ce68aa2287a8dcc6673d
4
+ data.tar.gz: 1df467a85ba4ab604668844a359704200ee4c88c
5
+ SHA512:
6
+ metadata.gz: adae72dbd6ab95c0501234706cdeceeb41fece5aacabf5847dd020c060e4d9a3f8cc36010ad34f70bd6c98bbe818b813f509711f98c2d7fd5bd7258ae2064513
7
+ data.tar.gz: 8a5219d0d537d6d8450de8518d334084ddcea5be42709c8db0e4871db4bd468fb2bf22c0af3c430b1231176cbad23b46e18b75e674e2c0ec1a150278eb94da8a
data/bin/subcl CHANGED
@@ -1,132 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative '../lib/Subcl'
4
- require 'optparse'
3
+ require 'subcl'
5
4
 
6
5
  #don't throw a huge stacktrace
7
6
  trap("INT") {
8
- puts "\n"
9
- exit
7
+ puts "\n"
8
+ exit
10
9
  }
11
10
 
12
- options = { :tty => true }
11
+ Runner.new.run ARGV
13
12
 
14
- if File.exist?('debug')
15
- puts "DEBUGGING"
16
- options[:debug] = true
17
- end
18
-
19
- #no idea how to get this variable from outside, so we'll just set it in the loop
20
- usage = nil
21
- OptionParser.new do |opts|
22
- opts.banner = "Usage: subcl [options] command"
23
- opts.separator %{
24
- Commands
25
- list to terminal
26
- search[-song|-album|-artist] <pattern>
27
- ss|sl|sr <pattern>
28
- clear queue and immediately start playing
29
- play[-song|-album|-artist|-playlist] <pattern>
30
- ps|pl|pr|pp <pattern>
31
- clear queue and immediately start playing random songs
32
- play-random <count, default 10>
33
- r <count, default 10>
34
- add to end of queue
35
- queue-last[-song|-album|-artist|-playlist] <pattern>
36
- ls|ll|lr|lp <pattern>
37
- add after the current song
38
- queue-next[-song|-album|-artist|-playlist] <pattern>
39
- ns|nl|nr|np <pattern>
40
- albumart-url [size] - print url of albumart to terminal,
41
- optionally with a specified image size
42
-
43
- Options }
44
-
45
- usage = opts
46
-
47
- opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
48
- options[:verbose] = v
49
- end
50
- opts.on('-1', '--use-first', 'On multiple matches, use the first match instead of asking interactively') do
51
- options[:interactive] = false
52
- end
53
- opts.on('-s', '--shuffle', "Shuffle playlist before queueing") do
54
- options[:shuffle] = true
55
- end
56
- opts.on('-c', '--current', 'Use info currently playing song instead of commandline argument') do
57
- options[:current] = true
58
- end
59
- opts.on('-h', '--help', 'Display this screen') do
60
- puts opts
61
- exit
62
- end
63
- opts.on("--version", "Print version information") do
64
- puts Configs.new.app_version
65
- exit
66
- end
67
-
68
- end.parse!
69
-
70
- unless ARGV.size >= 1
71
- puts usage
72
- exit
73
- end
74
-
75
- unless system('tty -s')
76
- #not running in a tty, so no use for interactivity
77
- options[:tty] = false
78
- options[:interactive] = false
79
- end
80
-
81
- subcl = Subcl.new options
82
-
83
- arg = ARGV[1,ARGV.length-1].join(" ") #put rest of args together so no quotes are required
84
-
85
-
86
- case ARGV[0].downcase
87
- when /^play-song$|^ps$/
88
- subcl.queue(arg, :song, {:play => true, :clear => true})
89
- when /^play-artist$|^pr$/
90
- subcl.queue(arg, :artist, {:play => true, :clear => true})
91
- when /^play-album$|^pl$/
92
- subcl.queue(arg, :album, {:play => true, :clear => true})
93
- when /^play-playlist$|^pp$/
94
- subcl.queue(arg, :playlist, {:play => true, :clear => true})
95
- when /^play-random$|^r$/
96
- subcl.queue(arg, :randomSong, {:play => true, :clear => true})
97
- when /^queue-next-song$|^ns$/
98
- subcl.queue(arg, :song, {:insert => true})
99
- when /^queue-next-artist$|^nr$/
100
- subcl.queue(arg, :artist, {:insert => true})
101
- when /^queue-next-album$|^nl$/
102
- subcl.queue(arg, :album, {:insert => true})
103
- when /^queue-next-playlist$|^np$/
104
- subcl.queue(arg, :playlist, {:insert => true})
105
- when /^queue-last-song$|^ls$/
106
- subcl.queue(arg, :song)
107
- when /^queue-last-artist$|^lr$/
108
- subcl.queue(arg, :artist)
109
- when /^queue-last-album$|^ll$/
110
- subcl.queue(arg, :album)
111
- when /^queue-last-playlist$|^lp$/
112
- subcl.queue(arg, :playlist)
113
- when /^search-song$|^ss$/
114
- subcl.searchSong(arg)
115
- when /^search-artist$|^sr$/
116
- subcl.searchArtist(arg)
117
- when /^search-album$|^sl$/
118
- subcl.searchAlbum(arg)
119
- when "albumart-url"
120
- arg = nil if arg.empty?
121
- puts subcl.albumartUrl(arg)
122
- when /^album-list$|^al$/
123
- subcl.subsonic.albumlist
124
- when "test-notify"
125
- subcl.testNotify
126
- else
127
- if options[:tty] then
128
- puts usage
129
- else
130
- subcl.notifier.notify "Unrecognized command"
131
- end
132
- end
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'subcl'
4
+
5
+ if ARGV.empty?
6
+ puts "Usage: subcl-api <method> [opt1=val1 [opt2=val2]...]"
7
+ exit 1
8
+ end
9
+
10
+ api = SubsonicAPI.new(Configs.new)
11
+ method = ARGV.shift
12
+ method += ".view" unless method.end_with? ".view"
13
+ params = Hash[ARGV.collect { |arg| arg.split('=') }]
14
+
15
+ LOGGER.level = Logger::INFO
16
+
17
+ begin
18
+ puts api.query(method, params)
19
+ rescue SubclError => e
20
+ $stderr.puts e.message
21
+ end
22
+
@@ -0,0 +1,14 @@
1
+ require 'subcl/configs'
2
+ require 'subcl/player'
3
+ require 'subcl/notify'
4
+ require 'subcl/picker'
5
+ require 'subcl/runner'
6
+ require 'subcl/song'
7
+ require 'subcl/subcl'
8
+ require 'subcl/subcl_error'
9
+ require 'subcl/subsonic_api'
10
+
11
+ require 'logger'
12
+
13
+ LOGGER = Logger.new(STDERR)
14
+ LOGGER.level = Logger::INFO
@@ -0,0 +1,61 @@
1
+ class Configs
2
+
3
+ attr_accessor :configs
4
+
5
+ REQUIRED_SETTINGS = %i{ server username password }
6
+ OPTIONAL_SETTINGS = %i{ max_search_results notify_method random_song_count }
7
+
8
+ def initialize(file = '~/.subcl')
9
+ @configs = {
10
+ :notifyMethod => "auto",
11
+ }
12
+
13
+ @filename = File.expand_path(file)
14
+ unless File.file?(@filename)
15
+ raise "Config file not found"
16
+ end
17
+
18
+ read_configs
19
+ end
20
+
21
+ def read_configs
22
+ settings = REQUIRED_SETTINGS + OPTIONAL_SETTINGS
23
+ open(@filename).each_line do |line|
24
+ next if line.start_with? '#'
25
+
26
+ key, value = line.split(' ')
27
+ key = key.to_sym
28
+ if settings.include? key
29
+ @configs[key] = value
30
+ else
31
+ LOGGER.warn { "Unknown setting: '#{key}'" }
32
+ end
33
+ end
34
+
35
+ REQUIRED_SETTINGS.each do |setting|
36
+ if @configs[setting].nil?
37
+ raise "Missing setting '#{setting}'"
38
+ end
39
+ end
40
+ end
41
+
42
+ def [](key)
43
+ raise "Undefined setting #{key}" unless @configs.has_key? key
44
+ @configs[key]
45
+ end
46
+
47
+ def []=(key, val)
48
+ settings = REQUIRED_SETTINGS + OPTIONAL_SETTINGS
49
+ settings.each do |name|
50
+ if key == name
51
+ @configs[key] = val
52
+ return
53
+ end
54
+ end
55
+ raise "Undefined setting #{key}"
56
+ end
57
+
58
+ def to_hash
59
+ @configs
60
+ end
61
+ end
@@ -0,0 +1,66 @@
1
+ class Notify
2
+
3
+ SupportedMethods = %w{notify-send growlnotify awesome-client}
4
+ Icon = File.dirname(__FILE__) + "/../../share/icon.png"
5
+
6
+ def initialize(notifyMethod)
7
+ @method = nil
8
+
9
+ case notifyMethod
10
+ when nil
11
+ #do nothing
12
+ when "none"
13
+ #do nothing
14
+ when "auto"
15
+ #auto detect notifier lib
16
+ SupportedMethods.each do |method|
17
+ @binary = get_binary(method)
18
+ unless @binary.nil?
19
+ @method = method
20
+ break
21
+ end
22
+ end
23
+ else
24
+ #use specified binary
25
+ unless SupportedMethods.include? notifyMethod
26
+ raise ArgumentError, "Notification method #{notifyMethod} is not supported"
27
+ end
28
+ @binary = get_binary(notifyMethod)
29
+ @method = notifyMethod
30
+ raise ArgumentError, "No binary found for #{notifyMethod} in PATH" if @binary.nil?
31
+ end
32
+ end
33
+
34
+ def get_binary(name)
35
+ binary = `which #{name}`.chomp
36
+ if $?.success?
37
+ binary
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ def notify(message)
44
+ case @method
45
+ when nil
46
+ #great, do nothing
47
+ when "notify-send"
48
+ system("notify-send --icon #{Icon} --urgency critical Subcl '#{message}'")
49
+ when "growlnotify"
50
+ system("growlnotify --image #{Icon} --title Subcl --message '#{message}'")
51
+ when "awesome-client"
52
+ naughtyCmd = %Q{
53
+ naughty.notify({
54
+ title='subcl',
55
+ text='#{message}',
56
+ icon='#{Icon}',
57
+ timeout = 10
58
+ })
59
+ }
60
+ naughtyCmd.gsub! "\n" " "
61
+
62
+ system(%Q{echo "#{naughtyCmd}" | awesome-client})
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,63 @@
1
+ class Picker
2
+ def initialize(ary)
3
+ @available = ary
4
+ if ary.empty? then
5
+ raise ArgumentError, "Cannot initialize Picker with an empty array!"
6
+ end
7
+ end
8
+
9
+ def pick
10
+ choices = {}
11
+
12
+ i = 1
13
+ @available.each do |elem|
14
+ choices[i] = elem
15
+ $stderr.print "[#{i}] "
16
+ yield(elem)
17
+ i = i + 1
18
+ end
19
+
20
+
21
+ begin
22
+ picks = []
23
+ valid = true
24
+ $stderr.print "Pick any: "
25
+
26
+ choice = $stdin.gets
27
+
28
+ return @available if choice.chomp == 'all'
29
+
30
+ choice.split(/[ ,]+/).each do |part|
31
+ possibleRange = part.split(/\.\.|-/)
32
+ if possibleRange.length == 2
33
+ start = possibleRange[0].to_i
34
+ stop = possibleRange[1].to_i
35
+ [start, stop].each do |num|
36
+ valid = validate(num)
37
+ end
38
+ (start..stop).each do |num|
39
+ picks << choices[num]
40
+ end
41
+
42
+ elsif validate(num = part.to_i)
43
+ picks << choices[num]
44
+ else
45
+ valid == false
46
+ end
47
+ end
48
+ end while !valid
49
+
50
+ return picks
51
+ end
52
+
53
+ def validate(pickNum)
54
+ #no -1, we start filling choices{} at 1
55
+ if pickNum > 0 and pickNum <= @available.length
56
+ true
57
+ else
58
+ $stderr.puts "Invalid pick. Try again."
59
+ false
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,54 @@
1
+ require 'ruby-mpd'
2
+ require 'delegate'
3
+
4
+ class Player < SimpleDelegator
5
+ def initialize
6
+ #TODO add configs for host/port/security
7
+ @mpd = MPD.new
8
+ @mpd.connect
9
+ super(@mpd)
10
+ end
11
+
12
+ #insert: whether to add the song after the currently playing one
13
+ #instead of the end of the queue
14
+ def add(song, insert = false)
15
+ unless song[:stream_url]
16
+ raise ArgumentError, "argument has no :stream_url!"
17
+ end
18
+ LOGGER.debug { "Adding #{song['title']}: #{song[:stream_url]}. Insert: #{insert}" }
19
+ if insert then
20
+ pos = @mpd.current_song.pos + 1
21
+ @mpd.addid(song[:stream_url], pos)
22
+ else
23
+ @mpd.add(song[:stream_url])
24
+ end
25
+ end
26
+
27
+ #stops the player and clears the playlist
28
+ def clearstop
29
+ @mpd.stop
30
+ @mpd.clear
31
+ end
32
+
33
+ # if song has been playing for more than 4 seconds, rewind it to the start
34
+ # otherwise go to the previous song
35
+ def rewind
36
+ if @mpd.status[:elapsed] > 4
37
+ @mpd.seek 0
38
+ else
39
+ @mpd.previous
40
+ end
41
+ end
42
+
43
+ # if mpd is playing, pause it. Otherwise resume playback
44
+ def toggle
45
+ @mpd.pause = @mpd.playing? ? 1 : 0
46
+ end
47
+
48
+ def pause
49
+ @mpd.pause = 1
50
+ end
51
+
52
+ #TODO: might create a wrapper for current_song that makes API calls for artist, album, etc,
53
+ # in case mpd is unable to decode the metadata
54
+ end
@@ -0,0 +1,154 @@
1
+ require 'optparse'
2
+
3
+ class Runner
4
+ def initialize(options = {})
5
+ @options = {
6
+ :tty => true,
7
+ :out_stream => STDOUT,
8
+ :err_stream => STDERR,
9
+ :mock_player => nil,
10
+ :mock_api => nil
11
+ }.merge! options
12
+
13
+ #TODO refactor this away
14
+ if File.exist?('debug')
15
+ puts "DEBUGGING"
16
+ @options[:debug] = true
17
+ end
18
+ end
19
+
20
+ def parse_options! args
21
+ OptionParser.new do |opts|
22
+ opts.banner = "Usage: subcl [options] command"
23
+ opts.separator %{
24
+ Queue Commands
25
+ clear queue and immediately start playing
26
+ play[-song|-album|-artist|-playlist] <pattern>
27
+ ps|pl|pr|pp <pattern>
28
+ clear queue and immediately start playing random songs
29
+ play-random <count, default 10>
30
+ r <count, default 10>
31
+ add to end of queue
32
+ queue-last[-song|-album|-artist|-playlist] <pattern>
33
+ ls|ll|lr|lp <pattern>
34
+ add after the current song
35
+ queue-next[-song|-album|-artist|-playlist] <pattern>
36
+ ns|nl|nr|np <pattern>
37
+ albumart-url [size] - print url of albumart to terminal,
38
+ optionally with a specified image size
39
+
40
+ Playback Commands
41
+ play
42
+ pause
43
+ toggle (play when pause, pause when played)
44
+ stop
45
+ next
46
+ previous
47
+ rewind (get to start of song, or previous song when at start)
48
+
49
+ Options }
50
+
51
+ @usage = opts
52
+
53
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
54
+ @options[:verbose] = v
55
+ end
56
+ opts.on('-1', '--use-first', 'On multiple matches, use the first match instead of asking interactively') do
57
+ @options[:interactive] = false
58
+ end
59
+ opts.on('-s', '--shuffle', "Shuffle playlist before queueing") do
60
+ @options[:shuffle] = true
61
+ end
62
+ opts.on('-c', '--current', 'Use info currently playing song instead of commandline argument') do
63
+ @options[:current] = true
64
+ end
65
+ opts.on('-h', '--help', 'Display this screen') do
66
+ out_stream.puts opts
67
+ exit
68
+ end
69
+ opts.on("--version", "Print version information") do
70
+ out_stream.puts Configs.new[:app_version]
71
+ exit
72
+ end
73
+
74
+ end.parse! args
75
+ end
76
+
77
+ def run(args)
78
+ LOGGER.debug { "args = #{args}" }
79
+
80
+ parse_options!(args)
81
+
82
+ LOGGER.debug { "args = #{args}" }
83
+
84
+ unless args.size >= 1
85
+ @options[:err_stream].puts @usage
86
+ exit 3
87
+ end
88
+
89
+ unless system('tty -s')
90
+ #not running in a tty, so no use for interactivity
91
+ @options[:tty] = false
92
+ @options[:interactive] = false
93
+ end
94
+
95
+ subcl = Subcl.new @options
96
+
97
+ arg = args[1,args.length-1].join(" ") #put rest of args together so no quotes are required
98
+
99
+ command = args[0].downcase
100
+ case command
101
+
102
+ when /^play-song$|^ps$/
103
+ subcl.queue(arg, :song, {:play => true, :clear => true})
104
+ when /^play-artist$|^pr$/
105
+ subcl.queue(arg, :artist, {:play => true, :clear => true})
106
+ when /^play-album$|^pl$/
107
+ subcl.queue(arg, :album, {:play => true, :clear => true})
108
+ when /^play-playlist$|^pp$/
109
+ subcl.queue(arg, :playlist, {:play => true, :clear => true})
110
+ when /^play-random$|^r$/
111
+ subcl.queue(arg, :randomSong, {:play => true, :clear => true})
112
+ when /^queue-next-song$|^ns$/
113
+ subcl.queue(arg, :song, {:insert => true})
114
+ when /^queue-next-artist$|^nr$/
115
+ subcl.queue(arg, :artist, {:insert => true})
116
+ when /^queue-next-album$|^nl$/
117
+ subcl.queue(arg, :album, {:insert => true})
118
+ when /^queue-next-playlist$|^np$/
119
+ subcl.queue(arg, :playlist, {:insert => true})
120
+ when /^queue-last-song$|^ls$/
121
+ subcl.queue(arg, :song)
122
+ when /^queue-last-artist$|^lr$/
123
+ subcl.queue(arg, :artist)
124
+ when /^queue-last-album$|^ll$/
125
+ subcl.queue(arg, :album)
126
+ when /^queue-last-playlist$|^lp$/
127
+ subcl.queue(arg, :playlist)
128
+ when "albumart-url"
129
+ arg = nil if arg.empty?
130
+ @options[:out_stream].puts subcl.albumart_url(arg)
131
+ when /^album-list$|^al$/
132
+ subcl.albumlist
133
+ when "test-notify"
134
+ subcl.testNotify
135
+ else
136
+ begin
137
+ #pass through for player commands
138
+ subcl.send(command, [])
139
+ rescue NoMethodError
140
+ unknown(command)
141
+ end
142
+ end
143
+ end
144
+
145
+ def unknown(command)
146
+ if @options[:tty] then
147
+ @options[:err_stream].puts "Unknown command '#{command}'"
148
+ @options[:err_stream].puts @usage
149
+ else
150
+ subcl.notifier.notify "Unknown command '#{command}'"
151
+ end
152
+ exit 3
153
+ end
154
+ end