listlace 0.0.5 → 0.0.6

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,196 @@
1
+ module Listlace
2
+ class Library
3
+ module Selectors
4
+ STRING_SELECTORS = %w(name artist composer album album_artist genre comments location)
5
+ INTEGER_SELECTORS = %w(disc_number disc_count track_number track_count year bit_rate sample_rate play_count skip_count rating)
6
+
7
+ STRING_SELECTORS.each do |column|
8
+ define_method(column) do |*args|
9
+ options = args.last.is_a?(Hash) ? args.pop : {}
10
+
11
+ playlists = args.map { |query| string_selector(column, query, options) }
12
+ playlist *playlists
13
+ end
14
+ end
15
+
16
+ INTEGER_SELECTORS.each do |column|
17
+ define_method(column) do |*args|
18
+ playlists = args.map { |arg| integer_selector(column, arg) }
19
+ playlist *playlists
20
+ end
21
+ end
22
+
23
+ # rename the "name" selector to "song"
24
+ alias_method :song, :name
25
+ remove_method :name
26
+
27
+ # The length selector is an integer selector for the length of a track. A
28
+ # plain integer given to it represents the number of seconds. It can also take
29
+ # a String in the format "1:23", to represent 83 seconds, for example. These
30
+ # can be part of a Range, as usual: "1:23".."2:05", for example.
31
+ def length(*args)
32
+ normalize = lambda do |value|
33
+ case value
34
+ when String
35
+ Track.parse_time(value)
36
+ when Integer
37
+ value * 1000
38
+ when Range
39
+ (normalize.(value.begin))..(normalize.(value.end))
40
+ end
41
+ end
42
+
43
+ playlists = args.map do |arg|
44
+ if arg.is_a? Hash
45
+ key = arg.keys.first
46
+ arg[key] = normalize.(arg[key])
47
+ else
48
+ arg = normalize.(arg)
49
+ end
50
+
51
+ # If they want tracks of length "0:05", for example, we need to look for
52
+ # tracks that are from 5000 to 5999 milliseconds long.
53
+ if arg.is_a? Integer
54
+ arg = (arg)..(arg + 999)
55
+ end
56
+
57
+ integer_selector(:total_time, arg)
58
+ end
59
+
60
+ playlist *playlists
61
+ end
62
+
63
+ # Makes a playlist out of tracks that match the string query on the given
64
+ # column. It's SQL underneath, so you can use % and _ as wildcards in the
65
+ # query. By default, % wildcards are inserted on the left and right of your
66
+ # query. Use the :match option to change this:
67
+ #
68
+ # :match => :middle "%query%" (default)
69
+ # :match => :left "query%"
70
+ # :match => :right "%query"
71
+ # :match => :exact "query"
72
+ #
73
+ # This method shouldn't have to be used directly. Many convenient methods are
74
+ # generated for you, one for each string field you may want to select on.
75
+ # These are: artist, composer, album, album_artist, genre, comments, location.
76
+ # For example:
77
+ #
78
+ # artist :muse, match: :exact #=> playlist (108 tracks)
79
+ # composer :rachmanino #=> playlist (33 tracks)
80
+ #
81
+ # To match the name of a track, use song:
82
+ #
83
+ # song "frontier psychiatrist" #=> playlist (1 track)
84
+ #
85
+ def string_selector(column, query, options = {})
86
+ options[:match] ||= :middle
87
+
88
+ query = {
89
+ exact: "#{query}",
90
+ left: "#{query}%",
91
+ right: "%#{query}",
92
+ middle: "%#{query}%"
93
+ }[options[:match]]
94
+
95
+ tracks = library.tracks.arel_table
96
+ library.tracks.where(tracks[column].matches(query)).all
97
+ end
98
+
99
+ # Makes a playlist out of tracks that satisfy certain conditions on the given
100
+ # integer column. You can pass an exact value to check for equality, a range,
101
+ # or a hash that specifies greater-than and less-than options like this:
102
+ #
103
+ # integer_selector :year, greater_than: 2000 #=> playlist (3555 tracks)
104
+ #
105
+ # The possible operators, with their shortcuts, are:
106
+ #
107
+ # :greater_than / :gt
108
+ # :less_than / :lt
109
+ # :greater_than_or_equal / :gte
110
+ # :less_than_or_equal / :lte
111
+ # :not_equal / :ne
112
+ #
113
+ # Note: You can only use one of these operators at a time. If you want a
114
+ # range, use a Range.
115
+ #
116
+ # This method shouldn't have to be used directly. Many convenient methods are
117
+ # generated for you, one for each integer field you may want to select on.
118
+ # These are: disc_number, disc_count, track_number, track_count, year,
119
+ # bit_rate, sample_rate, play_count, skip_count, rating, length. Length is
120
+ # special, it can take any of the time formats that the seek command can. For
121
+ # example:
122
+ #
123
+ # year 2010..2012 #=> playlist (1060 tracks)
124
+ # length gt: "4:00" #=> playlist (2543 tracks)
125
+ #
126
+ def integer_selector(column, value_or_options)
127
+ if value_or_options.is_a? Hash
128
+ operator = {
129
+ greater_than: ">",
130
+ gt: ">",
131
+ less_than: "<",
132
+ lt: "<",
133
+ greater_than_or_equal: ">=",
134
+ gte: ">=",
135
+ less_than_or_equal: "<=",
136
+ lte: "<=",
137
+ not_equal: "<>",
138
+ ne: "<>"
139
+ }[value_or_options.keys.first]
140
+ library.tracks.where("tracks.#{column} #{operator} ?", value_or_options.values.first).all
141
+ else
142
+ library.tracks.where(column => value_or_options).all
143
+ end
144
+ end
145
+
146
+ # Creates or looks up a playlist. You can pass any number of multiple types of
147
+ # objects to make a playlist:
148
+ #
149
+ # Track: Makes a playlist with one track
150
+ # ActiveRecord::Relation: Makes a playlist out of the resulting Track or Playlist records
151
+ # Symbol or String: Tries to retrieve a saved playlist with the given name.
152
+ # If it can't find one, it creates a new one.
153
+ #
154
+ # You can also pass in the results of a selector. In this way you can create a
155
+ # playlist out of many smaller pieces.
156
+ #
157
+ # If given no arguments, it returns a blank playlist.
158
+ #
159
+ def playlist(*args)
160
+ args.map do |object|
161
+ case object
162
+ when Track
163
+ [object]
164
+ when Playlist
165
+ pl = [object.tracks.all]
166
+ pl.name = object.name
167
+ pl
168
+ when Array
169
+ if object.playlist?
170
+ object
171
+ else
172
+ playlist *object
173
+ end
174
+ when ActiveRecord::Relation
175
+ if object.table.name == "tracks"
176
+ object.all
177
+ elsif object.table.name == "playlists"
178
+ playlist *object.all
179
+ end
180
+ when Symbol, String
181
+ playlists = library.playlists.arel_table
182
+ if playlist = library.playlists.where(playlists[:name].matches(object.to_s)).first
183
+ pl = playlist.tracks.all
184
+ pl.name = playlist.name
185
+ pl
186
+ else
187
+ pl = []
188
+ pl.name = object
189
+ pl
190
+ end
191
+ end
192
+ end.inject(:+)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,41 @@
1
+ require "listlace/library/database"
2
+ require "listlace/library/selectors"
3
+
4
+ module Listlace
5
+ class Library
6
+ def initialize(options = {})
7
+ options[:db_path] ||= "library"
8
+ options[:db_adapter] ||= "sqlite3"
9
+
10
+ unless File.exists? Listlace::DIR
11
+ FileUtils.mkdir_p Listlace::DIR
12
+ end
13
+
14
+ @db_path = options[:db_path]
15
+ @db_path = "#{Listlace::DIR}/#{@db_path}" unless @db_path.include? "/"
16
+ @db_path = "#{@db_path}.sqlite3" unless @db_path =~ /\.sqlite3$/
17
+
18
+ @db_adapter = options[:db_adapter]
19
+
20
+ Database.disconnect if Database.connected?
21
+ Database.connect(@db_adapter, @db_path)
22
+ Database.generate_schema unless Database.exists?(@db_path)
23
+ end
24
+
25
+ def tracks
26
+ Track.scoped
27
+ end
28
+
29
+ def playlists
30
+ Playlist.scoped
31
+ end
32
+
33
+ def size
34
+ tracks.length
35
+ end
36
+
37
+ def wipe
38
+ Database.wipe(@db_adapter, @db_path)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ require "listlace/models/track"
2
+ require "listlace/models/playlist"
3
+ require "listlace/models/playlist_item"
@@ -0,0 +1,53 @@
1
+ module Listlace
2
+ class Player
3
+ # This is a simple MPlayer wrapper, it just handles opening the MPlayer
4
+ # process, hooking into when mplayer exits (when the song is done), and
5
+ # issuing commands through the slave protocol.
6
+ class MPlayer
7
+ def initialize(track, &on_quit)
8
+ cmd = "/usr/bin/mplayer -slave -quiet #{Shellwords.shellescape(track.location)}"
9
+ @pid, @stdin, @stdout, @stderr = Open4.popen4(cmd)
10
+ @paused = false
11
+ @extra_lines = 0
12
+
13
+ until @stdout.gets["playback"]
14
+ end
15
+
16
+ @quit_hook_active = false
17
+ @quit_hook = Thread.new do
18
+ Process.wait(@pid)
19
+ @quit_hook_active = true
20
+ on_quit.call
21
+ end
22
+ end
23
+
24
+ def command(cmd, options = {})
25
+ if cmd == "pause"
26
+ @paused = !@paused
27
+ elsif @paused
28
+ cmd = "pausing #{cmd}"
29
+ end
30
+
31
+ @stdin.puts cmd
32
+
33
+ if options[:expect_answer]
34
+ answer = "\n"
35
+ answer = @stdout.gets.sub("\e[A\r\e[K", "") while answer == "\n"
36
+ answer
37
+ end
38
+ end
39
+
40
+ def quit
41
+ @quit_hook.kill unless @quit_hook_active
42
+ command "quit" if alive?
43
+ end
44
+
45
+ def alive?
46
+ Process.getpgid(@pid)
47
+ true
48
+ rescue Errno::ESRCH
49
+ false
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,5 @@
1
+ require "listlace/player/mplayer"
2
+
1
3
  module Listlace
