Class: Lllibrary
- Inherits:
-
Object
- Object
- Lllibrary
- Defined in:
- lib/lllibrary.rb,
lib/lllibrary/dsl.rb,
lib/lllibrary/track.rb,
lib/lllibrary/database.rb,
lib/lllibrary/playlist.rb,
lib/lllibrary/playlist_item.rb
Overview
A Lllibrary represents a database of tracks and playlists. It helps you manage and query this database by automatically pulling metadata from the audio files you add, being able to import music libraries from other programs like iTunes, and providing a DSL for selecting songs from your music library with ease.
Defined Under Namespace
Classes: DSL, Database, Playlist, PlaylistItem, Track
Constant Summary collapse
- TAGLIB_METADATA =
These are the fields that taglib can pull from audio files. The keys are taglib’s names for them, the values are the column names that lllibrary uses by default.
{ tag: { album: "album", artist: "artist", comment: "comments", genre: "genre", title: "title", track: "track_number", year: "year" }, audio_properties: { length: "total_time", bitrate: "bit_rate", channels: "channels", sample_rate: "sample_rate" } }
Instance Attribute Summary collapse
-
#dsl ⇒ Object
readonly
Returns the value of attribute dsl.
Class Method Summary collapse
-
.format_time(milliseconds, options = {}) ⇒ Object
Helper method to format a number of milliseconds as a string like “1:03:56.555”.
-
.parse_time(string) ⇒ Object
Helper method to parse a string like “1:03:56.555” and return the number of milliseconds that time length represents.
Instance Method Summary collapse
-
#add(paths_to_tracks, &blk) ⇒ Object
Adds one or more tracks to the library.
-
#add_playlist(name, tracks) ⇒ Object
Creates a new playlist and saves it.
-
#clear_all ⇒ Object
Deletes all tracks and playlists.
-
#clear_playlists ⇒ Object
Deletes all your playlists.
-
#import(type, path, options = {}, &blk) ⇒ Object
Imports a music library from another program, such as iTunes.
-
#initialize(db_path, db_adapter, &schema_blk) ⇒ Lllibrary
constructor
Create a new Lllibrary object.
-
#playlists ⇒ Object
Returns a bare Relation of the Playlist model.
-
#select(&blk) ⇒ Object
Takes a block, and evaluates that block in the context of the Lllibrary’s DSL.
-
#tracks ⇒ Object
Returns a bare Relation of the Track model.
Constructor Details
#initialize(db_path, db_adapter, &schema_blk) ⇒ Lllibrary
Create a new Lllibrary object. Takes the path to the database and the database adapter (e.g. “sqlite3”). It also takes a block which specifies the schema of the database. Without this block, the tracks table will only have three fields: location, created_at, and updated_at. You need to provide a block if you want your tracks table to have fields for metadata. Here’s an example:
library = Lllibrary.new("songs.sqlite3", "sqlite3") do |t|
t.string :title
t.string :artist
t.string :album
t.integer :year
end
There are also a couple aliases that automatically add a bunch of metadata fields for you. These are t.default_metadata and t.itunes_metadata.
t.default_metadata adds these fields: album, artist, comments, genre, title, track_number, year, total_time, bit_rate, channels, sample_rate. These are the fields that taglib is able to fill in by examining the audio files you add.
t.itunes_metadata add almost all of the metadata fields that iTunes uses, and will be filled in when you import your iTunes library. These fields are: original_id, title, artist, composer, album, album_artist, genre, total_time, disc_number, disc_count, track_number, track_count, year, date_modified, date_added, bit_rate, sample_rate, comments, play_count, play_date, skip_count, skip_date, rating.
Note: ActiveRecord doesn’t seem to allow you to connect to multiple databases at the same time. Please only instantiate one Lllibrary per process.
72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/lllibrary.rb', line 72 def initialize(db_path, db_adapter, &schema_blk) @db = Lllibrary::Database.new(db_path, db_adapter) if schema_blk @db.generate_schema do |t| t.string :location t. schema_blk.call(t) end end @dsl = Lllibrary::DSL.new(self) end |
Instance Attribute Details
#dsl ⇒ Object (readonly)
Returns the value of attribute dsl.
17 18 19 |
# File 'lib/lllibrary.rb', line 17 def dsl @dsl end |
Class Method Details
.format_time(milliseconds, options = {}) ⇒ Object
Helper method to format a number of milliseconds as a string like “1:03:56.555”. The only option is :include_milliseconds, true by default. If false, milliseconds won’t be included in the formatted string.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/lllibrary.rb', line 261 def self.format_time(milliseconds, = {}) ms = milliseconds % 1000 seconds = (milliseconds / 1000) % 60 minutes = (milliseconds / 60000) % 60 hours = milliseconds / 3600000 if ms.zero? || [:include_milliseconds] == false ms_string = "" else ms_string = ".%03d" % [ms] end if hours > 0 "%d:%02d:%02d%s" % [hours, minutes, seconds, ms_string] else "%d:%02d%s" % [minutes, seconds, ms_string] end end |
.parse_time(string) ⇒ Object
Helper method to parse a string like “1:03:56.555” and return the number of milliseconds that time length represents.
282 283 284 285 286 287 288 289 |
# File 'lib/lllibrary.rb', line 282 def self.parse_time(string) parts = string.split(":").map(&:to_f) parts = [0] + parts if parts.length == 2 hours, minutes, seconds = parts seconds = hours * 3600 + minutes * 60 + seconds milliseconds = seconds * 1000 milliseconds.to_i end |
Instance Method Details
#add(paths_to_tracks, &blk) ⇒ Object
Adds one or more tracks to the library. Takes a path to the audio file you wanted added, or array of multiple paths. Uses taglib to fill in metadata for each track, if the corresponding database fields exist. After filling in the metadata, if a block was given, it yields the Track object to this block. If the block returns a Track object (with possible modifications of your own), it then saves the Track and goes on to the next one. If the block doesn’t return a Track, the track is not saved.
For example, here’s how you would use the filename as the title of the track if the title is missing from the audio file’s metadata:
library.add(Dir["Music/**/*.mp3"]) do |track|
track.title ||= File.basename(track.location, ".mp3")
track
end
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/lllibrary.rb', line 132 def add(paths_to_tracks, &blk) Array(paths_to_tracks).each do |path| track = tracks.new(location: File.(path)) TagLib::FileRef.open(path) do |audio_file| TAGLIB_METADATA.each do |tag_or_audio_properties, properties| if audio_file.send(tag_or_audio_properties) properties.each do |property, column| value = audio_file.send(tag_or_audio_properties).send(property) value *= 1000 if property == :length # convert seconds to milliseconds value = nil if value == 0 track.send("#{column}=", value) if Track.column_names.include? column end end end end track = blk.call(track) if blk track.save! if track.is_a?(Track) end end |
#add_playlist(name, tracks) ⇒ Object
Creates a new playlist and saves it. Takes a name for the playlist and an Array of Tracks. The order of the Tracks is preserved, of course.
156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/lllibrary.rb', line 156 def add_playlist(name, tracks) playlist = playlists.new(name: name) playlist.save! tracks.each.with_index do |track, i| playlist_item = Lllibrary::PlaylistItem.new playlist_item.playlist = playlist playlist_item.track = track playlist_item.position = i playlist_item.save! end end |
#clear_all ⇒ Object
Deletes all tracks and playlists.
253 254 255 256 |
# File 'lib/lllibrary.rb', line 253 def clear_all tracks.destroy_all clear_playlists end |
#clear_playlists ⇒ Object
Deletes all your playlists.
248 249 250 |
# File 'lib/lllibrary.rb', line 248 def clear_playlists playlists.destroy_all end |
#import(type, path, options = {}, &blk) ⇒ Object
Imports a music library from another program, such as iTunes. Incidentally, iTunes is the only such program supported right now. Here’s an example:
library.import :itunes, "path/to/iTunes Music Library.xml", logger: method(:puts)
I will probably redesign this method to be more flexible and such, so I’ll curb my documenting of it any further till then.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/lllibrary.rb', line 175 def import(type, path, = {}, &blk) logger = [:logger] if type == :itunes logger.("Parsing XML...") if logger data = Plist::parse_xml(path) logger.("Importing #{data['Tracks'].length} tracks...") if logger num_tracks = 0 whitelist = tracks.new.attributes.keys data["Tracks"].each do |track_id, row| if row["Kind"] !~ /audio/ logger.("[skipping non-audio file]") if logger next end # row already contains a hash of attributes almost ready to be passed to # ActiveRecord. We just need to modify the keys, e.g. change "Play Count" # to "play_count". row["Title"] = row.delete("Name") row["Play Date"] = row.delete("Play Date UTC") row["Original ID"] = row.delete("Track ID") attributes = row.inject({}) do |acc, (key, value)| attribute = key.gsub(" ", "").underscore acc[attribute] = value if whitelist.include? attribute acc end # change iTunes' URL-style locations into simple paths if attributes["location"] && attributes["location"] =~ /^file:\/\// attributes["location"].sub! /^file:\/\/localhost/, "" # CGI::unescape changes plus signs to spaces. This is a work around to # keep the plus signs. attributes["location"].gsub! "+", "%2B" attributes["location"] = CGI::unescape(attributes["location"]) end track = tracks.new(attributes) track = blk.call(track) if track && track.save num_tracks += 1 end end logger.("Imported #{num_tracks} tracks successfully.") if logger if tracks.new.attributes.keys.include? "original_id" logger.("Importing #{data['Playlists'].length} playlists...") if logger num_playlists = 0 data["Playlists"].each do |playlist_data| playlist = [] if ["Library", "Music", "Movies", "TV Shows", "iTunes DJ"].include? playlist_data["Name"] logger.("[skipping \"#{playlist_data['Name']}\" playlist]") if logger elsif playlist_data["Playlist Items"].nil? logger.("[skipping \"#{playlist_data['Name']}\" playlist (because it's empty)]") if logger else playlist_data["Playlist Items"].map(&:values).flatten.each do |original_id| playlist << tracks.where(original_id: original_id).first end playlist.compact! add_playlist(playlist_data["Name"], playlist) num_playlists += 1 end end logger.("Imported #{num_playlists} playlists successfully.") if logger else logger.("Can't import playlists because tracks table doesn't have an original_id field.") if logger end end end |
#playlists ⇒ Object
Returns a bare Relation of the Playlist model.
112 113 114 |
# File 'lib/lllibrary.rb', line 112 def playlists Lllibrary::Playlist.scoped end |
#select(&blk) ⇒ Object
Takes a block, and evaluates that block in the context of the Lllibrary’s DSL. Inside the block, every database field on the tracks table becomes a method called a selector. Each selector takes a value to match tracks against, and returns an Array of those tracks. String-like fields have string selectors, and number-like fields have numeric selectors. There is also an all selector, a none selector, and a playlist selector. See dsl.rb for a detailed description of these.
library.select do
composer(:rachmanino) # example of string selector
year(2010..2012) # example of numeric selector
total_time(gt: "6:00") # example of time selector
playlist(:energetic) # example of playlist selector
all # returns array of all tracks
none # returns empty array
end
102 103 104 |
# File 'lib/lllibrary.rb', line 102 def select(&blk) @dsl.instance_eval &blk end |