lllibrary 0.0.1 → 0.0.2
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/README.md +6 -70
- data/lib/lllibrary.rb +124 -3
- data/lib/lllibrary/database.rb +64 -47
- data/lib/lllibrary/dsl.rb +42 -15
- data/lib/lllibrary/playlist.rb +27 -1
- data/lib/lllibrary/playlist_item.rb +2 -0
- data/lib/lllibrary/track.rb +9 -0
- data/lllibrary.gemspec +3 -3
- metadata +18 -2
data/README.md
CHANGED
@@ -1,85 +1,21 @@
|
|
1
|
-
#lllibrary
|
1
|
+
# lllibrary
|
2
2
|
|
3
3
|
`lllibrary` is a Ruby library that manages music libraries. It stores tracks (with metadata) and playlists in a database (using ActiveRecord), and gives you a sort of DSL to build complex playlists with ease.
|
4
4
|
|
5
5
|
## Install
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
## Usage
|
10
|
-
|
11
|
-
This gives you an idea. Better documentation coming soon.
|
12
|
-
|
13
|
-
# Lllibrary
|
14
|
-
library = Lllibrary.new(File.join(ENV["HOME"], "music.sqlite3"), "sqlite3") do |t|
|
15
|
-
# provide a block like this if the database doesn't exist yet.
|
16
|
-
# the location, created_at, and updated_at fields are always
|
17
|
-
# created, the rest must be specified like so.
|
7
|
+
First, you need [taglib](http://taglib.github.com/). Get it from there and install it, or [install it with a package manager](https://github.com/robinst/taglib-ruby#installation). (Apparently Windows users don't have to do this, as the taglib DLL is bundled with the taglib-ruby gem.)
|
18
8
|
|
19
|
-
|
20
|
-
t.default_metadata
|
9
|
+
Then, just type:
|
21
10
|
|
22
|
-
|
23
|
-
t.itunes_metadata
|
24
|
-
|
25
|
-
# you can also specify your own custom fields
|
26
|
-
t.string :five_word_review
|
27
|
-
t.integer :popularity, null: false, default: 0
|
28
|
-
end
|
29
|
-
library.tracks
|
30
|
-
library.playlists
|
31
|
-
library.add(Dir["Music/**/*.{mp3,m4a,ogg}"], &blk)
|
32
|
-
library.add_playlist(:favourites, library.select { rating 100 })
|
33
|
-
library.import(:itunes, "/path/to/iTunes Music Library.xml", &blk)
|
34
|
-
library.clear_playlists
|
35
|
-
library.clear_all
|
36
|
-
|
37
|
-
# Lllibrary::Track
|
38
|
-
track.playlists
|
39
|
-
track.location
|
40
|
-
track.created_at
|
41
|
-
track.updated_at
|
42
|
-
track.send(metadata_field)
|
43
|
-
|
44
|
-
# Lllibrary::Playlist
|
45
|
-
playlist.name
|
46
|
-
playlist.tracks
|
47
|
-
playlist.empty?
|
48
|
-
playlist.length
|
49
|
-
playlist.add(track_or_tracks, index = nil)
|
50
|
-
playlist.remove(track_or_tracks)
|
51
|
-
playlist.total_length
|
52
|
-
playlist.sort(&blk)
|
53
|
-
playlist.shuffle
|
54
|
-
playlist.playlist_items
|
55
|
-
playlist.created_at
|
56
|
-
playlist.updated_at
|
57
|
-
playlist.clear
|
11
|
+
$ gem install lllibrary
|
58
12
|
|
59
|
-
|
60
|
-
playlist_item.position
|
61
|
-
playlist_item.track
|
62
|
-
playlist_item.playlist
|
63
|
-
playlist_item.created_at
|
64
|
-
playlist_item.updated_at
|
13
|
+
## Usage
|
65
14
|
|
66
|
-
|
67
|
-
library.select do
|
68
|
-
title("blackout") # equivalent to library.tracks.where("title LIKE '%blackout%'").all
|
69
|
-
title("blackout", match: :left) # equivalent to library.tracks.where("title LIKE 'blackout%'").all
|
70
|
-
title("blackout", match: :exact) # equivalent to library.tracks.where("title LIKE 'blackout'").all
|
71
|
-
length("1:00".."2:00") # equivalent to library.tracks.where(length: 60000...120000).all
|
72
|
-
rating(gte: 80) # equivalent to library.tracks.where("rating >= 80").all
|
73
|
-
none # []
|
74
|
-
all # library.tracks.all
|
75
|
-
end
|
15
|
+
Erm... read the inline docs.
|
76
16
|
|
77
17
|
## TODO
|
78
18
|
|
79
19
|
* Better error reporting
|
80
|
-
* Fix bug with time selectors
|
81
|
-
* Get metadata from tags in audio files without requiring a C library to be installed (like taglib)
|
82
|
-
* Allow blocks to be passed to Lllibrary#add and Lllibrary#import
|
83
|
-
* Figure out how to handle multiple database connections
|
84
20
|
* Tests, documentation, the like
|
85
21
|
|
data/lib/lllibrary.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "active_record"
|
2
2
|
require "plist"
|
3
|
+
require "taglib"
|
3
4
|
|
4
5
|
require "lllibrary/track"
|
5
6
|
require "lllibrary/playlist"
|
@@ -8,7 +9,66 @@ require "lllibrary/playlist_item"
|
|
8
9
|
require "lllibrary/database"
|
9
10
|
require "lllibrary/dsl"
|
10
11
|
|
12
|
+
# A Lllibrary represents a database of tracks and playlists. It helps you manage
|
13
|
+
# and query this database by automatically pulling metadata from the audio files
|
14
|
+
# you add, being able to import music libraries from other programs like iTunes,
|
15
|
+
# and providing a DSL for selecting songs from your music library with ease.
|
11
16
|
class Lllibrary
|
17
|
+
attr_reader :dsl
|
18
|
+
|
19
|
+
# These are the fields that taglib can pull from audio files. The keys are
|
20
|
+
# taglib's names for them, the values are the column names that lllibrary uses
|
21
|
+
# by default.
|
22
|
+
TAGLIB_METADATA = {
|
23
|
+
tag: {
|
24
|
+
album: "album",
|
25
|
+
artist: "artist",
|
26
|
+
comment: "comments",
|
27
|
+
genre: "genre",
|
28
|
+
title: "title",
|
29
|
+
track: "track_number",
|
30
|
+
year: "year"
|
31
|
+
},
|
32
|
+
audio_properties: {
|
33
|
+
length: "total_time",
|
34
|
+
bitrate: "bit_rate",
|
35
|
+
channels: "channels",
|
36
|
+
sample_rate: "sample_rate"
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
# Create a new Lllibrary object. Takes the path to the database and the
|
41
|
+
# database adapter (e.g. "sqlite3"). It also takes a block which specifies
|
42
|
+
# the schema of the database. Without this block, the tracks table will only
|
43
|
+
# have three fields: location, created_at, and updated_at. You need to
|
44
|
+
# provide a block if you want your tracks table to have fields for metadata.
|
45
|
+
# Here's an example:
|
46
|
+
#
|
47
|
+
# library = Lllibrary.new("songs.sqlite3", "sqlite3") do |t|
|
48
|
+
# t.string :title
|
49
|
+
# t.string :artist
|
50
|
+
# t.string :album
|
51
|
+
# t.integer :year
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# There are also a couple aliases that automatically add a bunch of metadata
|
55
|
+
# fields for you. These are t.default_metadata and t.itunes_metadata.
|
56
|
+
#
|
57
|
+
# t.default_metadata adds these fields: album, artist, comments, genre, title,
|
58
|
+
# track_number, year, total_time, bit_rate, channels, sample_rate. These are
|
59
|
+
# the fields that taglib is able to fill in by examining the audio files you
|
60
|
+
# add.
|
61
|
+
#
|
62
|
+
# t.itunes_metadata add almost all of the metadata fields that iTunes uses,
|
63
|
+
# and will be filled in when you import your iTunes library. These fields are:
|
64
|
+
# original_id, title, artist, composer, album, album_artist, genre, total_time,
|
65
|
+
# disc_number, disc_count, track_number, track_count, year, date_modified,
|
66
|
+
# date_added, bit_rate, sample_rate, comments, play_count, play_date, skip_count,
|
67
|
+
# skip_date, rating.
|
68
|
+
#
|
69
|
+
# Note: ActiveRecord doesn't seem to allow you to connect to multiple databases
|
70
|
+
# at the same time. Please only instantiate one Lllibrary per process.
|
71
|
+
#
|
12
72
|
def initialize(db_path, db_adapter, &schema_blk)
|
13
73
|
@db = Lllibrary::Database.new(db_path, db_adapter)
|
14
74
|
if schema_blk
|
@@ -22,26 +82,77 @@ class Lllibrary
|
|
22
82
|
@dsl = Lllibrary::DSL.new(self)
|
23
83
|
end
|
24
84
|
|
85
|
+
# Takes a block, and evaluates that block in the context of the Lllibrary's DSL.
|
86
|
+
# Inside the block, every database field on the tracks table becomes a method
|
87
|
+
# called a selector. Each selector takes a value to match tracks against, and
|
88
|
+
# returns an Array of those tracks. String-like fields have string selectors,
|
89
|
+
# and number-like fields have numeric selectors. There is also an all selector,
|
90
|
+
# a none selector, and a playlist selector. See dsl.rb for a detailed
|
91
|
+
# description of these.
|
92
|
+
#
|
93
|
+
# library.select do
|
94
|
+
# composer(:rachmanino) # example of string selector
|
95
|
+
# year(2010..2012) # example of numeric selector
|
96
|
+
# total_time(gt: "6:00") # example of time selector
|
97
|
+
# playlist(:energetic) # example of playlist selector
|
98
|
+
# all # returns array of all tracks
|
99
|
+
# none # returns empty array
|
100
|
+
# end
|
101
|
+
#
|
25
102
|
def select(&blk)
|
26
103
|
@dsl.instance_eval &blk
|
27
104
|
end
|
28
105
|
|
106
|
+
# Returns a bare Relation of the Track model.
|
29
107
|
def tracks
|
30
108
|
Lllibrary::Track.scoped
|
31
109
|
end
|
32
110
|
|
111
|
+
# Returns a bare Relation of the Playlist model.
|
33
112
|
def playlists
|
34
113
|
Lllibrary::Playlist.scoped
|
35
114
|
end
|
36
115
|
|
116
|
+
# Adds one or more tracks to the library. Takes a path to the audio file you
|
117
|
+
# wanted added, or array of multiple paths. Uses taglib to fill in metadata
|
118
|
+
# for each track, if the corresponding database fields exist. After filling in
|
119
|
+
# the metadata, if a block was given, it yields the Track object to this block.
|
120
|
+
# If the block returns a Track object (with possible modifications of your own),
|
121
|
+
# it then saves the Track and goes on to the next one. If the block doesn't
|
122
|
+
# return a Track, the track is not saved.
|
123
|
+
#
|
124
|
+
# For example, here's how you would use the filename as the title of the track
|
125
|
+
# if the title is missing from the audio file's metadata:
|
126
|
+
#
|
127
|
+
# library.add(Dir["Music/**/*.mp3"]) do |track|
|
128
|
+
# track.title ||= File.basename(track.location, ".mp3")
|
129
|
+
# track
|
130
|
+
# end
|
131
|
+
#
|
37
132
|
def add(paths_to_tracks, &blk)
|
38
133
|
Array(paths_to_tracks).each do |path|
|
39
|
-
track = tracks.new(location: path)
|
134
|
+
track = tracks.new(location: File.expand_path(path))
|
135
|
+
|
136
|
+
TagLib::FileRef.open(path) do |audio_file|
|
137
|
+
TAGLIB_METADATA.each do |tag_or_audio_properties, properties|
|
138
|
+
if audio_file.send(tag_or_audio_properties)
|
139
|
+
properties.each do |property, column|
|
140
|
+
value = audio_file.send(tag_or_audio_properties).send(property)
|
141
|
+
value *= 1000 if property == :length # convert seconds to milliseconds
|
142
|
+
value = nil if value == 0
|
143
|
+
track.send("#{column}=", value) if Track.column_names.include? column
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
40
149
|
track = blk.call(track) if blk
|
41
150
|
track.save! if track.is_a?(Track)
|
42
151
|
end
|
43
152
|
end
|
44
153
|
|
154
|
+
# Creates a new playlist and saves it. Takes a name for the playlist and an
|
155
|
+
# Array of Tracks. The order of the Tracks is preserved, of course.
|
45
156
|
def add_playlist(name, tracks)
|
46
157
|
playlist = playlists.new(name: name)
|
47
158
|
playlist.save!
|
@@ -54,7 +165,14 @@ class Lllibrary
|
|
54
165
|
end
|
55
166
|
end
|
56
167
|
|
57
|
-
|
168
|
+
# Imports a music library from another program, such as iTunes. Incidentally,
|
169
|
+
# iTunes is the only such program supported right now. Here's an example:
|
170
|
+
#
|
171
|
+
# library.import :itunes, "path/to/iTunes Music Library.xml", logger: method(:puts)
|
172
|
+
#
|
173
|
+
# I will probably redesign this method to be more flexible and such, so I'll
|
174
|
+
# curb my documenting of it any further till then.
|
175
|
+
def import(type, path, options = {}, &blk)
|
58
176
|
logger = options[:logger]
|
59
177
|
if type == :itunes
|
60
178
|
logger.("Parsing XML...") if logger
|
@@ -93,7 +211,8 @@ class Lllibrary
|
|
93
211
|
end
|
94
212
|
|
95
213
|
track = tracks.new(attributes)
|
96
|
-
|
214
|
+
track = blk.call(track)
|
215
|
+
if track && track.save
|
97
216
|
num_tracks += 1
|
98
217
|
end
|
99
218
|
end
|
@@ -125,10 +244,12 @@ class Lllibrary
|
|
125
244
|
end
|
126
245
|
end
|
127
246
|
|
247
|
+
# Deletes all your playlists.
|
128
248
|
def clear_playlists
|
129
249
|
playlists.destroy_all
|
130
250
|
end
|
131
251
|
|
252
|
+
# Deletes all tracks and playlists.
|
132
253
|
def clear_all
|
133
254
|
tracks.destroy_all
|
134
255
|
clear_playlists
|
data/lib/lllibrary/database.rb
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
class Lllibrary
|
2
|
+
# Handles connecting to the database, and initializing the schema.
|
2
3
|
class Database
|
3
4
|
def initialize(path, adapter)
|
4
5
|
@path, @adapter = path, adapter
|
6
|
+
connect
|
7
|
+
end
|
8
|
+
|
9
|
+
def connect
|
5
10
|
ActiveRecord::Base.establish_connection(adapter: @adapter, database: @path)
|
11
|
+
@connected = true
|
6
12
|
end
|
7
13
|
|
8
14
|
def disconnect
|
9
15
|
ActiveRecord::Base.remove_connection
|
16
|
+
@connected = false
|
10
17
|
end
|
11
18
|
|
12
19
|
def exists?
|
@@ -18,64 +25,74 @@ class Lllibrary
|
|
18
25
|
end
|
19
26
|
|
20
27
|
def connected?
|
21
|
-
ActiveRecord::Base.connected?
|
28
|
+
@connected &&= ActiveRecord::Base.connected?
|
22
29
|
end
|
23
30
|
|
24
31
|
def generate_schema(&blk)
|
25
32
|
ActiveRecord::Schema.define do
|
26
|
-
|
27
|
-
|
28
|
-
|
33
|
+
unless table_exists? :tracks
|
34
|
+
create_table :tracks do |t|
|
35
|
+
def t.default_metadata
|
36
|
+
# tag info supported by taglib
|
37
|
+
string :title
|
38
|
+
string :artist
|
39
|
+
string :album
|
40
|
+
string :genre
|
41
|
+
text :comments
|
42
|
+
integer :year
|
43
|
+
integer :track_number
|
29
44
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
integer :track_number
|
37
|
-
integer :year
|
38
|
-
end
|
45
|
+
# audio properties supported by taglib
|
46
|
+
integer :total_time
|
47
|
+
integer :bit_rate
|
48
|
+
integer :sample_rate
|
49
|
+
integer :channels
|
50
|
+
end
|
39
51
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
52
|
+
def t.itunes_metadata
|
53
|
+
integer :original_id
|
54
|
+
string :title
|
55
|
+
string :artist
|
56
|
+
string :composer
|
57
|
+
string :album
|
58
|
+
string :album_artist
|
59
|
+
string :genre
|
60
|
+
integer :total_time
|
61
|
+
integer :disc_number
|
62
|
+
integer :disc_count
|
63
|
+
integer :track_number
|
64
|
+
integer :track_count
|
65
|
+
integer :year
|
66
|
+
datetime :date_modified, null: false
|
67
|
+
datetime :date_added, null: false
|
68
|
+
integer :bit_rate
|
69
|
+
integer :sample_rate
|
70
|
+
text :comments
|
71
|
+
integer :play_count, null: false, default: 0
|
72
|
+
datetime :play_date
|
73
|
+
integer :skip_count, null: false, default: 0
|
74
|
+
datetime :skip_date
|
75
|
+
integer :rating, null: false, default: 0
|
76
|
+
end
|
65
77
|
|
66
|
-
|
78
|
+
blk.call(t)
|
79
|
+
end
|
67
80
|
end
|
68
81
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
82
|
+
unless table_exists? :playlists
|
83
|
+
create_table :playlists do |t|
|
84
|
+
t.string :name
|
85
|
+
t.datetime :created_at
|
86
|
+
t.datetime :updated_at
|
87
|
+
end
|
73
88
|
end
|
74
89
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
90
|
+
unless table_exists? :playlist_items
|
91
|
+
create_table :playlist_items do |t|
|
92
|
+
t.references :playlist, null: false
|
93
|
+
t.references :track, null: false
|
94
|
+
t.integer :position
|
95
|
+
end
|
79
96
|
end
|
80
97
|
end
|
81
98
|
end
|
data/lib/lllibrary/dsl.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
class Lllibrary
|
2
|
+
# Contains all the selectors used in Lllibrary's DSL. See Lllibrary#select for
|
3
|
+
# how to access the DSL.
|
2
4
|
class DSL
|
3
5
|
def initialize(library)
|
4
6
|
@library = library
|
5
7
|
end
|
6
8
|
|
9
|
+
# Turns all the columns on the tracks table into methods that I call selectors.
|
10
|
+
# Based on the column's type in the database, this either delegates to
|
11
|
+
# DSL#numeric_selector or DSL#string_selector.
|
7
12
|
def method_missing(field, *args)
|
8
13
|
if Lllibrary::Track.column_names.include? field.to_s
|
9
14
|
type = Lllibrary::Track.columns_hash[field.to_s].type
|
@@ -30,15 +35,21 @@ class Lllibrary
|
|
30
35
|
[]
|
31
36
|
end
|
32
37
|
|
33
|
-
#
|
38
|
+
# Returns an Array of tracks that match the string query on the given
|
34
39
|
# column. It's SQL underneath, so you can use % and _ as wildcards in the
|
35
40
|
# query. By default, % wildcards are inserted on the left and right of your
|
36
41
|
# query. Use the :match option to change this:
|
37
42
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
43
|
+
# :match => :middle "%query%" (default)
|
44
|
+
# :match => :left "query%"
|
45
|
+
# :match => :right "%query"
|
46
|
+
# :match => :exact "query"
|
47
|
+
#
|
48
|
+
# Here are some examples:
|
49
|
+
#
|
50
|
+
# genre(:electronic, :edm) # matches "Electronic", "Electronica", "EDM", etc.
|
51
|
+
# genre(:tmbg, match: :exact) # only matches "TMBG" (but case-insensitively)
|
52
|
+
# composer(:rachmanino, match: :left) # matches "Rachmaninov", "Rachmaninoff", but not "Sergei Rachmaninov"
|
42
53
|
#
|
43
54
|
def string_selector(column, *queries)
|
44
55
|
options = queries.last.is_a?(Hash) ? queries.pop : {}
|
@@ -57,23 +68,33 @@ class Lllibrary
|
|
57
68
|
end.flatten
|
58
69
|
end
|
59
70
|
|
60
|
-
#
|
71
|
+
# Returns an Array of tracks that satisfy certain conditions on the given
|
61
72
|
# numeric column. You can pass an exact value to check for equality, a range,
|
62
|
-
# or a hash that specifies greater-than and less-than
|
63
|
-
#
|
64
|
-
# numeric_selector :year, greater_than: 2000
|
73
|
+
# or a hash that specifies greater-than and less-than operators.
|
65
74
|
#
|
66
75
|
# The possible operators, with their shortcuts, are:
|
67
76
|
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
77
|
+
# :greater_than / :gt
|
78
|
+
# :less_than / :lt
|
79
|
+
# :greater_than_or_equal / :gte
|
80
|
+
# :less_than_or_equal / :lte
|
81
|
+
# :not_equal / :ne
|
73
82
|
#
|
74
83
|
# Note: You can only use one of these operators at a time. If you want a
|
75
84
|
# range, use a Range.
|
76
85
|
#
|
86
|
+
# Time strings like "1:23" can be given, and will be converted to a Range of
|
87
|
+
# milliseconds. For example, "1:00" will be treated like 60000..60999.
|
88
|
+
# Obviously, any field this is used on should be storing time in milliseconds.
|
89
|
+
#
|
90
|
+
# Here's some examples:
|
91
|
+
#
|
92
|
+
# year(2010..2012) # equivalent to year(2010, 2011, 2012)
|
93
|
+
# year(2011) # only matches 2011
|
94
|
+
# year(gte: 2000) # matches 2000 and up
|
95
|
+
# total_time("2:30") # matches 150000..150999 milliseconds
|
96
|
+
# total_time("1:00".."2:00") # matches 60000..120999 milliseconds
|
97
|
+
#
|
77
98
|
def numeric_selector(column, *values_or_hash)
|
78
99
|
parse = lambda do |x|
|
79
100
|
if x.is_a? String
|
@@ -86,6 +107,7 @@ class Lllibrary
|
|
86
107
|
elsif x.is_a? Range
|
87
108
|
left = x.begin.is_a?(String) ? Lllibrary.parse_time(x.begin) : x.begin
|
88
109
|
right = x.end.is_a?(String) ? Lllibrary.parse_time(x.end) : x.end
|
110
|
+
right += 999 if right % 1000 == 0 && !x.exclude_end?
|
89
111
|
Range.new(left, right, x.exclude_end?)
|
90
112
|
else
|
91
113
|
x
|
@@ -127,10 +149,15 @@ class Lllibrary
|
|
127
149
|
raise ArgumentError, "'#{operator}' isn't a valid operator"
|
128
150
|
end
|
129
151
|
else
|
130
|
-
|
152
|
+
values_or_hash.map do |value|
|
153
|
+
@library.tracks.where(column => parse.(value)).all
|
154
|
+
end.flatten
|
131
155
|
end
|
132
156
|
end
|
133
157
|
|
158
|
+
# Returns an Array of all tracks that are in a Playlist whose name matches
|
159
|
+
# one of the strings given to this selector. It's kind of ugly, I will
|
160
|
+
# probably be changing this.
|
134
161
|
def playlist(*names)
|
135
162
|
names.map do |name|
|
136
163
|
playlists = @library.playlists.arel_table
|
data/lib/lllibrary/playlist.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
class Lllibrary
|
2
|
+
# A Playlist is a list of Tracks in a particular order and with possible
|
3
|
+
# repeats of Tracks. A Playlist has a name, and that's about it.
|
2
4
|
class Playlist < ActiveRecord::Base
|
3
5
|
has_many :playlist_items, order: "playlist_items.position ASC", dependent: :destroy
|
4
6
|
has_many :tracks, through: :playlist_items, order: "playlist_items.position ASC"
|
5
7
|
|
8
|
+
# Adds the given Track or Array of Tracks to the end of the Playlist. If
|
9
|
+
# an index is given, the track(s) are inserted at that position.
|
6
10
|
def add(track_or_tracks, at = nil)
|
7
11
|
base_position = nil
|
8
12
|
if at
|
@@ -24,35 +28,57 @@ class Lllibrary
|
|
24
28
|
reload
|
25
29
|
end
|
26
30
|
|
31
|
+
# Removes the Track or Array of Tracks from the Playlist.
|
27
32
|
def remove(track_or_tracks)
|
28
33
|
playlist_items.where(track_id: track_or_tracks).destroy_all
|
29
34
|
reload
|
30
35
|
end
|
31
36
|
|
37
|
+
# Removes the Track at the given index. If a number is given in the
|
38
|
+
# second argument, removes that number of tracks starting from index.
|
39
|
+
def remove_at(index, n = 1)
|
40
|
+
playlist_items.offset(index).limit(n).all.each(&:destroy)
|
41
|
+
reload
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns true if the playlist is empty.
|
32
45
|
def empty?
|
33
46
|
playlist_items.empty?
|
34
47
|
end
|
35
48
|
|
49
|
+
# Gets the number of items in the playlist.
|
36
50
|
def length
|
37
51
|
playlist_items.count
|
38
52
|
end
|
39
53
|
|
54
|
+
# Calculates the total length of the playlist by summing the tracks'
|
55
|
+
# total_time column by default, which stores milliseconds by default.
|
56
|
+
# You can provide a different column using the :field option, like so:
|
57
|
+
#
|
58
|
+
# playlist.total_length(field: :length)
|
59
|
+
#
|
60
|
+
# If the given field stores time in milliseconds, this method returns
|
61
|
+
# milliseconds. If it stores time in seconds, this returns seconds.
|
62
|
+
# And so on.
|
40
63
|
def total_length(options = {})
|
41
|
-
tracks.sum(options[:field] || :
|
64
|
+
tracks.sum(options[:field] || :total_time)
|
42
65
|
end
|
43
66
|
|
67
|
+
# Sorts the playlist using the given block, then saves. See Array#sort.
|
44
68
|
def sort(&blk)
|
45
69
|
sorted_tracks = tracks.sort(&blk)
|
46
70
|
clear
|
47
71
|
add(sorted_tracks)
|
48
72
|
end
|
49
73
|
|
74
|
+
# Shuffles the playlist and saves.
|
50
75
|
def shuffle
|
51
76
|
shuffled_tracks = tracks.shuffle
|
52
77
|
clear
|
53
78
|
add(shuffled_tracks)
|
54
79
|
end
|
55
80
|
|
81
|
+
# Clears the playlist and saves.
|
56
82
|
def clear
|
57
83
|
playlist_items.destroy_all
|
58
84
|
reload
|
data/lib/lllibrary/track.rb
CHANGED
@@ -1,8 +1,17 @@
|
|
1
1
|
class Lllibrary
|
2
|
+
# A Track represents an audio file. At minimum, it contains a location field
|
3
|
+
# which stores the path to the audio file it represents.
|
2
4
|
class Track < ActiveRecord::Base
|
3
5
|
has_many :playlist_items, dependent: :destroy
|
4
6
|
has_many :playlists, through: :playlist_items
|
5
7
|
|
6
8
|
validates :location, presence: true, uniqueness: true
|
9
|
+
|
10
|
+
# By default, Tracks are sorted by their file path.
|
11
|
+
#
|
12
|
+
# TODO: have a way of specifying sorting by parsing a Symbol like :artist_asc_album_asc_track_number_asc
|
13
|
+
def <=>(other)
|
14
|
+
location <=> other.location
|
15
|
+
end
|
7
16
|
end
|
8
17
|
end
|
data/lllibrary.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "lllibrary"
|
3
|
-
s.version = "0.0.
|
4
|
-
s.date = "2012-
|
3
|
+
s.version = "0.0.2"
|
4
|
+
s.date = "2012-02-02"
|
5
5
|
s.summary = "A library for managing and querying a music library."
|
6
6
|
s.description = "lllibrary is a Ruby library for managing and querying a music library."
|
7
7
|
s.author = "Jeremy Ruten"
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "lllibrary.gemspec", "README.md"]
|
14
14
|
s.files += Dir["lib/**/*.rb"]
|
15
15
|
|
16
|
-
%w(bundler plist sqlite3 activerecord activesupport).each do |gem_name|
|
16
|
+
%w(bundler plist sqlite3 activerecord activesupport taglib-ruby).each do |gem_name|
|
17
17
|
s.add_runtime_dependency gem_name
|
18
18
|
end
|
19
19
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lllibrary
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-02-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -91,6 +91,22 @@ dependencies:
|
|
91
91
|
- - ! '>='
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: taglib-ruby
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
94
110
|
- !ruby/object:Gem::Dependency
|
95
111
|
name: rake
|
96
112
|
requirement: !ruby/object:Gem::Requirement
|