Skip to content

Instantly share code, notes, and snippets.

@mschuerig
Created April 20, 2013 08:40
Show Gist options
  • Save mschuerig/5425277 to your computer and use it in GitHub Desktop.
Save mschuerig/5425277 to your computer and use it in GitHub Desktop.
Generate yearly and year-monthly playlists based on when albums were added to the collection. The heuristic used to find the adding date is to take the date of the oldest file in the album's directory. No CLI yet, use at your own risk.
#! /usr/bin/ruby1.9.3
require 'taglib'
require 'debugger'
# TODO
# - CLI
# - -m, --monthly
# - -u, --update
# - -q, --quiet
# - -v, --verbose
# - 1999,2002-2005,2008
# - --current (update current year and month)
$quiet = false
$verbose = true
$update = true
class BottomUpDirectories
include Enumerable
def self.find(*dirs, &block)
if block
new(*dirs).each(&block)
else
new(*dirs)
end
end
def initialize(*dirs)
options = dirs.last.is_a?(Hash) ? dirs.pop : {}
@dirs = dirs
@excludes = Array(options[:exclude])
@leaves_only = options[:leaves_only]
end
def each(&block)
@dirs.each do |d|
root_re = %r{^#{d}/}
each_dir([d], root_re, &block)
end
end
private
MATCH_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH
def each_dir(dirs, root_re, &block)
dirs.each do |dir|
begin
children = []
Dir.open(dir) do |d|
d.each do |it|
next if it == '.' || it == '..'
path = File.join(dir, it)
next unless File.exist?(path) && File.lstat(path).directory?
unless @excludes.empty?
match_path = path.sub(root_re, '')
next if @excludes.any? { |excl| File.fnmatch(excl, match_path, MATCH_FLAGS) }
end
children << path
end
end
each_dir(children, root_re, &block)
block[dir] unless @leaves_only && !children.empty?
rescue Errno::ENOENT, Errno::EACCES
### really?
end
end
end
end
class Album
include Enumerable
def initialize(dir)
@dir = dir
end
def each(&block)
tracks.each(&block)
end
def tracks
track_files.map { |tf| Track.new(File.join(@dir, tf)) }.sort_by(&:order)
end
private
def track_files
Dir.open(@dir) do |d|
d.select do |f|
f =~ /\A[^.].*\.(:?aac|flac|mp3|ogg)\z/
end
end
end
end
class Track
attr_reader :file
def initialize(file)
@file = file
end
def order
if File.basename(@file) =~ /^\d/
@file
else
TagLib::FileRef.open(@file) { |ref|
ref.tag && ref.tag.track
} || @file
@file
end
end
def to_s
@file.sub(%r{\./}, '')
end
end
class Playlist
attr_reader :root_dir, :playlist_file
def initialize(name, root_dir = nil)
@root_dir = root_dir || '.'
@playlist_file = File.join(@root_dir, 'yearly', name)
end
def self.open(name, root_dir = nil, &block)
new(name, root_dir).open(&block)
end
def exist?
File.exist?(playlist_file)
end
def open
with_lazy_stream do
yield self
end
end
def write_album(album)
with_stream do |stream|
stream.puts "# #{album}"
Album.new(album_dir(album)).tap do |a|
a.each do |track|
stream.puts track
end
end
end
end
private
def album_dir(album)
File.join(@root_dir, album)
end
def with_lazy_stream
yield
ensure
close_stream
end
def with_stream
@stream ||= File.open(playlist_file, "w")
yield(@stream)
end
def close_stream
@stream.close if @stream
@steam = nil
end
end
class Grouper
def initialize(*parts)
@parts = parts
end
def group(date)
@parts.map { |p| date.public_send(p) }
end
def playlist(group)
"#{group.join('-')}.m3u"
end
end
BY_YEAR = Grouper.new(:year)
BY_MONTH = Grouper.new(:year, :month)
@grouper = BY_YEAR
def albums_and_times
BottomUpDirectories.find('.', leaves_only: true).map do |d|
Dir.open(d) do |dir|
oldest = dir.map do |e|
next if e == '..'
File.mtime(File.join(dir, e))
end.compact.sort.first
d.sub!(%r{\./}, '')
[d, oldest] if oldest
end
end.compact
end
grouped_albums = albums_and_times.group_by do |album, time|
@grouper.group(time)
end
grouped_albums.sort_by(&:first).each do |group, albums|
puts "*** #{group} (#{albums.size})" unless $quiet
Playlist.open(@grouper.playlist(group)) do |playlist|
break if playlist.exist? && !$update
albums.sort_by(&:last).each do |album, _|
puts album if $verbose
playlist.write_album(album)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment