listlace 0.0.9 → 0.1.0

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