subcl 1.1.1 → 1.1.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.
- checksums.yaml +4 -4
- data/lib/subcl.rb +4 -1
- data/lib/subcl/configs.rb +61 -7
- data/lib/subcl/picker.rb +5 -1
- data/lib/subcl/player.rb +9 -1
- data/lib/subcl/runner.rb +13 -3
- data/lib/subcl/subcl.rb +39 -5
- data/lib/subcl/subsonic_api.rb +8 -1
- data/lib/subcl/version.rb +3 -0
- data/share/subcl.default +13 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb19261a8a56302438bd138708e9e12384b91da6
|
4
|
+
data.tar.gz: 0d2078fd2198df4d12232547d4eadb524e2ff925
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14ecdf1251f53bd8f92155064d7a3c5cefa6a29a07e54eebcbd6007b5a96a65d72e13607936eab27ad4004e140308588b83f42d4afc565ab03274991430b58ab
|
7
|
+
data.tar.gz: 88c24c10b931983f6750b267c0ae0250719d81668ab65f4f827f9f0aafe78da1c048eb3385c81544173ee50cccaec911adbca0e99cc3caccbc3299fbf6b31138
|
data/lib/subcl.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'subcl/version'
|
2
|
+
|
1
3
|
require 'subcl/configs'
|
2
4
|
require 'subcl/player'
|
3
5
|
require 'subcl/notify'
|
@@ -5,9 +7,10 @@ require 'subcl/picker'
|
|
5
7
|
require 'subcl/runner'
|
6
8
|
require 'subcl/song'
|
7
9
|
require 'subcl/subcl'
|
8
|
-
require 'subcl/subcl_error'
|
9
10
|
require 'subcl/subsonic_api'
|
10
11
|
|
12
|
+
require 'subcl/subcl_error'
|
13
|
+
|
11
14
|
require 'logger'
|
12
15
|
|
13
16
|
LOGGER = Logger.new(STDERR)
|
data/lib/subcl/configs.rb
CHANGED
@@ -1,18 +1,29 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
1
3
|
class Configs
|
2
4
|
|
3
5
|
attr_accessor :configs
|
4
6
|
|
5
7
|
REQUIRED_SETTINGS = %i{ server username password }
|
6
|
-
OPTIONAL_SETTINGS = %i{ max_search_results notify_method random_song_count }
|
8
|
+
OPTIONAL_SETTINGS = %i{ max_search_results notify_method random_song_count wildcard_order play_any_on_unknown_command}
|
9
|
+
DEFAULT_PATH = File.expand_path('~/.subcl')
|
10
|
+
DEFAULT_CONFIG = File.dirname(__FILE__) + "/../../share/subcl.default"
|
11
|
+
|
12
|
+
WILDCARD_ORDER_ITEMS = %i{ song album artist playlist }
|
7
13
|
|
8
|
-
def initialize(file =
|
14
|
+
def initialize(file = DEFAULT_PATH)
|
9
15
|
@configs = {
|
10
16
|
:notifyMethod => "auto",
|
17
|
+
:play_any_on_unknown_command => false
|
11
18
|
}
|
12
19
|
|
13
|
-
@
|
14
|
-
unless File.file?(@
|
15
|
-
|
20
|
+
@file = File.expand_path(file)
|
21
|
+
unless File.file?(@file)
|
22
|
+
if @file == DEFAULT_PATH and Configs.tty?
|
23
|
+
ask_create_config
|
24
|
+
else
|
25
|
+
raise "Config file '#{@file}' not found"
|
26
|
+
end
|
16
27
|
end
|
17
28
|
|
18
29
|
read_configs
|
@@ -20,10 +31,13 @@ class Configs
|
|
20
31
|
|
21
32
|
def read_configs
|
22
33
|
settings = REQUIRED_SETTINGS + OPTIONAL_SETTINGS
|
23
|
-
open(@
|
34
|
+
open(@file).each_line do |line|
|
24
35
|
next if line.start_with? '#'
|
36
|
+
next if line.chomp.empty?
|
37
|
+
|
38
|
+
key, value = line.split(' ', 2)
|
39
|
+
value.chomp!
|
25
40
|
|
26
|
-
key, value = line.split(' ')
|
27
41
|
key = key.to_sym
|
28
42
|
if settings.include? key
|
29
43
|
@configs[key] = value
|
@@ -32,6 +46,8 @@ class Configs
|
|
32
46
|
end
|
33
47
|
end
|
34
48
|
|
49
|
+
validate_wildcard_order
|
50
|
+
|
35
51
|
REQUIRED_SETTINGS.each do |setting|
|
36
52
|
if @configs[setting].nil?
|
37
53
|
raise "Missing setting '#{setting}'"
|
@@ -39,6 +55,30 @@ class Configs
|
|
39
55
|
end
|
40
56
|
end
|
41
57
|
|
58
|
+
def validate_wildcard_order
|
59
|
+
if @configs[:wildcard_order]
|
60
|
+
raw_order = @configs[:wildcard_order]
|
61
|
+
final_order = []
|
62
|
+
raw_order.split(',').each do |item|
|
63
|
+
item = item.to_sym
|
64
|
+
if WILDCARD_ORDER_ITEMS.include? item
|
65
|
+
final_order << item
|
66
|
+
else
|
67
|
+
LOGGER.warn("Invalid wildcard_order item #{item}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
WILDCARD_ORDER_ITEMS.each do |item|
|
72
|
+
unless final_order.include? item
|
73
|
+
LOGGER.warn("wildcard_order is missing #{item}")
|
74
|
+
final_order << item
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
@configs[:wildcard_order] = final_order
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
42
82
|
def [](key)
|
43
83
|
raise "Undefined setting #{key}" unless @configs.has_key? key
|
44
84
|
@configs[key]
|
@@ -58,4 +98,18 @@ class Configs
|
|
58
98
|
def to_hash
|
59
99
|
@configs
|
60
100
|
end
|
101
|
+
|
102
|
+
def ask_create_config
|
103
|
+
$stderr.puts "No configuration found at #{DEFAULT_PATH}. Create one? [y/n]"
|
104
|
+
if $stdin.gets.chomp =~ /[yY]/
|
105
|
+
FileUtils.cp(DEFAULT_CONFIG, DEFAULT_PATH)
|
106
|
+
$stderr.puts "Created #{DEFAULT_PATH}"
|
107
|
+
exit 0
|
108
|
+
end
|
109
|
+
exit 4
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.tty?
|
113
|
+
system('tty -s')
|
114
|
+
end
|
61
115
|
end
|
data/lib/subcl/picker.rb
CHANGED
@@ -9,10 +9,14 @@ class Picker
|
|
9
9
|
def pick
|
10
10
|
choices = {}
|
11
11
|
|
12
|
+
#TODO add type column when multiple types are available
|
13
|
+
|
14
|
+
counter_padding = @available.length.to_s.length
|
12
15
|
i = 1
|
13
16
|
@available.each do |elem|
|
14
17
|
choices[i] = elem
|
15
|
-
|
18
|
+
#TODO add padding for numbers with one digit
|
19
|
+
$stderr.print "[#{i.to_s.rjust(counter_padding)}] "
|
16
20
|
yield(elem)
|
17
21
|
i = i + 1
|
18
22
|
end
|
data/lib/subcl/player.rb
CHANGED
@@ -42,7 +42,15 @@ class Player < SimpleDelegator
|
|
42
42
|
|
43
43
|
# if mpd is playing, pause it. Otherwise resume playback
|
44
44
|
def toggle
|
45
|
-
|
45
|
+
if @mpd.playing?
|
46
|
+
@mpd.pause = 1
|
47
|
+
else
|
48
|
+
# experimental hack: I think this forces mpd to start downloading the stream again.
|
49
|
+
# this should prevent a bug that fails to resume streams after pausing
|
50
|
+
# TODO might make this configurable
|
51
|
+
@mpd.seek(@mpd.status[:elapsed].to_i)
|
52
|
+
@mpd.pause = 0
|
53
|
+
end
|
46
54
|
end
|
47
55
|
|
48
56
|
def pause
|
data/lib/subcl/runner.rb
CHANGED
@@ -63,11 +63,11 @@ class Runner
|
|
63
63
|
@options[:current] = true
|
64
64
|
end
|
65
65
|
opts.on('-h', '--help', 'Display this screen') do
|
66
|
-
out_stream.puts opts
|
66
|
+
@options[:out_stream].puts opts
|
67
67
|
exit
|
68
68
|
end
|
69
69
|
opts.on("--version", "Print version information") do
|
70
|
-
out_stream.puts
|
70
|
+
@options[:out_stream].puts Subcl::VERSION
|
71
71
|
exit
|
72
72
|
end
|
73
73
|
|
@@ -109,6 +109,8 @@ class Runner
|
|
109
109
|
subcl.queue(arg, :playlist, {:play => true, :clear => true})
|
110
110
|
when /^play-random$|^r$/
|
111
111
|
subcl.queue(arg, :randomSong, {:play => true, :clear => true})
|
112
|
+
when /^play-any$|^pn$|^p$/
|
113
|
+
subcl.queue(arg, :any, {:play => true, :clear => true})
|
112
114
|
when /^queue-next-song$|^ns$/
|
113
115
|
subcl.queue(arg, :song, {:insert => true})
|
114
116
|
when /^queue-next-artist$|^nr$/
|
@@ -117,6 +119,8 @@ class Runner
|
|
117
119
|
subcl.queue(arg, :album, {:insert => true})
|
118
120
|
when /^queue-next-playlist$|^np$/
|
119
121
|
subcl.queue(arg, :playlist, {:insert => true})
|
122
|
+
when /^queue-next-any$|^nn$|^n$/
|
123
|
+
subcl.queue(arg, :any, {:insert => true})
|
120
124
|
when /^queue-last-song$|^ls$/
|
121
125
|
subcl.queue(arg, :song)
|
122
126
|
when /^queue-last-artist$|^lr$/
|
@@ -125,6 +129,8 @@ class Runner
|
|
125
129
|
subcl.queue(arg, :album)
|
126
130
|
when /^queue-last-playlist$|^lp$/
|
127
131
|
subcl.queue(arg, :playlist)
|
132
|
+
when /^queue-last-any$|^ln$|^l$/
|
133
|
+
subcl.queue(arg, :any)
|
128
134
|
when "albumart-url"
|
129
135
|
arg = nil if arg.empty?
|
130
136
|
@options[:out_stream].puts subcl.albumart_url(arg)
|
@@ -137,7 +143,11 @@ class Runner
|
|
137
143
|
#pass through for player commands
|
138
144
|
subcl.send(command, [])
|
139
145
|
rescue NoMethodError
|
140
|
-
|
146
|
+
if subcl.configs[:play_any_on_unknown_command]
|
147
|
+
subcl.queue(args.join(" "), :any, {:play => true, :clear => true})
|
148
|
+
else
|
149
|
+
unknown(command)
|
150
|
+
end
|
141
151
|
end
|
142
152
|
end
|
143
153
|
end
|
data/lib/subcl/subcl.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
class Subcl
|
3
|
-
attr_accessor :player, :api, :notifier
|
3
|
+
attr_accessor :player, :api, :notifier, :configs
|
4
4
|
|
5
5
|
def initialize(options = {})
|
6
6
|
#TODO merge options and configs
|
@@ -9,7 +9,8 @@ class Subcl
|
|
9
9
|
:tty => true,
|
10
10
|
:insert => false,
|
11
11
|
:out_stream => STDOUT,
|
12
|
-
:err_stream => STDERR
|
12
|
+
:err_stream => STDERR,
|
13
|
+
:wildcard_order => %i{playlist album artist song}
|
13
14
|
}.merge! options
|
14
15
|
|
15
16
|
@out = @options[:out_stream]
|
@@ -36,7 +37,7 @@ class Subcl
|
|
36
37
|
@out.puts sprintf "%-20.20s %-20.20s %-20.20s %-4.4s", song[:title], song[:artist], song[:album], song[:year]
|
37
38
|
},
|
38
39
|
:album => proc { |album|
|
39
|
-
@out.puts sprintf "%-30.30s %-30.30s
|
40
|
+
@out.puts sprintf "%-30.30s %-30.30s %-4.4s", album[:name], album[:artist], album[:year]
|
40
41
|
},
|
41
42
|
:artist => proc { |artist|
|
42
43
|
@out.puts "#{artist[:name]}"
|
@@ -44,8 +45,11 @@ class Subcl
|
|
44
45
|
:playlist => proc { |playlist|
|
45
46
|
@out.puts "#{playlist[:name]} by #{playlist[:owner]}"
|
46
47
|
},
|
48
|
+
:any => proc { |thing|
|
49
|
+
#TODO this works, but looks confusing when multiple types are displayed
|
50
|
+
@display[thing[:type]].call(thing)
|
51
|
+
}
|
47
52
|
}
|
48
|
-
|
49
53
|
end
|
50
54
|
|
51
55
|
def albumart_url(size = nil)
|
@@ -80,8 +84,9 @@ class Subcl
|
|
80
84
|
rescue ArgumentError
|
81
85
|
raise ArgumentError, "random-songs takes an integer as argument"
|
82
86
|
end
|
83
|
-
else #song, album, artist, playlist
|
87
|
+
else #song, album, artist, playlist, any
|
84
88
|
entities = @api.search(query, type)
|
89
|
+
entities.sort!(&any_sorter(query)) if type == :any
|
85
90
|
entities = invoke_picker(entities, &@display[type])
|
86
91
|
@api.get_songs(entities)
|
87
92
|
end
|
@@ -99,6 +104,34 @@ class Subcl
|
|
99
104
|
@player.play if args[:play]
|
100
105
|
end
|
101
106
|
|
107
|
+
#returns a sorter proc for two hashes with the attribute :type and :name
|
108
|
+
#
|
109
|
+
#it will use split(" ") on query and then count how many words of query each
|
110
|
+
#:name contains. If two hashes have the same amount of query words,
|
111
|
+
#@options[:wildcard_order] is used
|
112
|
+
#
|
113
|
+
#the closest matches will be at the beginning
|
114
|
+
#
|
115
|
+
def any_sorter(query)
|
116
|
+
#TODO do things with query! find the things that match the query the most
|
117
|
+
order = @options[:wildcard_order]
|
118
|
+
lambda do |e1, e2|
|
119
|
+
cmp = match_score(e1, query) <=> match_score(e2, query)
|
120
|
+
if cmp == 0
|
121
|
+
out = order.index(e1[:type]) <=> order.index(e2[:type])
|
122
|
+
out
|
123
|
+
else
|
124
|
+
-cmp
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def match_score(entity, query)
|
130
|
+
query.split(' ').inject(0) do |memo, word|
|
131
|
+
memo + (entity[:name].downcase.include?(word.downcase) ? 1 : 0)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
102
135
|
def print(name, type)
|
103
136
|
entities = @api.search(name, type)
|
104
137
|
no_matches(type) if entities.empty?
|
@@ -141,6 +174,7 @@ class Subcl
|
|
141
174
|
return Picker.new(array).pick(&display_proc)
|
142
175
|
end
|
143
176
|
|
177
|
+
#these methods will be passed through to the underlying player
|
144
178
|
PLAYER_METHODS = %i{play pause toggle stop next previous rewind}
|
145
179
|
def method_missing(name, args)
|
146
180
|
raise NoMethodError unless PLAYER_METHODS.include? name
|
data/lib/subcl/subsonic_api.rb
CHANGED
@@ -151,22 +151,29 @@ class SubsonicAPI
|
|
151
151
|
params[:songCount] = max
|
152
152
|
params[:albumCount] = max
|
153
153
|
params[:artistCount] = max
|
154
|
+
#TODO need to search for playlists too!
|
154
155
|
else
|
155
156
|
raise "Cannot search for type '#{type}'"
|
156
157
|
end
|
157
158
|
|
158
159
|
doc = query('search3.view', params)
|
159
160
|
|
160
|
-
%i{artist album song}.collect_concat do |entity_type|
|
161
|
+
results = %i{artist album song}.collect_concat do |entity_type|
|
161
162
|
doc.elements.collect("subsonic-response/searchResult3/#{entity_type}") do |entity|
|
162
163
|
entity = Hash[entity.attributes.collect{ |key, val| [key.to_sym, val]}]
|
163
164
|
entity[:type] = entity_type
|
164
165
|
if entity_type == :song
|
165
166
|
entity[:stream_url] = stream_url(entity[:id])
|
167
|
+
entity[:name] = entity[:title]
|
166
168
|
end
|
167
169
|
entity
|
168
170
|
end
|
169
171
|
end
|
172
|
+
|
173
|
+
if type == :any
|
174
|
+
results += get_playlists(query)
|
175
|
+
end
|
176
|
+
return results
|
170
177
|
end
|
171
178
|
|
172
179
|
def query(method, params = {})
|
data/share/subcl.default
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
## Subcl Configuration
|
2
|
+
## Lines started with # are ignored
|
3
|
+
|
4
|
+
## Mandatory settings
|
5
|
+
server http://demo.subsonic.org
|
6
|
+
username guest5
|
7
|
+
password guest
|
8
|
+
|
9
|
+
## Optional settings
|
10
|
+
#notify_method awesome-client
|
11
|
+
#random_song_count 100
|
12
|
+
#max_search_results
|
13
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: subcl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Latzer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -61,7 +61,9 @@ files:
|
|
61
61
|
- lib/subcl/subcl.rb
|
62
62
|
- lib/subcl/subcl_error.rb
|
63
63
|
- lib/subcl/subsonic_api.rb
|
64
|
+
- lib/subcl/version.rb
|
64
65
|
- share/icon.png
|
66
|
+
- share/subcl.default
|
65
67
|
homepage: https://github.com/Tourniquet/subcl
|
66
68
|
licenses:
|
67
69
|
- MIT
|