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 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