listlace 0.2.0 → 0.2.1

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: 5e2d5c543763b463ddeb7d210dd9462eaca05944
4
- data.tar.gz: a91502ded9981eade141ba10f7ee90b4fdf0e9ee
3
+ metadata.gz: 81dd49a642dc3679a690ddc7b4107d614d2356c6
4
+ data.tar.gz: 49cfdc7c09b8e1835346c3d334796e2a25a81287
5
5
  SHA512:
6
- metadata.gz: da90361e229cf18c7f273ea58fa1bae79f2ae6e7f8b3fbfbf9d7e7163798c3653299562602863030a36937c0b98fd9a57b785ce119e707992a9a34fd0057142b
7
- data.tar.gz: 116b0aa40716b9cde245cb3042fcf0efd37b89a119f17fcbe36ad8697de08c30fa7f72966d73e1378d78e5d9f60337c3f7fbdd0519c42a438e11de7d77189823
6
+ metadata.gz: 1894c03f8bcc99d20c212bbc8c2b80ae37cf4ebe67bd44e6053cd64eb22a19223a7f2fbbf44fd22a40bebdf2a82cbe51e985d629c2811401dd277ad1fabbc2a9
7
+ data.tar.gz: fe454c5393bdedf4acbc145393ea94c83726670eeaf7b243e6b7df7f62fdf856ccec7a377427082117bc22d15eebde2de6f80559df5900b8924ad1d0964fbd68
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- listlace (0.1.0)
4
+ listlace (0.2.0)
5
5
  bundler
6
6
  pry
7
7
  ruby-mpd
data/README.md CHANGED
@@ -26,7 +26,25 @@ Query your music library and build playlists using selectors. There are selector
26
26
  ♫> artist "thirsty cups"
27
27
  => [8 songs]
28
28
 
29
- It returned an Array of 8 Songs. To do an exact, case-sensitive search, use `artist_exact`:
29
+ It returned an Array of 8 Songs.
30
+
31
+ You can pass multiple queries to a selector, and it will select all songs that match **any** of those queries:
32
+
33
+ ♫> _why = artist(:the_thirsty_cups, :moonboots, :the_child_who_was_a_keyhole)
34
+ => [33 songs]
35
+
36
+ You can chain selectors together to narrow down a playlist:
37
+
38
+ ♫> _why.album(:elfin_princess).title(/^the/)
39
+ => [1 song]
40
+
41
+ ### String selectors
42
+
43
+ The string selectors are: `title`, `artist`, `album`, and `genre`.
44
+
45
+ String selectors have "exact" counterparts: `title_exact`, `artist_exact`, `album_exact`, and `genre_exact`.
46
+
47
+ Use these "exact" selectors to do an exact, case-sensitive search:
30
48
 
31
49
  ♫> artist_exact "thirsty cups"
32
50
  => []
