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.
- data/Gemfile.lock +1 -1
- data/README.md +53 -0
- data/{README → README.old} +0 -0
- data/bin/listlace +13 -8
- data/lib/listlace/array_ext.rb +41 -0
- data/lib/listlace/commands/library_commands.rb +101 -0
- data/lib/listlace/commands/player_commands.rb +192 -0
- data/lib/listlace/commands.rb +7 -0
- data/lib/listlace/library/database.rb +89 -0
- data/lib/listlace/library/selectors.rb +196 -0
- data/lib/listlace/library.rb +41 -0
- data/lib/listlace/models.rb +3 -0
- data/lib/listlace/player/mplayer.rb +53 -0
- data/lib/listlace/player.rb +19 -2
- data/lib/listlace.rb +12 -27
- data/listlace.gemspec +3 -3
- metadata +13 -11
- data/lib/listlace/commands/library.rb +0 -85
- data/lib/listlace/commands/playback.rb +0 -137
- data/lib/listlace/commands/queue.rb +0 -52
- data/lib/listlace/commands/selectors.rb +0 -186
- data/lib/listlace/commands/volume.rb +0 -0
- data/lib/listlace/database.rb +0 -74
- data/lib/listlace/mplayer.rb +0 -51
- data/lib/listlace/playlist_array.rb +0 -82
data/Gemfile.lock
CHANGED
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Listlace
|
2
|
+
|
3
|
+
Listlace is a music player that does Ruby. Here's how it works:
|
4
|
+
|
5
|
+
First add your music library.
|
6
|
+
|
7
|
+
>> add "/path/to/Music"
|
8
|
+
1067 songs added.
|
9
|
+
|
10
|
+
...Or import your iTunes library.
|
11
|
+
|
12
|
+
>> import :itunes, "/path/to/iTunes/iTunes Music Library.xml"
|
13
|
+
|
14
|
+
Once your library is in place, start grouping your songs into playlists.
|
15
|
+
|
16
|
+
>> save artist("muse"), :muse
|
17
|
+
=> muse (108 tracks)
|
18
|
+
|
19
|
+
Finally, go ahead and play your playlists.
|
20
|
+
|
21
|
+
>> p :muse
|
22
|
+
Playlist: muse (108 tracks)
|
23
|
+
Blackout - Muse (0:00 / 4:22)
|
24
|
+
|
25
|
+
## Install
|
26
|
+
|
27
|
+
It's a gem, so do this:
|
28
|
+
|
29
|
+
$ gem install listlace
|
30
|
+
|
31
|
+
You also need mplayer in your $PATH, if you want to atually play your music.
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
The gem gives you an executable, so do this:
|
36
|
+
|
37
|
+
$ listlace
|
38
|
+
Hello, you have 0 songs.
|
39
|
+
>>
|
40
|
+
|
41
|
+
Now you're ready to play.
|
42
|
+
|
43
|
+
## Library
|
44
|
+
|
45
|
+
...
|
46
|
+
|
47
|
+
## Playlists
|
48
|
+
|
49
|
+
...
|
50
|
+
|
51
|
+
## The Player
|
52
|
+
|
53
|
+
...
|
data/{README → README.old}
RENAMED
File without changes
|
data/bin/listlace
CHANGED
@@ -4,17 +4,22 @@ require "bundler/setup"
|
|
4
4
|
require "pry"
|
5
5
|
require "listlace"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
ActiveRecord::Migration.verbose = false
|
8
|
+
|
9
|
+
Listlace.extend Listlace::Commands::LibraryCommands
|
10
|
+
Listlace.extend Listlace::Commands::PlayerCommands
|
10
11
|
|
11
|
-
Listlace::
|
12
|
-
Listlace
|
12
|
+
Listlace.library = Listlace::Library.new
|
13
|
+
Listlace.player = Listlace::Player.new
|
13
14
|
|
14
|
-
puts "Hello, you have #{Listlace
|
15
|
+
puts "Hello, you have #{Listlace.library.size} songs."
|
16
|
+
|
17
|
+
at_exit do
|
18
|
+
Listlace.player.stop
|
19
|
+
end
|
15
20
|
|
16
21
|
Listlace.pry(
|
17
22
|
quiet: true,
|
18
|
-
prompt:
|
19
|
-
print:
|
23
|
+
prompt: [proc { ">> " }, proc { " | " }],
|
24
|
+
print: proc { |output, value| Pry::DEFAULT_PRINT.call(output, value) unless value.nil? }
|
20
25
|
)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Array
|
2
|
+
attr_accessor :name
|
3
|
+
|
4
|
+
def playlist?
|
5
|
+
if @name || all? { |x| x.is_a? Listlace::Track }
|
6
|
+
@name ||= ""
|
7
|
+
true
|
8
|
+
else
|
9
|
+
false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def shuffle_except(elem)
|
14
|
+
ary = dup
|
15
|
+
dup.shuffle_except! elem
|
16
|
+
dup
|
17
|
+
end
|
18
|
+
|
19
|
+
def shuffle_except!(elem)
|
20
|
+
replace([elem] + (self - [elem]).shuffle)
|
21
|
+
end
|
22
|
+
|
23
|
+
alias _original_to_s to_s
|
24
|
+
def to_s
|
25
|
+
if playlist?
|
26
|
+
"%s (%d track%s)" % [@name || "playlist", length, ("s" if length != 1)]
|
27
|
+
else
|
28
|
+
_original_to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
alias _original_inspect inspect
|
33
|
+
def inspect
|
34
|
+
playlist? ? to_s : _original_inspect
|
35
|
+
end
|
36
|
+
|
37
|
+
alias _original_pretty_inspect pretty_inspect
|
38
|
+
def pretty_inspect
|
39
|
+
playlist? ? inspect : _original_pretty_inspect
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Listlace
|
2
|
+
module Commands
|
3
|
+
module LibraryCommands
|
4
|
+
def save(playlist, name = nil)
|
5
|
+
playlist.name = name if name
|
6
|
+
if model = library.playlists.where(name: playlist.name).first
|
7
|
+
model.playlist_items.destroy_all
|
8
|
+
else
|
9
|
+
model = library.playlists.new(name: playlist.name)
|
10
|
+
model.save!
|
11
|
+
end
|
12
|
+
|
13
|
+
playlist.each.with_index do |track, i|
|
14
|
+
item = PlaylistItem.new(position: i)
|
15
|
+
item.playlist = model
|
16
|
+
item.track = track
|
17
|
+
item.save!
|
18
|
+
end
|
19
|
+
playlist
|
20
|
+
end
|
21
|
+
|
22
|
+
# Imports the music library from another program. Currently only iTunes is
|
23
|
+
# supported.
|
24
|
+
def import(from, path)
|
25
|
+
if not File.exists?(path)
|
26
|
+
puts "File '%s' doesn't exist." % [path]
|
27
|
+
elsif from == :itunes
|
28
|
+
puts "Parsing XML..."
|
29
|
+
data = Plist::parse_xml(path)
|
30
|
+
|
31
|
+
puts "Importing #{data['Tracks'].length} tracks..."
|
32
|
+
num_tracks = 0
|
33
|
+
whitelist = library.tracks.new.attributes.keys
|
34
|
+
data["Tracks"].each do |track_id, row|
|
35
|
+
# row already contains a hash of attributes almost ready to be passed to
|
36
|
+
# ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
|
37
|
+
# to "play_count".
|
38
|
+
attributes = row.inject({}) do |acc, (key, value)|
|
39
|
+
attribute = key.gsub(" ", "").underscore
|
40
|
+
attribute = "original_id" if attribute == "track_id"
|
41
|
+
acc[attribute] = value if whitelist.include? attribute
|
42
|
+
acc
|
43
|
+
end
|
44
|
+
|
45
|
+
# change iTunes' URL-style locations into simple paths
|
46
|
+
if attributes["location"] && attributes["location"] =~ /^file:\/\//
|
47
|
+
attributes["location"].sub! /^file:\/\/localhost/, ""
|
48
|
+
|
49
|
+
# CGI::unescape changes plus signs to spaces. This is a work around to
|
50
|
+
# keep the plus signs.
|
51
|
+
attributes["location"].gsub! "+", "%2B"
|
52
|
+
|
53
|
+
attributes["location"] = CGI::unescape(attributes["location"])
|
54
|
+
end
|
55
|
+
|
56
|
+
track = library.tracks.new(attributes)
|
57
|
+
|
58
|
+
if track.kind =~ /audio/
|
59
|
+
if track.save
|
60
|
+
num_tracks += 1
|
61
|
+
end
|
62
|
+
else
|
63
|
+
puts "[skipping non-audio file]"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
puts "Imported #{num_tracks} tracks successfully."
|
67
|
+
|
68
|
+
puts "Importing #{data['Playlists'].length} playlists..."
|
69
|
+
num_playlists = 0
|
70
|
+
data["Playlists"].each do |playlist_data|
|
71
|
+
playlist = []
|
72
|
+
playlist.name = playlist_data["Name"]
|
73
|
+
|
74
|
+
if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist.name
|
75
|
+
puts "[skipping \"#{playlist.name}\" playlist]"
|
76
|
+
else
|
77
|
+
playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id|
|
78
|
+
playlist << library.tracks.where(original_id: original_id).first
|
79
|
+
end
|
80
|
+
playlist.compact!
|
81
|
+
save playlist
|
82
|
+
num_playlists += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
puts "Imported #{num_playlists} playlists successfully."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Wipes the database. With no arguments, it just asks "Are you sure?" without
|
90
|
+
# doing anything. To actually wipe the database, pass :yes_im_sure.
|
91
|
+
def wipe_library(are_you_sure = :nope)
|
92
|
+
if are_you_sure == :yes_im_sure
|
93
|
+
library.wipe
|
94
|
+
puts "Library wiped."
|
95
|
+
else
|
96
|
+
puts "Are you sure? If you are, then type: wipe_library :yes_im_sure"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module Listlace
|
2
|
+
module Commands
|
3
|
+
module PlayerCommands
|
4
|
+
REPEAT_SYMBOL = "\u221E"
|
5
|
+
TIMES_SYMBOL = "\u00D7"
|
6
|
+
|
7
|
+
# The play command. With no arguments, it either resumes playback or starts
|
8
|
+
# playing the queue. With arguments, it replaces the queue with the given
|
9
|
+
# tracks and starts playing.
|
10
|
+
def p(*tracks)
|
11
|
+
if tracks.empty?
|
12
|
+
if player.paused?
|
13
|
+
player.resume
|
14
|
+
status
|
15
|
+
elsif player.started?
|
16
|
+
if player.speed == 1
|
17
|
+
player.pause
|
18
|
+
status
|
19
|
+
else
|
20
|
+
player.set_speed 1
|
21
|
+
status
|
22
|
+
end
|
23
|
+
else
|
24
|
+
if player.empty?
|
25
|
+
puts "Nothing to play."
|
26
|
+
else
|
27
|
+
player.start
|
28
|
+
status
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
player.stop
|
33
|
+
player.clear
|
34
|
+
q *tracks
|
35
|
+
p
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Stops playback. The queue is still intact, but goes back to the beginning
|
40
|
+
# when playback is started again.
|
41
|
+
def stop
|
42
|
+
player.stop
|
43
|
+
puts "Stopped."
|
44
|
+
end
|
45
|
+
|
46
|
+
# Start the current track from the beginning.
|
47
|
+
def restart
|
48
|
+
player.restart
|
49
|
+
status
|
50
|
+
end
|
51
|
+
|
52
|
+
# Go back one song in the queue.
|
53
|
+
def back(n = 1)
|
54
|
+
if player.back(n)
|
55
|
+
status
|
56
|
+
else
|
57
|
+
puts "End of queue."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Go directly to the next song in the queue.
|
62
|
+
def skip(n = 1)
|
63
|
+
if player.skip(n)
|
64
|
+
status
|
65
|
+
else
|
66
|
+
puts "End of queue."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Seek to a particular position in the current track. If given an integer, it
|
71
|
+
# will seek that many seconds forward or backward. If given a Range, it will
|
72
|
+
# seek to that specific time, the first number in the Range representing the
|
73
|
+
# minutes, the second number representing the seconds. You can also pass a
|
74
|
+
# String like "1:23:45" to do the same thing. To seek to an absolute time in
|
75
|
+
# seconds, do it like "seek(abs: 40)". To seek to a percentage, do something
|
76
|
+
# like "seek(percent: 75)".
|
77
|
+
def seek(where)
|
78
|
+
player.seek(where)
|
79
|
+
status
|
80
|
+
end
|
81
|
+
|
82
|
+
# Fast-forward at a particular speed. Induces the chipmunk effect, which I
|
83
|
+
# find agreeable. Call p to go back to normal. You can also pass a value
|
84
|
+
# smaller than one to slow down.
|
85
|
+
def ff(speed = 2)
|
86
|
+
player.set_speed(speed)
|
87
|
+
status
|
88
|
+
end
|
89
|
+
|
90
|
+
# Pass :all to start playing from the top of the queue when it gets to the
|
91
|
+
# end. Pass :one to repeat the current track.
|
92
|
+
def repeat(one_or_all = :all)
|
93
|
+
player.repeat one_or_all
|
94
|
+
status
|
95
|
+
end
|
96
|
+
|
97
|
+
# Turn off the repeat mode set by the repeat command.
|
98
|
+
def norepeat
|
99
|
+
player.repeat :off
|
100
|
+
status
|
101
|
+
end
|
102
|
+
|
103
|
+
# Show various information about the status of the player. The information it
|
104
|
+
# shows depends on what status types you pass:
|
105
|
+
#
|
106
|
+
# :playlist - Shows the playlist that is currently playing
|
107
|
+
# :playing - Shows the current track
|
108
|
+
#
|
109
|
+
def status(*types)
|
110
|
+
types = [:playlist, :playing] if types.empty?
|
111
|
+
types.each do |type|
|
112
|
+
case type
|
113
|
+
when :playlist
|
114
|
+
if player.started?
|
115
|
+
track_number = player.current_track_index + 1
|
116
|
+
num_tracks = q.length
|
117
|
+
repeat_one = player.repeat_mode == :one ? REPEAT_SYMBOL : ""
|
118
|
+
repeat_all = player.repeat_mode == :all ? REPEAT_SYMBOL : ""
|
119
|
+
puts "Playlist: %s (%d%s / %d%s)" % [q.name, track_number, repeat_one, num_tracks, repeat_all]
|
120
|
+
else
|
121
|
+
puts "Playlist: %s" % [q]
|
122
|
+
end
|
123
|
+
when :playing
|
124
|
+
if player.started?
|
125
|
+
name = player.current_track.name
|
126
|
+
artist = player.current_track.artist
|
127
|
+
time = player.formatted_current_time
|
128
|
+
total_time = player.current_track.formatted_total_time
|
129
|
+
paused = player.paused? ? "|| " : ""
|
130
|
+
speed = player.speed != 1 ? "#{TIMES_SYMBOL}#{player.speed} " : ""
|
131
|
+
puts "%s - %s (%s / %s) %s%s" % [name, artist, time, total_time, paused, speed]
|
132
|
+
else
|
133
|
+
puts "Stopped."
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
nil
|
138
|
+
end
|
139
|
+
|
140
|
+
# The queue command. Simply appends tracks to the queue. It creates a playlist
|
141
|
+
# with the arguments you give it, so anything you can pass to the playlist()
|
142
|
+
# method you can pass to this. It returns the queue, so you can use this
|
143
|
+
# method as an accessor by not passing any arguments.
|
144
|
+
def q(*args)
|
145
|
+
player.queue playlist(*args)
|
146
|
+
player.queue
|
147
|
+
end
|
148
|
+
|
149
|
+
# Clears the queue.
|
150
|
+
def clear
|
151
|
+
player.clear
|
152
|
+
puts "Queue cleared."
|
153
|
+
end
|
154
|
+
|
155
|
+
# Shuffles the queue, keeping the current track at the top.
|
156
|
+
def shuffle
|
157
|
+
player.shuffle
|
158
|
+
puts "Shuffled."
|
159
|
+
end
|
160
|
+
|
161
|
+
# Sorts the queue by a list of fields and directions in the form of a symbol,
|
162
|
+
# or uses the proc given to it, which should take two Tracks and return -1, 0,
|
163
|
+
# or 1.
|
164
|
+
def sort(by = :artist_asc_album_asc_track_number_asc, &proc)
|
165
|
+
if proc
|
166
|
+
player.sort(&proc)
|
167
|
+
else
|
168
|
+
player.sort do |a, b|
|
169
|
+
result = 0
|
170
|
+
by.to_s.scan(/([a-z_]+?)_(asc|desc)(?:_|$)/).each do |column, direction|
|
171
|
+
a_value = a.send(column)
|
172
|
+
b_value = b.send(column)
|
173
|
+
a_value = a_value.downcase if a_value.respond_to? :downcase
|
174
|
+
b_value = b_value.downcase if b_value.respond_to? :downcase
|
175
|
+
dir = (direction == "desc") ? -1 : 1
|
176
|
+
if a_value != b_value
|
177
|
+
if a_value.nil? || b_value.nil?
|
178
|
+
result = dir
|
179
|
+
else
|
180
|
+
result = (a_value <=> b_value) * dir
|
181
|
+
end
|
182
|
+
break
|
183
|
+
end
|
184
|
+
end
|
185
|
+
result
|
186
|
+
end
|
187
|
+
end
|
188
|
+
puts "Sorted."
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Listlace
|
2
|
+
class Library
|
3
|
+
module Database
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def connect(adapter, path)
|
7
|
+
ActiveRecord::Base.establish_connection(adapter: adapter, database: path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def disconnect
|
11
|
+
ActiveRecord::Base.remove_connection
|
12
|
+
end
|
13
|
+
|
14
|
+
def exists?(path)
|
15
|
+
File.exists? path
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(path)
|
19
|
+
FileUtils.rm path
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
ActiveRecord::Base.connected?
|
24
|
+
end
|
25
|
+
|
26
|
+
def connected_to?(path)
|
27
|
+
if ActiveRecord::Base.connected?
|
28
|
+
File.expand_path(path) == File.expand_path(ActiveRecord::Base.connection_config[:database])
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def wipe(adapter, path)
|
35
|
+
delete(path)
|
36
|
+
connect(adapter, path)
|
37
|
+
generate_schema
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_schema
|
41
|
+
ActiveRecord::Schema.define do
|
42
|
+
create_table :tracks do |t|
|
43
|
+
t.integer :original_id
|
44
|
+
t.string :name
|
45
|
+
t.string :artist
|
46
|
+
t.string :composer
|
47
|
+
t.string :album
|
48
|
+
t.string :album_artist
|
49
|
+
t.string :genre
|
50
|
+
t.string :kind
|
51
|
+
t.integer :size
|
52
|
+
t.integer :total_time
|
53
|
+
t.integer :disc_number
|
54
|
+
t.integer :disc_count
|
55
|
+
t.integer :track_number
|
56
|
+
t.integer :track_count
|
57
|
+
t.integer :year
|
58
|
+
t.datetime :date_modified
|
59
|
+
t.datetime :date_added
|
60
|
+
t.integer :bit_rate
|
61
|
+
t.integer :sample_rate
|
62
|
+
t.text :comments
|
63
|
+
t.integer :play_count
|
64
|
+
t.integer :play_date
|
65
|
+
t.datetime :play_date_utc
|
66
|
+
t.integer :skip_count
|
67
|
+
t.datetime :skip_date
|
68
|
+
t.integer :rating
|
69
|
+
t.integer :album_rating
|
70
|
+
t.boolean :album_rating_computed
|
71
|
+
t.string :location
|
72
|
+
end
|
73
|
+
|
74
|
+
create_table :playlists do |t|
|
75
|
+
t.string :name
|
76
|
+
t.datetime :created_at
|
77
|
+
t.datetime :updated_at
|
78
|
+
end
|
79
|
+
|
80
|
+
create_table :playlist_items do |t|
|
81
|
+
t.references :playlist, null: false
|
82
|
+
t.references :track, null: false
|
83
|
+
t.integer :position
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|