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