listlace 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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