2
4
  # This is the music box. It contains a queue, which is an array of tracks. It
3
5
  # then plays these tracks sequentially. The buttons for play, pause, next,
@@ -7,7 +9,8 @@ module Listlace
7
9
 
8
10
  def initialize
9
11
  @mplayer = nil
10
- @queue = PlaylistArray.new([], :queue)
12
+ @queue = []
13
+ @queue.name = :queue
11
14
  @current_track = nil
12
15
  @current_track_index = nil
13
16
  @paused = false
@@ -16,13 +19,21 @@ module Listlace
16
19
  end
17
20
 
18
21
  def queue(playlist = nil)
19
- @queue << playlist if playlist
22
+ if playlist.is_a? Array
23
+ if @queue.empty? && playlist.name && !playlist.name.empty?
24
+ @queue = playlist.dup
25
+ else
26
+ @queue += playlist
27
+ @queue.name = :queue
28
+ end
29
+ end
20
30
  @queue.dup
21
31
  end
22
32
 
23
33
  def clear
24
34
  stop
25
35
  @queue.clear
36
+ @queue.name = :queue
26
37
  end
27
38
 
28
39
  def empty?
@@ -93,6 +104,8 @@ module Listlace
93
104
  end
94
105
 
95
106
  def skip(n = 1)
