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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eafcc67f7917cd55aa322705366dc3361e7e6545
4
- data.tar.gz: deb40883dc7d4f41a3f3ce57880a25bf2c11b1be
3
+ metadata.gz: fb19261a8a56302438bd138708e9e12384b91da6
4
+ data.tar.gz: 0d2078fd2198df4d12232547d4eadb524e2ff925
5
5
  SHA512:
6
- metadata.gz: 476ac05a7de41e4079078e7a2a6e4d1d5c20d5fa5c29766176a0eabd3eda3b58faa2c6bf60a666dcc7bfaa19dff892d19e8a9b3849ed578eee8f21516b44b3ab
7
- data.tar.gz: 7ac85d384fd3da414123433db4bb59a64995e375fa287d73faf7a7797ad0aaf317af8cebf71493f3d9318ccae12a7a685ec00d9cd31237110c09b180510b5bd0
6
+ metadata.gz: 14ecdf1251f53bd8f92155064d7a3c5cefa6a29a07e54eebcbd6007b5a96a65d72e13607936eab27ad4004e140308588b83f42d4afc565ab03274991430b58ab
7
+ data.tar.gz: 88c24c10b931983f6750b267c0ae0250719d81668ab65f4f827f9f0aafe78da1c048eb3385c81544173ee50cccaec911adbca0e99cc3caccbc3299fbf6b31138
@@ -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)
@@ -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 = '~/.subcl')
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
- @filename = File.expand_path(file)
14
- unless File.file?(@filename)
15
- raise "Config file not found"
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(@filename).each_line do |line|
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
@@ -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
- $stderr.print "[#{i}] "
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
@@ -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
- @mpd.pause = @mpd.playing? ? 1 : 0
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
@@ -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 Configs.new[:app_version]
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
- unknown(command)
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
@@ -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 %-4.4s", album[:name], album[:artist], album[:year]
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
@@ -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 = {})
@@ -0,0 +1,3 @@
1
+ class Subcl
2
+ VERSION = '1.1.2'
3
+ end
@@ -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.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-02-25 00:00:00.000000000 Z
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