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 CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- listlace (0.0.4)
4
+ listlace (0.0.5)
5
5
  activerecord
6
6
  activesupport
7
7
  open4
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
+ ...
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
- unless File.exists? Listlace::DIR
8
- FileUtils.mkdir_p Listlace::DIR
9
- end
7
+ ActiveRecord::Migration.verbose = false
8
+
9
+ Listlace.extend Listlace::Commands::LibraryCommands
10
+ Listlace.extend Listlace::Commands::PlayerCommands
10
11
 
11
- Listlace::Database.connect
12
- Listlace::Database.generate_schema unless Listlace::Database.exists?
12
+ Listlace.library = Listlace::Library.new
13
+ Listlace.player = Listlace::Player.new
13
14
 
14
- puts "Hello, you have #{Listlace::Track.count} songs."
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: Listlace::PROMPT,
19
- print: Listlace::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,7 @@
1
+ require "listlace/commands/library_commands"
2
+ require "listlace/commands/player_commands"
3
+
4
+ module Listlace
5
+ module Commands
6
+ end
7
+ 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