107
+ @current_track.increment! :skip_count
108
+ @current_track.update_column :skip_date, Time.now
96
109
  change_track(n)
97
110
  end
98
111
 
@@ -159,6 +172,10 @@ module Listlace
159
172
  private
160
173
 
161
174
  def change_track(by = 1, options = {})
175
+ if options[:auto]
176
+ @current_track.increment! :play_count
177
+ @current_track.update_column :play_date_utc, Time.now
178
+ end
162
179
  @current_track_index += by
163
180
  if options[:auto] && @repeat_mode
164
181
  case @repeat_mode
data/lib/listlace.rb CHANGED
@@ -1,37 +1,22 @@
1
- module Listlace
2
- extend self
3
-
4
- DIR = ENV["LISTLACE_DIR"] || (ENV["HOME"] + "/.listlace")
5
- PROMPT = [proc { ">> " }, proc { " | " }]
6
- PRINT = proc do |output, value|
7
- unless value.nil?
8
- Pry::DEFAULT_PRINT.call(output, value)
9
- end
10
- end
11
- end
12
-
13
1
  require "open4"
14
2
  require "shellwords"
15
3
  require "active_record"
16
4
  require "fileutils"
5
+ require "plist"
6
+ require "active_support/core_ext/string"
17
7
 
18
- require "listlace/database"
8
+ require "listlace/array_ext"
9
+ require "listlace/library"
19
10
  require "listlace/player"
