lllibrary 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +39 -0
- data/LICENSE +7 -0
- data/README.md +85 -0
- data/lib/lllibrary.rb +170 -0
- data/lib/lllibrary/database.rb +84 -0
- data/lib/lllibrary/dsl.rb +144 -0
- data/lib/lllibrary/playlist.rb +62 -0
- data/lib/lllibrary/playlist_item.rb +6 -0
- data/lib/lllibrary/track.rb +8 -0
- data/lllibrary.gemspec +23 -0
- metadata +152 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
lllibrary (0.0.1)
|
5
|
+
activerecord
|
6
|
+
activesupport
|
7
|
+
bundler
|
8
|
+
plist
|
9
|
+
sqlite3
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: http://rubygems.org/
|
13
|
+
specs:
|
14
|
+
activemodel (3.2.9)
|
15
|
+
activesupport (= 3.2.9)
|
16
|
+
builder (~> 3.0.0)
|
17
|
+
activerecord (3.2.9)
|
18
|
+
activemodel (= 3.2.9)
|
19
|
+
activesupport (= 3.2.9)
|
20
|
+
arel (~> 3.0.2)
|
21
|
+
tzinfo (~> 0.3.29)
|
22
|
+
activesupport (3.2.9)
|
23
|
+
i18n (~> 0.6)
|
24
|
+
multi_json (~> 1.0)
|
25
|
+
arel (3.0.2)
|
26
|
+
builder (3.0.4)
|
27
|
+
i18n (0.6.1)
|
28
|
+
multi_json (1.5.0)
|
29
|
+
plist (3.1.0)
|
30
|
+
rake (10.0.3)
|
31
|
+
sqlite3 (1.3.6)
|
32
|
+
tzinfo (0.3.35)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
lllibrary!
|
39
|
+
rake
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012 Jeremy Ruten
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#lllibrary
|
2
|
+
|
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
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
$ gem install lllibrary
|
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.
|
18
|
+
|
19
|
+
# adds database fields for title, artist, etc.
|
20
|
+
t.default_metadata
|
21
|
+
|
22
|
+
# adds all the database fields that iTunes uses
|
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
|
58
|
+
|
59
|
+
# Lllibrary::PlaylistItem
|
60
|
+
playlist_item.position
|
61
|
+
playlist_item.track
|
62
|
+
playlist_item.playlist
|
63
|
+
playlist_item.created_at
|
64
|
+
playlist_item.updated_at
|
65
|
+
|
66
|
+
# Selectors DSL
|
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
|
76
|
+
|
77
|
+
## TODO
|
78
|
+
|
79
|
+
* 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
|
+
* Tests, documentation, the like
|
85
|
+
|
data/lib/lllibrary.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "plist"
|
3
|
+
|
4
|
+
require "lllibrary/track"
|
5
|
+
require "lllibrary/playlist"
|
6
|
+
require "lllibrary/playlist_item"
|
7
|
+
|
8
|
+
require "lllibrary/database"
|
9
|
+
require "lllibrary/dsl"
|
10
|
+
|
11
|
+
class Lllibrary
|
12
|
+
def initialize(db_path, db_adapter, &schema_blk)
|
13
|
+
@db = Lllibrary::Database.new(db_path, db_adapter)
|
14
|
+
if schema_blk
|
15
|
+
@db.generate_schema do |t|
|
16
|
+
t.string :location
|
17
|
+
t.timestamps
|
18
|
+
|
19
|
+
schema_blk.call(t)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@dsl = Lllibrary::DSL.new(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def select(&blk)
|
26
|
+
@dsl.instance_eval &blk
|
27
|
+
end
|
28
|
+
|
29
|
+
def tracks
|
30
|
+
Lllibrary::Track.scoped
|
31
|
+
end
|
32
|
+
|
33
|
+
def playlists
|
34
|
+
Lllibrary::Playlist.scoped
|
35
|
+
end
|
36
|
+
|
37
|
+
def add(paths_to_tracks, &blk)
|
38
|
+
Array(paths_to_tracks).each do |path|
|
39
|
+
track = tracks.new(location: path)
|
40
|
+
track = blk.call(track) if blk
|
41
|
+
track.save! if track.is_a?(Track)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_playlist(name, tracks)
|
46
|
+
playlist = playlists.new(name: name)
|
47
|
+
playlist.save!
|
48
|
+
tracks.each.with_index do |track, i|
|
49
|
+
playlist_item = Lllibrary::PlaylistItem.new
|
50
|
+
playlist_item.playlist = playlist
|
51
|
+
playlist_item.track = track
|
52
|
+
playlist_item.position = i
|
53
|
+
playlist_item.save!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def import(type, path, options = {})
|
58
|
+
logger = options[:logger]
|
59
|
+
if type == :itunes
|
60
|
+
logger.("Parsing XML...") if logger
|
61
|
+
data = Plist::parse_xml(path)
|
62
|
+
|
63
|
+
logger.("Importing #{data['Tracks'].length} tracks...") if logger
|
64
|
+
num_tracks = 0
|
65
|
+
whitelist = tracks.new.attributes.keys
|
66
|
+
data["Tracks"].each do |track_id, row|
|
67
|
+
if row["Kind"] !~ /audio/
|
68
|
+
logger.("[skipping non-audio file]") if logger
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
# row already contains a hash of attributes almost ready to be passed to
|
73
|
+
# ActiveRecord. We just need to modify the keys, e.g. change "Play Count"
|
74
|
+
# to "play_count".
|
75
|
+
row["Title"] = row.delete("Name")
|
76
|
+
row["Play Date"] = row.delete("Play Date UTC")
|
77
|
+
row["Original ID"] = row.delete("Track ID")
|
78
|
+
attributes = row.inject({}) do |acc, (key, value)|
|
79
|
+
attribute = key.gsub(" ", "").underscore
|
80
|
+
acc[attribute] = value if whitelist.include? attribute
|
81
|
+
acc
|
82
|
+
end
|
83
|
+
|
84
|
+
# change iTunes' URL-style locations into simple paths
|
85
|
+
if attributes["location"] && attributes["location"] =~ /^file:\/\//
|
86
|
+
attributes["location"].sub! /^file:\/\/localhost/, ""
|
87
|
+
|
88
|
+
# CGI::unescape changes plus signs to spaces. This is a work around to
|
89
|
+
# keep the plus signs.
|
90
|
+
attributes["location"].gsub! "+", "%2B"
|
91
|
+
|
92
|
+
attributes["location"] = CGI::unescape(attributes["location"])
|
93
|
+
end
|
94
|
+
|
95
|
+
track = tracks.new(attributes)
|
96
|
+
if track.save
|
97
|
+
num_tracks += 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
logger.("Imported #{num_tracks} tracks successfully.") if logger
|
101
|
+
|
102
|
+
if tracks.new.attributes.keys.include? "original_id"
|
103
|
+
logger.("Importing #{data['Playlists'].length} playlists...") if logger
|
104
|
+
num_playlists = 0
|
105
|
+
data["Playlists"].each do |playlist_data|
|
106
|
+
playlist = []
|
107
|
+
|
108
|
+
if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist_data["Name"]
|
109
|
+
logger.("[skipping \"#{playlist_data['Name']}\" playlist]") if logger
|
110
|
+
elsif playlist_data["Playlist Items"].nil?
|
111
|
+
logger.("[skipping \"#{playlist_data['Name']}\" playlist (because it's empty)]") if logger
|
112
|
+
else
|
113
|
+
playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id|
|
114
|
+
playlist << tracks.where(original_id: original_id).first
|
115
|
+
end
|
116
|
+
playlist.compact!
|
117
|
+
add_playlist(playlist_data["Name"], playlist)
|
118
|
+
num_playlists += 1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
logger.("Imported #{num_playlists} playlists successfully.") if logger
|
122
|
+
else
|
123
|
+
logger.("Can't import playlists because tracks table doesn't have an original_id field.") if logger
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def clear_playlists
|
129
|
+
playlists.destroy_all
|
130
|
+
end
|
131
|
+
|
132
|
+
def clear_all
|
133
|
+
tracks.destroy_all
|
134
|
+
clear_playlists
|
135
|
+
end
|
136
|
+
|
137
|
+
# Helper method to format a number of milliseconds as a string like
|
138
|
+
# "1:03:56.555". The only option is :include_milliseconds, true by default. If
|
139
|
+
# false, milliseconds won't be included in the formatted string.
|
140
|
+
def self.format_time(milliseconds, options = {})
|
141
|
+
ms = milliseconds % 1000
|
142
|
+
seconds = (milliseconds / 1000) % 60
|
143
|
+
minutes = (milliseconds / 60000) % 60
|
144
|
+
hours = milliseconds / 3600000
|
145
|
+
|
146
|
+
if ms.zero? || options[:include_milliseconds] == false
|
147
|
+
ms_string = ""
|
148
|
+
else
|
149
|
+
ms_string = ".%03d" % [ms]
|
150
|
+
end
|
151
|
+
|
152
|
+
if hours > 0
|
153
|
+
"%d:%02d:%02d%s" % [hours, minutes, seconds, ms_string]
|
154
|
+
else
|
155
|
+
"%d:%02d%s" % [minutes, seconds, ms_string]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Helper method to parse a string like "1:03:56.555" and return the number of
|
160
|
+
# milliseconds that time length represents.
|
161
|
+
def self.parse_time(string)
|
162
|
+
parts = string.split(":").map(&:to_f)
|
163
|
+
parts = [0] + parts if parts.length == 2
|
164
|
+
hours, minutes, seconds = parts
|
165
|
+
seconds = hours * 3600 + minutes * 60 + seconds
|
166
|
+
milliseconds = seconds * 1000
|
167
|
+
milliseconds.to_i
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Lllibrary
|
2
|
+
class Database
|
3
|
+
def initialize(path, adapter)
|
4
|
+
@path, @adapter = path, adapter
|
5
|
+
ActiveRecord::Base.establish_connection(adapter: @adapter, database: @path)
|
6
|
+
end
|
7
|
+
|
8
|
+
def disconnect
|
9
|
+
ActiveRecord::Base.remove_connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def exists?
|
13
|
+
File.exists? @path
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete
|
17
|
+
FileUtils.rm @path
|
18
|
+
end
|
19
|
+
|
20
|
+
def connected?
|
21
|
+
ActiveRecord::Base.connected?
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_schema(&blk)
|
25
|
+
ActiveRecord::Schema.define do
|
26
|
+
create_table :tracks do |t|
|
27
|
+
t.string :location
|
28
|
+
t.timestamps
|
29
|
+
|
30
|
+
def t.default_metadata
|
31
|
+
string :title
|
32
|
+
string :artist
|
33
|
+
string :album
|
34
|
+
string :genre
|
35
|
+
integer :length
|
36
|
+
integer :track_number
|
37
|
+
integer :year
|
38
|
+
end
|
39
|
+
|
40
|
+
def t.itunes_metadata
|
41
|
+
integer :original_id
|
42
|
+
string :title
|
43
|
+
string :artist
|
44
|
+
string :composer
|
45
|
+
string :album
|
46
|
+
string :album_artist
|
47
|
+
string :genre
|
48
|
+
integer :total_time
|
49
|
+
integer :disc_number
|
50
|
+
integer :disc_count
|
51
|
+
integer :track_number
|
52
|
+
integer :track_count
|
53
|
+
integer :year
|
54
|
+
datetime :date_modified, null: false
|
55
|
+
datetime :date_added, null: false
|
56
|
+
integer :bit_rate
|
57
|
+
integer :sample_rate
|
58
|
+
text :comments
|
59
|
+
integer :play_count, null: false, default: 0
|
60
|
+
datetime :play_date
|
61
|
+
integer :skip_count, null: false, default: 0
|
62
|
+
datetime :skip_date
|
63
|
+
integer :rating, null: false, default: 0
|
64
|
+
end
|
65
|
+
|
66
|
+
blk.call(t)
|
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
|
+
|
@@ -0,0 +1,144 @@
|
|
1
|
+
class Lllibrary
|
2
|
+
class DSL
|
3
|
+
def initialize(library)
|
4
|
+
@library = library
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(field, *args)
|
8
|
+
if Lllibrary::Track.column_names.include? field.to_s
|
9
|
+
type = Lllibrary::Track.columns_hash[field.to_s].type
|
10
|
+
case type
|
11
|
+
when :integer, :float, :decimal, :boolean, :datetime, :timestamp, :date, :time
|
12
|
+
numeric_selector(field, *args)
|
13
|
+
when :string, :text
|
14
|
+
string_selector(field, *args)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "lllibrary: selectors aren't supported for #{field}'s type '#{type}'"
|
17
|
+
end
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Selects all the tracks in the library.
|
24
|
+
def all
|
25
|
+
@library.tracks.all
|
26
|
+
end
|
27
|
+
|
28
|
+
# Selects no tracks, returning an empty playlist.
|
29
|
+
def none
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Makes a playlist out of tracks that match the string query on the given
|
34
|
+
# column. It's SQL underneath, so you can use % and _ as wildcards in the
|
35
|
+
# query. By default, % wildcards are inserted on the left and right of your
|
36
|
+
# query. Use the :match option to change this:
|
37
|
+
#
|
38
|
+
# :match => :middle "%query%" (default)
|
39
|
+
# :match => :left "query%"
|
40
|
+
# :match => :right "%query"
|
41
|
+
# :match => :exact "query"
|
42
|
+
#
|
43
|
+
def string_selector(column, *queries)
|
44
|
+
options = queries.last.is_a?(Hash) ? queries.pop : {}
|
45
|
+
options[:match] ||= :middle
|
46
|
+
|
47
|
+
tracks = @library.tracks.arel_table
|
48
|
+
queries.map do |query|
|
49
|
+
query = {
|
50
|
+
exact: "#{query}",
|
51
|
+
left: "#{query}%",
|
52
|
+
right: "%#{query}",
|
53
|
+
middle: "%#{query}%"
|
54
|
+
}[options[:match]]
|
55
|
+
|
56
|
+
@library.tracks.where(tracks[column].matches(query)).all
|
57
|
+
end.flatten
|
58
|
+
end
|
59
|
+
|
60
|
+
# Makes a playlist out of tracks that satisfy certain conditions on the given
|
61
|
+
# 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 options like this:
|
63
|
+
#
|
64
|
+
# numeric_selector :year, greater_than: 2000
|
65
|
+
#
|
66
|
+
# The possible operators, with their shortcuts, are:
|
67
|
+
#
|
68
|
+
# :greater_than / :gt
|
69
|
+
# :less_than / :lt
|
70
|
+
# :greater_than_or_equal / :gte
|
71
|
+
# :less_than_or_equal / :lte
|
72
|
+
# :not_equal / :ne
|
73
|
+
#
|
74
|
+
# Note: You can only use one of these operators at a time. If you want a
|
75
|
+
# range, use a Range.
|
76
|
+
#
|
77
|
+
def numeric_selector(column, *values_or_hash)
|
78
|
+
parse = lambda do |x|
|
79
|
+
if x.is_a? String
|
80
|
+
ms = Lllibrary.parse_time(x)
|
81
|
+
if ms % 1000 == 0
|
82
|
+
ms...(ms + 1000)
|
83
|
+
else
|
84
|
+
ms
|
85
|
+
end
|
86
|
+
elsif x.is_a? Range
|
87
|
+
left = x.begin.is_a?(String) ? Lllibrary.parse_time(x.begin) : x.begin
|
88
|
+
right = x.end.is_a?(String) ? Lllibrary.parse_time(x.end) : x.end
|
89
|
+
Range.new(left, right, x.exclude_end?)
|
90
|
+
else
|
91
|
+
x
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if values_or_hash.last.is_a? Hash
|
96
|
+
operator = values_or_hash.last.keys.first.to_sym
|
97
|
+
value = parse.(values_or_hash.last.values.first)
|
98
|
+
case operator
|
99
|
+
when :greater_than, :gt
|
100
|
+
op = ">"
|
101
|
+
if value.is_a? Range
|
102
|
+
op = ">=" if value.exclude_end?
|
103
|
+
value = value.end
|
104
|
+
end
|
105
|
+
@library.tracks.where("tracks.#{column} #{op} ?", value).all
|
106
|
+
when :less_than, :lt
|
107
|
+
value = value.begin if value.is_a? Range
|
108
|
+
@library.tracks.where("tracks.#{column} < ?", value).all
|
109
|
+
when :greater_than_or_equal, :gte
|
110
|
+
value = value.begin if value.is_a? Range
|
111
|
+
@library.tracks.where("tracks.#{column} >= ?", value).all
|
112
|
+
when :less_than_or_equal, :lte
|
113
|
+
op = "<="
|
114
|
+
if value.is_a? Range
|
115
|
+
op = "<" if value.exclude_end?
|
116
|
+
value = value.end
|
117
|
+
end
|
118
|
+
@library.tracks.where("tracks.#{column} #{op} ?", value)
|
119
|
+
when :not_equal, :ne
|
120
|
+
if value.is_a? Range
|
121
|
+
op = value.exclude_end? ? ">=" : ">"
|
122
|
+
@library.tracks.where("tracks.#{column} < ? AND tracks.#{column} #{op} ?", value.begin, value.end)
|
123
|
+
else
|
124
|
+
@library.tracks.where("tracks.#{column} != ?", value)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
raise ArgumentError, "'#{operator}' isn't a valid operator"
|
128
|
+
end
|
129
|
+
else
|
130
|
+
@library.tracks.where(column => parse.(values_or_hash)).all
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def playlist(*names)
|
135
|
+
names.map do |name|
|
136
|
+
playlists = @library.playlists.arel_table
|
137
|
+
if playlist = @library.playlists.where(playlists[:name].matches(name.to_s)).first
|
138
|
+
playlist.tracks.all
|
139
|
+
end
|
140
|
+
end.flatten
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Lllibrary
|
2
|
+
class Playlist < ActiveRecord::Base
|
3
|
+
has_many :playlist_items, order: "playlist_items.position ASC", dependent: :destroy
|
4
|
+
has_many :tracks, through: :playlist_items, order: "playlist_items.position ASC"
|
5
|
+
|
6
|
+
def add(track_or_tracks, at = nil)
|
7
|
+
base_position = nil
|
8
|
+
if at
|
9
|
+
at_track = playlist_items.offset(at).first
|
10
|
+
base_position = at_track.position if at_track
|
11
|
+
end
|
12
|
+
base_position ||= empty? ? 0 : playlist_items.last.position + 1
|
13
|
+
|
14
|
+
playlist_items.where("playlist_items.position >= ?", base_position).update_all("position = position + #{Array(track_or_tracks).length}")
|
15
|
+
|
16
|
+
Array(track_or_tracks).each.with_index do |track, i|
|
17
|
+
playlist_item = Lllibrary::PlaylistItem.new
|
18
|
+
playlist_item.track = track
|
19
|
+
playlist_item.playlist = self
|
20
|
+
playlist_item.position = base_position + i
|
21
|
+
playlist_item.save!
|
22
|
+
end
|
23
|
+
|
24
|
+
reload
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove(track_or_tracks)
|
28
|
+
playlist_items.where(track_id: track_or_tracks).destroy_all
|
29
|
+
reload
|
30
|
+
end
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
playlist_items.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def length
|
37
|
+
playlist_items.count
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_length(options = {})
|
41
|
+
tracks.sum(options[:field] || :length)
|
42
|
+
end
|
43
|
+
|
44
|
+
def sort(&blk)
|
45
|
+
sorted_tracks = tracks.sort(&blk)
|
46
|
+
clear
|
47
|
+
add(sorted_tracks)
|
48
|
+
end
|
49
|
+
|
50
|
+
def shuffle
|
51
|
+
shuffled_tracks = tracks.shuffle
|
52
|
+
clear
|
53
|
+
add(shuffled_tracks)
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
playlist_items.destroy_all
|
58
|
+
reload
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/lllibrary.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "lllibrary"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.date = "2012-12-17"
|
5
|
+
s.summary = "A library for managing and querying a music library."
|
6
|
+
s.description = "lllibrary is a Ruby library for managing and querying a music library."
|
7
|
+
s.author = "Jeremy Ruten"
|
8
|
+
s.email = "jeremy.ruten@gmail.com"
|
9
|
+
s.homepage = "http://github.com/yjerem/lllibrary"
|
10
|
+
s.license = "MIT"
|
11
|
+
s.required_ruby_version = ">= 1.9.2"
|
12
|
+
|
13
|
+
s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "lllibrary.gemspec", "README.md"]
|
14
|
+
s.files += Dir["lib/**/*.rb"]
|
15
|
+
|
16
|
+
%w(bundler plist sqlite3 activerecord activesupport).each do |gem_name|
|
17
|
+
s.add_runtime_dependency gem_name
|
18
|
+
end
|
19
|
+
|
20
|
+
%w(rake).each do |gem_name|
|
21
|
+
s.add_development_dependency gem_name
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lllibrary
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeremy Ruten
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: plist
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sqlite3
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activerecord
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: activesupport
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rake
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: lllibrary is a Ruby library for managing and querying a music library.
|
111
|
+
email: jeremy.ruten@gmail.com
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- Gemfile
|
117
|
+
- Gemfile.lock
|
118
|
+
- LICENSE
|
119
|
+
- lllibrary.gemspec
|
120
|
+
- README.md
|
121
|
+
- lib/lllibrary/database.rb
|
122
|
+
- lib/lllibrary/dsl.rb
|
123
|
+
- lib/lllibrary/playlist.rb
|
124
|
+
- lib/lllibrary/playlist_item.rb
|
125
|
+
- lib/lllibrary/track.rb
|
126
|
+
- lib/lllibrary.rb
|
127
|
+
homepage: http://github.com/yjerem/lllibrary
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: 1.9.2
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 1.8.23
|
149
|
+
signing_key:
|
150
|
+
specification_version: 3
|
151
|
+
summary: A library for managing and querying a music library.
|
152
|
+
test_files: []
|