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