20
- require "listlace/mplayer"
21
- require "listlace/playlist_array"
22
-
23
- require "listlace/models/track"
24
- require "listlace/models/playlist"
25
- require "listlace/models/playlist_item"
11
+ require "listlace/commands"
12
+ require "listlace/models"
26
13
 
27
- require "listlace/commands/library"
28
- require "listlace/commands/playback"
29
- require "listlace/commands/selectors"
30
- require "listlace/commands/volume"
31
- require "listlace/commands/queue"
14
+ module Listlace
15
+ extend Listlace::Library::Selectors
32
16
 
33
- $player = Listlace::Player.new
17
+ class << self
18
+ attr_accessor :library, :player
19
+ end
34
20
 
35
- at_exit do
36
- Listlace.stop
21
+ DIR = ENV["LISTLACE_DIR"] || (ENV["HOME"] + "/.listlace")
37
22
  end
data/listlace.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "listlace"
3
- s.version = "0.0.5"
4
- s.date = "2012-08-26"
3
+ s.version = "0.0.6"
4
+ s.date = "2012-08-31"
5
5
  s.summary = "A music player in a REPL."
6
6
  s.description = "Listlace is a music player which is interacted with through a Ruby REPL."
7
7
  s.author = "Jeremy Ruten"
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.requirements << "mplayer"
13
13
  s.executables << "listlace"
14
14
 
15
- s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "listlace.gemspec", "README"]
15
+ s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "listlace.gemspec", "README.md", "README.old"]
16
16
  s.files += ["bin/listlace"]
17
17
  s.files += Dir["lib/**/*.rb"]
18
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listlace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-26 00:00:00.000000000 Z
12
+ date: 2012-08-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pry
@@ -134,20 +134,22 @@ files:
134
134
  - Gemfile.lock
135
135
  - LICENSE
136
136
  - listlace.gemspec
137
- - README
137
+ - README.md
138
+ - README.old
138
139
  - bin/listlace
139
- - lib/listlace/commands/library.rb
140
- - lib/listlace/commands/playback.rb
141
- - lib/listlace/commands/queue.rb
142
- - lib/listlace/commands/selectors.rb
143
- - lib/listlace/commands/volume.rb
144
- - lib/listlace/database.rb
140
+ - lib/listlace/array_ext.rb
141
+ - lib/listlace/commands/library_commands.rb
142
+ - lib/listlace/commands/player_commands.rb
143
+ - lib/listlace/commands.rb
144
+ - lib/listlace/library/database.rb
145
+ - lib/listlace/library/selectors.rb
146
+ - lib/listlace/library.rb
145
147
  - lib/listlace/models/playlist.rb