@@ -35,7 +53,7 @@ It returned an Array of 8 Songs. To do an exact, case-sensitive search, use `art
35
53
 
36
54
  The first one didn't match anything, the second one matched the same 8 songs.
37
55
 
38
- You can also pass in a `Regexp` or a `Symbol`:
56
+ You can also pass in a `Regexp` or a `Symbol` to a string selector:
39
57
 
40
58
  ♫> artist :the_thirsty_cups
41
59
  => [8 songs]
@@ -44,9 +62,39 @@ You can also pass in a `Regexp` or a `Symbol`:
44
62
 
45
63
  Underscores in symbols are interpreted as spaces.
46
64
 
47
- Here's a list of all tag selectors: `title`, `title_exact`, `artist`, `artist_exact`, `album`, `album_exact`, `genre`, and `genre_exact`.
65
+ ### Numeric selectors
66
+
67
+ The numeric selectors are: `track`, `disc`, and `year`.
68
+
69
+ In addition to `Integers`, you can pass a `Range` to match against or a `Hash` that specifies one or more comparison operators:
70
+
71
+ ♫> year 2000, 2004
72
+ => [362 songs]
73
+ ♫> year 1990..1999
74
+ => [521 songs]
75
+ ♫> year lt: 1970
76
+ => [76 songs]
77
+
78
+ The comparison operators are `:eq`, `:ne`, `:gt`, `:ge`, `:lt`, and `:le`. You can also just use `:==`, `:>`, `:>=`, `:<`, and `:<=`.
79
+
80
+ ### Time selectors
81
+
82
+ The time selector is just `time`, which matches the length of songs in seconds.
48
83
 
49
- In addition to tag selectors, here are some special selectors:
84
+ It's basically a numeric selector, but you can pass a String containing a formatted time in place of a number:
85
+
86
+ ♫> time "1:23"
87
+ => [2 songs]
88
+ ♫> time "8:00"..."9:00"
89
+ => [79 songs]
90
+ ♫> time gt: "20:00"
91
+ => [5 songs]
92
+ ♫> time lt: 20
93
+ => [27 songs]
94
+
95
+ "1:23" means 1 minute and 23 seconds, and "1:23:45" means 1 hour, 23 minutes, 45 seconds. If you just pass an integer, it is the number of seconds.
96
+
97
+ ### Special selectors
50
98
 
51
99
  ### all
52
100
 
@@ -62,20 +110,6 @@ In addition to tag selectors, here are some special selectors:
62
110
  ♫> none
63
111
  => []
64
112
 
65
- ### search
66
-
67
- `search` will match **any** tag that contains your query.
68
-
69
- ♫> search "thirsty"
70
- => [8 songs]
71
-
72
- ### where, where\_exact
73
-
74
- `where` and `where_exact` let you specify multiple queries for different tags:
75
-
76
- ♫> where(artist: "thirsty", title: "belljar")
77
- => [1 song]
78
-
79
113
  ## Commands
80
114
 
81
115
  ### p
@@ -105,15 +139,15 @@ In addition to tag selectors, here are some special selectors:
105
139
  `list` with no arguments lists all the songs in your music library. If you pass it a playlist, it will list all the songs in that playlist.
106
140
 
107
141
  ♫> list
108
- Air - 10 000 Hz Legend - Electronic Performers
109
- Air - 10 000 Hz Legend - How Does It Make You Feel?
110
- Air - 10 000 Hz Legend - Radio #1
142
+ Air - 10 000 Hz Legend - Electronic Performers (5:36)
143
+ Air - 10 000 Hz Legend - How Does It Make You Feel? (4:38)
144
+ Air - 10 000 Hz Legend - Radio #1 (4:23)
111
145
  ...
112
146
  ♫> list title :fish
113
- Eisley - Currents - Blue Fish
114
- Moonboots - Elfin Princess - The Fish Said Hello
115
- Radiohead - In Rainbows - Weird Fishes/Arpeggi
116
- Thee More Shallows - A History of Sport Fishing - A History of Sport Fishing
147
+ Eisley - Currents - Blue Fish (4:02)
148
+ Moonboots - Elfin Princess - The Fish Said Hello (2:44)
149
+ Radiohead - In Rainbows - Weird Fishes/Arpeggi (5:18)
150
+ Thee More Shallows - A History of Sport Fishing - A History of Sport Fishing (7:00)
117
151
 
118
152
  ### artists
119
153
 
@@ -158,6 +192,18 @@ In addition to tag selectors, here are some special selectors:
158
192
  ♫> genres title(:constantinople)
159
193
  TMBG (1 song)
160
194
 
195
+ ### years
196
+
197
+ `years` with no arguments lists all the years in your music library. If you pass it a playlist, it will list all the years in that playlist.
198
+
199
+ ♫> years
200
+ 2007 (205 songs)
201
+ 2010 (220 songs)
202
+ 2012 (148 songs)
203
+ ...
204
+ ♫> years artist :the_avalanches
205
+ 2000 (18 songs)
206
+
161
207
  ### mpd
162
208
 
163
209
  `mpd` gives you an instance of the `MPDClient` object, which you can use to send any `mpd` command that isn't available in Listlace yet. This'll probably be removed once I actually implement all the commands.
data/lib/listlace.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  require "ruby-mpd"
2
2
 
3
- require "listlace/core_ext/array"
4
-
5
3
  require "listlace/commands"
6
4
  require "listlace/selectors"
5
+ require "listlace/time_helpers"
6
+
7
+ require "listlace/core_ext/array"
7
8
 
8
9
  class Listlace
9
10
  attr_reader :mpd
@@ -39,7 +39,8 @@ class Listlace
39
39
 
40
40
  def list(playlist = nil)
41
41
  (playlist || all).each do |song|
42
- puts "#{song.artist} - #{song.album} - #{song.title}"
42
+ time = TimeHelpers.format_time(song.time)
43
+ puts "#{song.artist} - #{song.album} - #{song.title} (#{time})"
43
44
  end
44
45
  nil
45
46
  end
@@ -67,6 +68,14 @@ class Listlace
67
68
  end
68
69
  nil
69
70
  end
71
+
72
+ def years(playlist = nil)
73
+ (playlist || all).group_by(&:date).each do |year, songs|
74
+ plural = (songs.length == 1) ? "" : "s"
75
+ puts "#{year} (#{songs.length} song#{plural})"
76
+ end
77
+ nil
78
+ end
70
79
  end
71
80
  end
72
81
 
@@ -2,7 +2,31 @@ class Array
2
2
  def playlist?
3
3
  @is_playlist ||= all? { |x| x.is_a? MPD::Song }
4
4
  end
5
-
5
+
6
+ Listlace::Selectors::STRING_SELECTORS.each do |tag|
7
+ define_method(tag) do |*queries|
8
+ Listlace::Selectors.string_selector(tag, self, false, *queries)
9
+ end
10
+
11
+ define_method("#{tag}_exact") do |*queries|
12
+ Listlace::Selectors.string_selector(tag, self, true, *queries)
13
+ end
14
+ end
15
+
16
+ Listlace::Selectors::NUMERIC_SELECTORS.each do |tag|
17
+ define_method(tag) do |*queries|
18
+ Listlace::Selectors.numeric_selector(tag, self, *queries)
19
+ end
20
+ end
21
+
22
+ Listlace::Selectors::TIME_SELECTORS.each do |tag|
23
+ define_method(tag) do |*queries|
24
+ Listlace::Selectors.time_selector(tag, self, *queries)
25
+ end
26
+ end
27
+
28
+ alias year date
29
+
6
30
  alias _original_inspect inspect
7
31
  def inspect
8
32
  if playlist?
@@ -1,43 +1,89 @@
1
1
  class Listlace
2
2
  module Selectors
3
- TAG_SELECTORS = %w(title artist album genre)
4
-
5
- TAG_SELECTORS.each do |tag|
6
- define_method(tag) do |what|
7
- case what
8
- when Regexp
9
- what = Regexp.new(what.source, Regexp::IGNORECASE) # case-insensitize
10
- all.select { |song| song.send(tag).to_s =~ what }
11
- when Symbol
12
- mpd.where(tag => what.to_s.tr("_", " "))
13
- when String
14
- mpd.where(tag => what)
15
- end
3
+ STRING_SELECTORS = %w(title artist album genre)
4
+ NUMERIC_SELECTORS = %w(track date disc)
5
+ TIME_SELECTORS = %w(time)
6
+
7
+ (STRING_SELECTORS | NUMERIC_SELECTORS | TIME_SELECTORS).each do |tag|
8
+ define_method(tag) do |*queries|
9
+ all.send(tag, *queries)
16
10
  end
17
11
 
18
- define_method("#{tag}_exact") do |what|
19
- mpd.where({tag => what}, {strict: true})
12
+ if STRING_SELECTORS.include? tag
13
+ define_method("#{tag}_exact") do |*queries|
14
+ all.send("#{tag}_exact", *queries)
15
+ end
20
16
  end
21
17
  end
22
18
 
19
+ alias year date
20
+
23
21
  def all
24
- mpd.songs
22
+ @all ||= mpd.songs
25
23
  end
26
24
 
27
25
  def none
28
26
  []
29
27
  end
30
28
 
31
- def search(what)
32
- mpd.where any: what
29
+ def self.string_selector(tag, playlist, exact, *queries)
30
+ if exact
31
+ playlist.select { |song| song.send(tag).to_s == query.to_s }
32
+ else
33
+ queries.map do |query|
34
+ case query
35
+ when Regexp
36
+ query = Regexp.new(query.source, Regexp::IGNORECASE) # case-insensitize
37
+ playlist.select { |song| song.send(tag).to_s =~ query }
38
+ when Symbol
39
+ playlist.select { |song| song.send(tag).to_s.downcase[query.to_s.downcase.tr("_", " ")] }
40
+ when String
41
+ playlist.select { |song| song.send(tag).to_s.downcase[query.downcase] }
42
+ end
43
+ end.inject(:|)
44
+ end
33
45
  end
34
-
35
- def where(params)
36
- mpd.where(params)
46
+
47
+ def self.numeric_selector(tag, playlist, *queries)
48
+ queries.map do |query|
49
+ case query
50
+ when Numeric
51
+ playlist.select { |song| song.send(tag) == query }
52
+ when Hash
53
+ query.map do |op, value|
54
+ op = { eq: "==", ne: "!=", lt: "<", le: "<=", gt: ">", ge: ">=" }[op] || op
55
+ playlist.select { |song| song.send(tag).to_i.send(op, value) }
56
+ end.inject(:&)
57
+ when Range
58
+ playlist.select { |song| query === song.send(tag) }
59
+ end
60
+ end.inject(:|)
37
61
  end
38
62
 
39
- def where_exact(params)
40
- mpd.where(params, {strict: true})
63
+ def self.time_selector(tag, playlist, *queries)
64
+ queries.map! do |query|
65
+ case query
66
+ when String
67
+ TimeHelpers.parse_time(query)
68
+ when Range
69
+ Range.new(
70
+ TimeHelpers.parse_time(query.begin.to_s),
71
+ TimeHelpers.parse_time(query.end.to_s),
72
+ query.exclude_end?
73
+ )
74
+ when Hash
75
+ query.each do |op, value|
76
+ if value.is_a? String
77
+ query[op] = TimeHelpers.parse_time(value)
78
+ end
79
+ end
80
+ query
81
+ else
82
+ query
83
+ end
84
+ end
85
+
86
+ numeric_selector(tag, playlist, *queries)
41
87
  end
42
88
  end
43
89
  end
@@ -0,0 +1,34 @@
1
+ class Listlace
2
+ module TimeHelpers
3
+ extend self
4
+
5
+ # Helper method to format a number of seconds as a string like "1:03:56".
6
+ def format_time(seconds, options = {})
7
+ raise ArgumentError, "can't format negative time" if seconds < 0
8
+
9
+ hours = seconds / 3600
10
+ minutes = (seconds / 60) % 60
11
+ seconds = seconds % 60
12
+
13
+ if hours > 0
14
+ "%d:%02d:%02d" % [hours, minutes, seconds]
15
+ else
16
+ "%d:%02d" % [minutes, seconds]
17
+ end
18
+ end
19
+
20
+ # Helper method to parse a string like "1:03:56" and return the number of
21
+ # seconds that time length represents.
22
+ def parse_time(string)
23
+ parts = string.split(":", -1).map(&:to_i)
24
+
25
+ raise ArgumentError, "too many parts" if parts.length > 3
26
+ raise ArgumentError, "can't parse negative numbers" if parts.any? { |x| x < 0 }
27
+
28
+ parts.unshift(0) until parts.length == 3
29
+ hours, minutes, seconds = parts
30
+ hours * 3600 + minutes * 60 + seconds
31
+ end
32
+ end
33
+ end
34
+
data/listlace.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "listlace"
3
- s.version = "0.2.0"
4
- s.date = "2014-03-22"
3
+ s.version = "0.2.1"
4
+ s.date = "2014-03-24"
5
5
  s.summary = "An mpd (music player daemon) client with a Ruby shell as the interface."
6
6
  s.description = "Listlace is an mpd (music player daemon) client with a Ruby shell as the interface."
7
7
  s.author = "Jeremy Ruten"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listlace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Ruten
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-22 00:00:00.000000000 Z
11
+ date: 2014-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -83,6 +83,7 @@ files:
83
83
  - lib/listlace/commands.rb
84
84
  - lib/listlace/core_ext/array.rb
85
85
  - lib/listlace/selectors.rb
86
+ - lib/listlace/time_helpers.rb
86
87
  - listlace.gemspec
87
88
  homepage: http://github.com/yjerem/listlace
88
89
  licenses: