listlace 0.0.9 → 0.1.0

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.
@@ -1,194 +0,0 @@
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.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.clear
33
- q *tracks
34
- p
35
- end
36
- end
37
-
38
- # Stops playback. The queue is still intact, but goes back to the beginning
39
- # when playback is started again.
40
- def stop
41
- player.stop
42
- puts "Stopped."
43
- end
44
-
45
- # Start the current track from the beginning.
46
- def restart
47
- player.restart
48
- status
49
- end
50
-
51
- # Go back one song in the queue.
52
- def back(n = 1)
53
- player.back(n)
54
- if player.started?
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
- player.skip(n)
64
- if player.started?
65
- status
66
- else
67
- puts "End of queue."
68
- end
69
- end
70
-
71
- # Seek to a particular position in the current track. If given an integer, it
72
- # will seek that many seconds forward or backward. If given a Range, it will
73
- # seek to that specific time, the first number in the Range representing the
74
- # minutes, the second number representing the seconds. You can also pass a
75
- # String like "1:23:45" to do the same thing. To seek to an absolute time in
76
- # seconds, do it like "seek(abs: 40)". To seek to a percentage, do something
77
- # like "seek(percent: 75)".
78
- def seek(where)
79
- player.seek(where)
80
- status
81
- end
82
-
83
- # Fast-forward at a particular speed. Induces the chipmunk effect, which I
84
- # find agreeable. Call p to go back to normal. You can also pass a value
85
- # smaller than one to slow down.
86
- def ff(speed = 2)
87
- player.speed = speed
88
- status
89
- end
90
-
91
- # Pass :all to start playing from the top of the queue when it gets to the
92
- # end. Pass :one to repeat the current track.
93
- def repeat(one_or_all = :all)
94
- player.repeat one_or_all
95
- status
96
- end
97
-
98
- # Turn off the repeat mode set by the repeat command.
99
- def norepeat
100
- player.repeat :off
101
- status
102
- end
103
-
104
- # Show various information about the status of the player. The information it
105
- # shows depends on what status types you pass:
106
- #
107
- # :playlist - Shows the playlist that is currently playing
108
- # :playing - Shows the current track
109
- #
110
- def status(*types)
111
- types = [:playlist, :playing] if types.empty?
112
- types.each do |type|
113
- case type
114
- when :playlist
115
- if player.started?
116
- track_number = player.current_track_index + 1
117
- num_tracks = q.length
118
- repeat_one = player.repeat_mode == :one ? REPEAT_SYMBOL : ""
119
- repeat_all = player.repeat_mode == :all ? REPEAT_SYMBOL : ""
120
- puts "Playlist: %s (%d%s / %d%s)" % [q.name, track_number, repeat_one, num_tracks, repeat_all]
121
- else
122
- puts "Playlist: %s" % [q]
123
- end
124
- when :playing
125
- if player.started?
126
- title = player.current_track.title
127
- artist = player.current_track.artist
128
- time = Listlace.format_time(player.current_time, include_milliseconds: false)
129
- total_time = Listlace.format_time(player.total_time, include_milliseconds: false)
130
- paused = player.paused? ? "|| " : ""
131
- speed = player.speed
132
- speed = speed != 1 ? "#{TIMES_SYMBOL}#{speed} " : ""
133
- puts "%s - %s (%s / %s) %s%s" % [title, artist, time, total_time, paused, speed]
134
- else
135
- puts "Stopped."
136
- end
137
- end
138
- end
139
- nil
140
- end
141
-
142
- # The queue command. Simply appends tracks to the queue. It creates a playlist
143
- # with the arguments you give it, so anything you can pass to the playlist()
144
- # method you can pass to this. It returns the queue, so you can use this
145
- # method as an accessor by not passing any arguments.
146
- def q(*args)
147
- player.queue playlist(*args)
148
- player.queue
149
- end
150
-
151
- # Clears the queue.
152
- def clear
153
- player.clear
154
- puts "Queue cleared."
155
- end
156
-
157
- # Shuffles the queue, keeping the current track at the top.
158
- def shuffle
159
- player.shuffle
160
- puts "Shuffled."
161
- end
162
-
163
- # Sorts the queue by a list of fields and directions in the form of a symbol,
164
- # or uses the proc given to it, which should take two Tracks and return -1, 0,
165
- # or 1.
166
- def sort(by = :artist_asc_album_asc_track_number_asc, &proc)
167
- if proc
168
- player.sort(&proc)
169
- else
170
- player.sort do |a, b|
171
- result = 0
172
- by.to_s.scan(/([a-z_]+?)_(asc|desc)(?:_|$)/).each do |column, direction|
173
- a_value = a.send(column)
174
- b_value = b.send(column)
175
- a_value = a_value.downcase if a_value.respond_to? :downcase
176
- b_value = b_value.downcase if b_value.respond_to? :downcase
177
- dir = (direction == "desc") ? -1 : 1
178
- if a_value != b_value
179
- if a_value.nil? || b_value.nil?
180
- result = dir
181
- else
182
- result = (a_value <=> b_value) * dir
183
- end
184
- break
185
- end
186
- end
187
- result
188
- end
189
- end
190
- puts "Sorted."
191
- end
192
- end
193
- end
194
- end
@@ -1,58 +0,0 @@
1
- class Array
2
- attr_accessor :name
3
-
4
- # Check if this array is a playlist. It's a playlist if it has
5
- # a name attribute set or consists entirely of Track instances.
6
- def playlist?
7
- if @name || all? { |x| x.is_a? Listlace::Track }
8
- @name ||= ""
9
- true
10
- else
11
- false
12
- end
13
- end
14
-
15
- # Returns a new array that is shuffled, but with elem at the top.
16
- # This is how playlists that are currently playing are shuffled.
17
- # The currently playing track goes to the top, the rest of the
18
- # tracks are shuffled.
19
- def shuffle_except(elem)
20
- ary = dup
21
- dup.shuffle_except! elem
22
- dup
23
- end
24
-
25
- # Like shuffle_except, but shuffles in-place.
26
- def shuffle_except!(elem)
27
- if i = index(elem)
28
- delete_at(i)
29
- shuffle!
30
- unshift(elem)
31
- else
32
- shuffle!
33
- end
34
- end
35
-
36
- # Override to_s to check if the array is a playlist, and format
37
- # it accordingly.
38
- alias _original_to_s to_s
39
- def to_s
40
- if playlist?
41
- "%s (%d track%s)" % [@name || "playlist", length, ("s" if length != 1)]
42
- else
43
- _original_to_s
44
- end
45
- end
46
-
47
- # Override inspect for nice pry output.
48
- alias _original_inspect inspect
49
- def inspect
50
- playlist? ? to_s : _original_inspect
51
- end
52
-
53
- # Override pretty_inspect for nice pry output.
54
- alias _original_pretty_inspect pretty_inspect
55
- def pretty_inspect
56
- playlist? ? inspect : _original_pretty_inspect
57
- end
58
- end
@@ -1,84 +0,0 @@
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 :title
45
- t.string :artist
46
- t.string :composer
47
- t.string :album
48
- t.string :album_artist
49
- t.string :genre
50
- t.integer :total_time
51
- t.integer :disc_number
52
- t.integer :disc_count
53
- t.integer :track_number
54
- t.integer :track_count
55
- t.integer :year
56
- t.datetime :date_modified, null: false
57
- t.datetime :date_added, null: false
58
- t.integer :bit_rate
59
- t.integer :sample_rate
60
- t.text :comments
61
- t.integer :play_count, null: false, default: 0
62
- t.datetime :play_date
63
- t.integer :skip_count, null: false, default: 0
64
- t.datetime :skip_date
65
- t.integer :rating, null: false, default: 0
66
- t.string :location, null: false
67
- end
68
-
69
- create_table :playlists do |t|
70
- t.string :name
71
- t.datetime :created_at
72
- t.datetime :updated_at
73
- end
74
-
75
- create_table :playlist_items do |t|
76
- t.references :playlist, null: false
77
- t.references :track, null: false
78
- t.integer :position
79
- end
80
- end
81
- end
82
- end
83
- end
84
- end
@@ -1,199 +0,0 @@
1
- module Listlace
2
- class Library
3
- module Selectors
4
- STRING_SELECTORS = %w(title 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
- # Selects all the tracks in the library.
24
- def all
25
- playlist library.tracks
26
- end
27
-
28
- # Selects no tracks, returning an empty playlist.
29
- def none
30
- playlist
31
- end
32
-
33
- # The length selector is an integer selector for the length of a track. A
34
- # plain integer given to it represents the number of seconds. It can also take
35
- # a String in the format "1:23", to represent 83 seconds, for example. These
36
- # can be part of a Range, as usual: "1:23".."2:05", for example.
37
- def length(*args)
38
- normalize = lambda do |value|
39
- case value
40
- when String
41
- Listlace.parse_time(value)
42
- when Integer
43
- value * 1000
44
- when Range
45
- (normalize.(value.begin))..(normalize.(value.end))
46
- end
47
- end
48
-
49
- playlists = args.map do |arg|
50
- if arg.is_a? Hash
51
- key = arg.keys.first
52
- arg[key] = normalize.(arg[key])
53
- else
54
- arg = normalize.(arg)
55
- end
56
-
57
- # If they want tracks of length "0:05", for example, we need to look for
58
- # tracks that are from 5000 to 5999 milliseconds long.
59
- if arg.is_a? Integer
60
- arg = (arg)..(arg + 999)
61
- end
62
-
63
- integer_selector(:total_time, arg)
64
- end
65
-
66
- playlist *playlists
67
- end
68
-
69
- # Makes a playlist out of tracks that match the string query on the given
70
- # column. It's SQL underneath, so you can use % and _ as wildcards in the
71
- # query. By default, % wildcards are inserted on the left and right of your
72
- # query. Use the :match option to change this:
73
- #
74
- # :match => :middle "%query%" (default)
75
- # :match => :left "query%"
76
- # :match => :right "%query"
77
- # :match => :exact "query"
78
- #
79
- # This method shouldn't have to be used directly. Many convenient methods are
80
- # generated for you, one for each string field you may want to select on.
81
- # These are: title, artist, composer, album, album_artist, genre, comments,
82
- # location. For example:
83
- #
84
- # artist :muse, match: :exact #=> playlist (108 tracks)
85
- # composer :rachmanino #=> playlist (33 tracks)
86
- #
87
- def string_selector(column, query, options = {})
88
- options[:match] ||= :middle
89
-
90
- query = {
91
- exact: "#{query}",
92
- left: "#{query}%",
93
- right: "%#{query}",
94
- middle: "%#{query}%"
95
- }[options[:match]]
96
-
97
- tracks = library.tracks.arel_table
98
- library.tracks.where(tracks[column].matches(query)).all
99
- end
100
-
101
- # Makes a playlist out of tracks that satisfy certain conditions on the given
102
- # integer column. You can pass an exact value to check for equality, a range,
103
- # or a hash that specifies greater-than and less-than options like this:
104
- #
105
- # integer_selector :year, greater_than: 2000 #=> playlist (3555 tracks)
106
- #
107
- # The possible operators, with their shortcuts, are:
108
- #
109
- # :greater_than / :gt
110
- # :less_than / :lt
111
- # :greater_than_or_equal / :gte
112
- # :less_than_or_equal / :lte
113
- # :not_equal / :ne
114
- #
115
- # Note: You can only use one of these operators at a time. If you want a
116
- # range, use a Range.
117
- #
118
- # This method shouldn't have to be used directly. Many convenient methods are
119
- # generated for you, one for each integer field you may want to select on.
120
- # These are: disc_number, disc_count, track_number, track_count, year,
121
- # bit_rate, sample_rate, play_count, skip_count, rating, length. Length is
122
- # special, it can take any of the time formats that the seek command can. For
123
- # example:
124
- #
125
- # year 2010..2012 #=> playlist (1060 tracks)
126
- # length gt: "4:00" #=> playlist (2543 tracks)
127
- #
128
- def integer_selector(column, value_or_options)
129
- if value_or_options.is_a? Hash
130
- operator = {
131
- greater_than: ">",
132
- gt: ">",
133
- less_than: "<",
134
- lt: "<",
135
- greater_than_or_equal: ">=",
136
- gte: ">=",
137
- less_than_or_equal: "<=",
138
- lte: "<=",
139
- not_equal: "<>",
140
- ne: "<>"
141
- }[value_or_options.keys.first]
142
- library.tracks.where("tracks.#{column} #{operator} ?", value_or_options.values.first).all
143
- else
144
- library.tracks.where(column => value_or_options).all
145
- end
146
- end
147
-
148
- # Creates or looks up a playlist. You can pass any number of multiple types of
149
- # objects to make a playlist:
150
- #
151
- # Track: Makes a playlist with one track
152
- # ActiveRecord::Relation: Makes a playlist out of the resulting Track or Playlist records
153
- # Symbol or String: Tries to retrieve a saved playlist with the given name.
154
- # If it can't find one, it creates a new one.
155
- #
156
- # You can also pass in the results of a selector. In this way you can create a
157
- # playlist out of many smaller pieces.
158
- #
159
- # If given no arguments, it returns a blank playlist.
160
- #
161
- def playlist(*args)
162
- args << [] if args.empty?
163
- args.map do |object|
164
- case object
165
- when Track
166
- [object]
167
- when Playlist
168
- pl = [object.tracks.all]
169
- pl.name = object.name
170
- pl
171
- when Array
172
- if object.playlist?
173
- object
174
- else
175
- playlist *object
176
- end
177
- when ActiveRecord::Relation
178
- if object.table.name == "tracks"
179
- object.all
180
- elsif object.table.name == "playlists"
181
- playlist *object.all
182
- end
183
- when Symbol, String
184
- playlists = library.playlists.arel_table
185
- if playlist = library.playlists.where(playlists[:name].matches(object.to_s)).first
186
- pl = playlist.tracks.all
187
- pl.name = playlist.name
188
- pl
189
- else
190
- pl = []
191
- pl.name = object
192
- pl
193
- end
194
- end
195
- end.inject(:+)
196
- end
197
- end
198
- end
199
- end
@@ -1,160 +0,0 @@
1
- module Listlace
2
- class Library
3
- class FileNotFoundError < ArgumentError; end
4
-
5
- def initialize(options = {})
6
- options[:db_path] ||= "library"
7
- options[:db_adapter] ||= "sqlite3"
8
-
9
- unless File.exists? Listlace::DIR
10
- FileUtils.mkdir_p Listlace::DIR
11
- end
12
-
13
- @db_path = options[:db_path]
14
- @db_path = "#{Listlace::DIR}/#{@db_path}" unless @db_path.include? "/"
15
- @db_path = "#{@db_path}.sqlite3" unless @db_path =~ /\.sqlite3$/
16
-
17
- @db_adapter = options[:db_adapter]
18
-
19
- Database.disconnect if Database.connected?
20
- Database.connect(@db_adapter, @db_path)
21
- Database.generate_schema unless Database.exists?(@db_path)
22
- end
23
-
24
- def tracks
25
- Track.scoped
26
- end
27
-
28
- def playlists
29
- Playlist.scoped
30
- end
31
-
32
- def size
33
- tracks.length
34
- end
35
-
36
- def wipe
37
- Database.wipe(@db_adapter, @db_path)
38
- end
39
-
40
- def save_playlist(playlist)
41
- playlist_table = playlists.arel_table
42
- if model = playlists.where(playlist_table[:name].matches(playlist.name)).first
43
- model.playlist_items.destroy_all
44
- else
45
- model = playlists.new(name: playlist.name)
46
- model.save!
47
- end
48
-
49
- playlist.each.with_index do |track, i|
50
- item = PlaylistItem.new(position: i)
51
- item.playlist = model
52
- item.track = track
53
- item.save!
54
- end
55
- playlist
56
- end
57
-
58
- def add_track(path, options = {})
59
- metadata = options.dup
60
- if File.exists?(path)
61
- TagLib::FileRef.open(path) do |file|
62
- if tag = file.tag
63
- metadata[:album] ||= tag.album
64
- metadata[:artist] ||= tag.artist
65
- metadata[:comments] ||= tag.comment
66
- metadata[:genre] ||= tag.genre
67
- metadata[:title] ||= tag.title
68
- metadata[:track_number] ||= tag.track unless tag.track.zero?
69
- metadata[:year] ||= tag.year unless tag.year.zero?
70
- end
71
-
72
- if prop = file.audio_properties
73
- metadata[:bit_rate] = prop.bitrate
74
- metadata[:sample_rate] = prop.sample_rate
75
- metadata[:total_time] = prop.length * 1000
76
- end
77
-
78
- if metadata[:title].nil? or metadata[:title].empty?
79
- metadata[:title] = File.basename(path, ".*")
80
- end
81
-
82
- metadata[:location] = File.expand_path(path)
83
-
84
- track = Track.new(metadata)
85
- track.save && track
86
- end
87
- else
88
- raise FileNotFoundError, "File '%s' doesn't exist." % [path]
89
- end
90
- end
91
-
92
- def import(from, path, options = {})
93
- logger = options[:logger]
94
- if not File.exists?(path)
95
- raise FileNotFoundError, "File '%s' doesn't exist." % [path]
96
- elsif from == :itunes
97
- logger.("Parsing XML...") if logger
98
- data = Plist::parse_xml(path)
99
-
100
- logger.("Importing #{data['Tracks'].length} tracks...") if logger
101
- num_tracks = 0
102
- whitelist = tracks.new.attributes.keys
103
- data["Tracks"].each do |track_id, row|
104
- if row["Kind"] !~ /audio/
105
- logger.("[skipping non-audio file]") if logger
106
- next
107
- end
108
-
109
- # row already contains a hash of attributes almost ready to be passed to
110
- # ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
111
- # to "play_count".
112
- row["Title"] = row.delete("Name")
113
- row["Play Date"] = row.delete("Play Date UTC")
114
- row["Original ID"] = row.delete("Track ID")
115
- attributes = row.inject({}) do |acc, (key, value)|
116
- attribute = key.gsub(" ", "").underscore
117
- acc[attribute] = value if whitelist.include? attribute
118
- acc
119
- end
120
-
121
- # change iTunes' URL-style locations into simple paths
122
- if attributes["location"] && attributes["location"] =~ /^file:\/\//
123
- attributes["location"].sub! /^file:\/\/localhost/, ""
124
-
125
- # CGI::unescape changes plus signs to spaces. This is a work around to
126
- # keep the plus signs.
127
- attributes["location"].gsub! "+", "%2B"
128
-
129
- attributes["location"] = CGI::unescape(attributes["location"])
130
- end
131
-
132
- track = tracks.new(attributes)
133
- if track.save
134
- num_tracks += 1
135
- end
136
- end
137
- logger.("Imported #{num_tracks} tracks successfully.") if logger
138
-
139
- logger.("Importing #{data['Playlists'].length} playlists...") if logger
140
- num_playlists = 0
141
- data["Playlists"].each do |playlist_data|
142
- playlist = []
143
- playlist.name = playlist_data["Name"]
144
-
145
- if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist.name
146
- logger.("[skipping \"#{playlist.name}\" playlist]") if logger
147
- else
148
- playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id|
149
- playlist << tracks.where(original_id: original_id).first
150
- end
151
- playlist.compact!
152
- save_playlist playlist
153
- num_playlists += 1
154
- end
155
- end
156
- logger.("Imported #{num_playlists} playlists successfully.") if logger
157
- end
158
- end
159
- end
160
- end
@@ -1,6 +0,0 @@
1
- module Listlace
2
- class Playlist < ActiveRecord::Base
3
- has_many :playlist_items
4
- has_many :tracks, through: :playlist_items, order: "playlist_items.position ASC"
5
- end
6
- end