146
148
  - lib/listlace/models/playlist_item.rb
147
149
  - lib/listlace/models/track.rb
148
- - lib/listlace/mplayer.rb
150
+ - lib/listlace/models.rb
151
+ - lib/listlace/player/mplayer.rb
149
152
  - lib/listlace/player.rb
150
- - lib/listlace/playlist_array.rb
151
153
  - lib/listlace.rb
152
154
  homepage: http://github.com/yjerem/listlace
153
155
  licenses:
@@ -1,85 +0,0 @@
1
- require "plist"
2
- require "active_support/core_ext/string"
3
-
4
- module Listlace
5
- # Imports the music library from another program. Currently only iTunes is
6
- # supported.
7
- def import(from, path)
8
- if not File.exists?(path)
9
- puts "File '%s' doesn't exist." % [path]
10
- elsif from == :itunes
11
- puts "Parsing XML..."
12
- data = Plist::parse_xml(path)
13
-
14
- puts "Importing #{data['Tracks'].length} tracks..."
15
- num_tracks = 0
16
- whitelist = Track.new.attributes.keys
17
- data["Tracks"].each do |track_id, row|
18
- # row already contains a hash of attributes almost ready to be passed to
19
- # ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
20
- # to "play_count".
21
- attributes = row.inject({}) do |acc, (key, value)|
22
- attribute = key.gsub(" ", "").underscore
23
- attribute = "original_id" if attribute == "track_id"
24
- acc[attribute] = value if whitelist.include? attribute
25
- acc
26
- end
27
-
28
- # change iTunes' URL-style locations into simple paths
29
- if attributes["location"] && attributes["location"] =~ /^file:\/\//
30
- attributes["location"].sub! /^file:\/\/localhost/, ""
31
-
32
- # CGI::unescape changes plus signs to spaces. This is a work around to
33
- # keep the plus signs.
34
- attributes["location"].gsub! "+", "%2B"
35
-
36
- attributes["location"] = CGI::unescape(attributes["location"])
37
- end
38
-
39
- track = Track.new(attributes)
40
-
41
- if track.kind =~ /audio/
42
- if track.save
43
- num_tracks += 1
44
- end
45
- else
46
- puts "[skipping non-audio file]"
47
- end
48
- end
49
- puts "Imported #{num_tracks} tracks successfully."
50
-
51
- puts "Importing #{data['Playlists'].length} playlists..."
52
- num_playlists = 0
53
- data["Playlists"].each do |playlist_data|
54
- playlist = Playlist.new(name: playlist_data["Name"])
55
-
56
- if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist.name
57
- puts "[skipping \"#{playlist.name}\" playlist]"
58
- else
59
- if playlist.save
60
- playlist_data["Playlist Items"].map(&:values).flatten.each.with_index do |track_id, i|
61
- playlist_item = PlaylistItem.new(position: i)
62
- playlist_item.playlist = playlist
63
- if playlist_item.track = Track.where(original_id: track_id).first
64
- playlist_item.save!
65
- end
66
- end
67
- num_playlists += 1
68
- end
69
- end
70
- end
71
- puts "Imported #{num_playlists} playlists successfully."
72
- end
73
- end
74
-
75
- # Wipes the database. With no arguments, it just asks "Are you sure?" without
76
- # doing anything. To actually wipe the database, pass :yes_im_sure.
77
- def wipe_library(are_you_sure = :nope)
78
- if are_you_sure == :yes_im_sure
79
- Database.wipe
80
- puts "Library wiped."
81
- else
82
- puts "Are you sure? If you are, then type: wipe_library :yes_im_sure"
83
- end
84
- end
85
- end
@@ -1,137 +0,0 @@
1
- module Listlace
2
- REPEAT_SYMBOL = "\u221E"
3
- TIMES_SYMBOL = "\u00D7"
4
-
5
- # The play command. With no arguments, it either resumes playback or starts
6
- # playing the queue. With arguments, it replaces the queue with the given
7
- # tracks and starts playing.
8
- def p(*tracks)
9
- if tracks.empty?
10
- if $player.paused?
11
- $player.resume
12
- status
13
- elsif $player.started?
14
- if $player.speed == 1
15
- $player.pause
16
- status
17
- else
18
- $player.set_speed 1
19
- status
20
- end
21
- else
22
- if $player.empty?
23
- puts "Nothing to play."
24
- else
25
- $player.start
26
- status
27
- end
28
- end
29
- else
30
- $player.stop
31
- $player.clear
32
- q *tracks
33
- p
34
- end
35
- end
36
-
37
- # Stops playback. The queue is still intact, but goes back to the beginning
38
- # when playback is started again.
39
- def stop
40
- $player.stop
41
- puts "Stopped."
42
- end
43
-
44
- # Start the current track from the beginning.
45
- def restart
46
- $player.restart
47
- status
48
- end
49
-
50
- # Go back one song in the queue.
51
- def back(n = 1)
52
- if $player.back(n)
53
- status
54
- else
55
- puts "End of queue."
56
- end
57
- end
58
-
59
- # Go directly to the next song in the queue.
60
- def skip(n = 1)
61
- if $player.skip(n)
62
- status
63
- else
64
- puts "End of queue."
65
- end
66
- end
67
-
68
- # Seek to a particular position in the current track. If given an integer, it
69
- # will seek that many seconds forward or backward. If given a Range, it will
70
- # seek to that specific time, the first number in the Range representing the
71
- # minutes, the second number representing the seconds. You can also pass a
72
- # String like "1:23:45" to do the same thing. To seek to an absolute time in
73
- # seconds, do it like "seek(abs: 40)". To seek to a percentage, do something
74
- # like "seek(percent: 75)".
75
- def seek(where)
76
- $player.seek(where)
77
- status
78
- end
79
-
80
- # Fast-forward at a particular speed. Induces the chipmunk effect, which I
81
- # find agreeable. Call p to go back to normal. You can also pass a value
82
- # smaller than one to slow down.
83
- def ff(speed = 2)
84
- $player.set_speed(speed)
85
- status
86
- end
87
-
88
- # Pass :all to start playing from the top of the queue when it gets to the
89
- # end. Pass :one to repeat the current track.
90
- def repeat(one_or_all = :all)
91
- $player.repeat one_or_all
92
- status
93
- end
94
-
95
- # Turn off the repeat mode set by the repeat command.
96
- def norepeat
97
- $player.repeat :off
98
- status
99
- end
100
-
101
- # Show various information about the status of the player. The information it
102
- # shows depends on what status types you pass:
103
- #
104
- # :playlist - Shows the playlist that is currently playing
105
- # :playing - Shows the current track
106
- #
107
- def status(*types)
108
- types = [:playlist, :playing] if types.empty?
109
- types.each do |type|
110
- case type
111
- when :playlist
112
- if $player.started?
113
- track_number = $player.current_track_index + 1
114
- num_tracks = q.length
115
- repeat_one = $player.repeat_mode == :one ? REPEAT_SYMBOL : ""
116
- repeat_all = $player.repeat_mode == :all ? REPEAT_SYMBOL : ""
117
- puts "Playlist: %s (%d%s / %d%s)" % [q.name, track_number, repeat_one, num_tracks, repeat_all]
118
- else
119
- puts "Playlist: %s" % [q]
120
- end
121
- when :playing
122
- if $player.started?
123
- name = $player.current_track.name
124
- artist = $player.current_track.artist
125
- time = $player.formatted_current_time
126
- total_time = $player.current_track.formatted_total_time
127
- paused = $player.paused? ? "|| " : ""
128
- speed = $player.speed != 1 ? "#{TIMES_SYMBOL}#{$player.speed} " : ""
129
- puts "%s - %s (%s / %s) %s%s" % [name, artist, time, total_time, paused, speed]
130
- else
131
- puts "Stopped."
132
- end
133
- end
134
- end
135
- nil
136
- end
137
- end