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.
- checksums.yaml +7 -0
- data/Gemfile +0 -0
- data/Gemfile.lock +9 -33
- data/LICENSE +1 -1
- data/README.md +10 -32
- data/bin/listlace +12 -13
- data/lib/listlace/commands.rb +18 -0
- data/lib/listlace.rb +12 -28
- data/listlace.gemspec +7 -8
- metadata +22 -127
- data/README.old +0 -86
- data/lib/listlace/commands/library_commands.rb +0 -90
- data/lib/listlace/commands/player_commands.rb +0 -194
- data/lib/listlace/core_ext/array.rb +0 -58
- data/lib/listlace/library/database.rb +0 -84
- data/lib/listlace/library/selectors.rb +0 -199
- data/lib/listlace/library.rb +0 -160
- data/lib/listlace/models/playlist.rb +0 -6
- data/lib/listlace/models/playlist_item.rb +0 -6
- data/lib/listlace/models/track.rb +0 -21
- data/lib/listlace/player.rb +0 -249
- data/lib/listlace/simple_track.rb +0 -13
- data/lib/listlace/single_player.rb +0 -129
- data/lib/listlace/single_players/mplayer.rb +0 -255
- data/lib/listlace/time_helper.rb +0 -34
@@ -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
|
data/lib/listlace/library.rb
DELETED
@@ -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
|