bin/ipod
#!/usr/bin/env ruby
Main {
version IpodDB::VERSION
author 'artm <femistofel@gmail.com>'
name 'ipod_db'
description <<-__
A couple of tools for working with iPod Shuffle (2nd gen) database from
command line. Each subcommand understands -h/--help flag.
__
option('books','b') {
argument_required
default 'books'
description 'subdirectory of ipod with bookmarkable media'
}
option('songs','s') {
argument_required
default 'songs'
description 'subdirectory of ipod with non-bookmarkable media'
}
option('version','v') {
description 'show package version and exit'
}
def run
if param['version'].given?
puts IpodDB::VERSION
else
help!
end
end
mode('sync') {
description <<-__
Update the iPod database. Given directories of bookmarkable and non-bookmarkable
media '#{program}' will find all supported tracks add them to the iPod database so
the device is aware of their existance.
The tracks under 'books' folder will get "riffled" - tracks from the same folder
are spread out in the list so they don't follow each other if possible. IN THE
FUTURE it is planned to allow configuring of track groups which are treated like
single folders - e.g. to spread out all SciAm's "60 second somthing" podcasts along
the playlist.
It is perfectly possible to have other directories full of tracks in device's
subconscious - e.g. when time-sharing the device among members of a poor
family. Just make sure you update the database using your directories when
receiving it from a relation.
iPod remembers playback position on bookmarkable media and the '#{program}' goes
out of its way to preserve the bookmarks. It also removes bookmarkable files
from shuffle list.
I configure gpodder to place podcast files inside IPOD/books directory and delete
them after syncing. Having copied podcasts I run 'ipod sync' to update the
database on the device and it's ready for consumption.
__
def run
load_ipod_db
sync
end
}
mode('ls') {
description 'produce a colorful listing of the tracks in the ipod database'
def run
load_ipod_db
ls
end
}
mode('rm') {
description <<-__
Remove tracks from the device by their numbers (that's why ls
displays numbers: so it's easier to select them for rm).
__
argument('track') {
arity -2
description <<-__
track numbers to delete from device (ranges like
2-5 are accepted too). As a special case, the word
'done' means all finished tracks, i.e. with play count
above zero or progress above
#{ (100*Ipod::Track::FinishedProgress).floor }%
__
}
option('pretend','n') {
description "don't delete, just list what would be deleted"
}
def run
@pretend = params['pretend'].value
load_ipod_db
rm
end
}
def sync
books_path = params['books'].value
songs_path = params['songs'].value
sanitize_filenames books_path
sanitize_filenames songs_path
books = collect_tracks books_path
books = spread(books)
songs = collect_tracks songs_path
@ipod_db.update books: books, songs: songs
@ipod_db.save
ls
end
def ls
@ipod_db.each_track_with_index do |track,i|
list_track i, wrapped_track(track)
end
end
def rm
tracks = parse_track_list(params['track'].values).map{|i,t| [i,t.filename]}
puts "The following tracks are selected for removal:"
tracks.each { |i,path| puts " %2d. %s" % [i,path.green] }
unless @pretend
if agree "Are you sure you want them gone (Y/n)?", true
FileUtils.rm tracks.map{|i,path|resolve_ipod_path path}
sync
end
end
end
def ipod_path path
'/' + Pathname.new(path).relative_path_from(Pathname.new @ipod_root).to_s
end
def resolve_ipod_path ipath
File.join @ipod_root, ipath
end
def load_ipod_db
@ipod_root = config['ipod_root']
unless IpodDB.looks_like_ipod? @ipod_root
fatal { "#{@ipod_root} does not appear to be a mounted ipod" }
exit exit_failure
end
@ipod_db = IpodDB.new @ipod_root
end
def collect_tracks path
tracks = []
Find.find(resolve_ipod_path path) do |filepath|
tracks << ipod_path(filepath) if track? filepath
end
tracks
rescue Errno::ENOENT
[]
end
def sanitize_filenames path
rename_us = []
Find.find(resolve_ipod_path path) do |filepath|
base_name = File.basename filepath
sane_name = IpodDB.sanitize_filename base_name
unless base_name == sane_name
dir = File.dirname filepath
rename_us << [ filepath, "#{dir}/#{sane_name}" ]
end
end
rename_us.reverse_each do |from,to|
if Dir.exists? to
info "moving #{from}/* to #{to}/"
FileUtils.mv Dir["#{from}/*"], to
FileUtils.rm_rf from
else
info "renaming #{from} to #{to}"
File.rename from, to
end
end
end
def track? path
IpodDB::ExtToFileType.include? File.extname(path)
end
def list_track i, track
listing_entry = "%2d: %s" % [i,track.filename.apply_format( color: track_color(track))]
listing_entry = listing_entry.bold if @ipod_db.playback_state.trackno == i
puts listing_entry
display_progress track
end
def track_color track
if track.exists? then
if track.finished?
:cyan
else
:green
end
else
:red
end
end
def display_progress track
if track.exists? and track.pos >= IgnoreProgressUnder and
track.progress.between?(IgnoreProgressUnder,Ipod::Track::FinishedProgress)
progress = ProgressBar.create(
format: " [%b #{Pretty.seconds(track.pos).yellow} %P%%%i] #{Pretty.seconds(track.length).yellow}",
starting_at: (100*track.progress),
)
puts
end
end
def parse_track_list args
tracks = Map.new
args.each do |arg|
case arg
when /^\d+$/
n = arg.to_i
tracks[n] = @ipod_db[n]
when /^(\d+)-(\d+)$/
(Regexp.last_match(1)..Regexp.last_match(2)).each do |n_str|
n = n_str.to_i
tracks[n] = @ipod_db[n]
end
when 'done'
@ipod_db.each_track_with_index do |track,n|
tracks[n] = track if wrapped_track(track).finished?
end
end
end
tracks
end
def wrapped_track record
@wrapped_tracks ||= Map.new
@wrapped_tracks[record[:filename]] ||= Ipod::Track.new( record.merge(ipod_root: @ipod_root) )
end
def spread paths, group_lambda = lambda {|path| track_group(path)}
bins = paths.group_by &group_lambda
bins.each do |key, bin|
bins[key] = spread(bins[key], lambda{|path| File.dirname(path)}) if key.is_a? Integer
end
Spread.spread *bins.values
end
def track_group track
config['group_together'].each_with_index do |patterns,group|
patterns.each do |pattern|
return group if track.downcase.index pattern.downcase
end
end
File.dirname(track)
end
IgnorePlaybackPosUnder = 10
IgnoreProgressUnder = 0.01
config({
ipod_root: "/media/#{ENV['USER']}/IPOD",
group_together: [
# Just an example
[ "60-Second", "Science Talk", "This Week in Science" ],
[ "StarShipSofa", "Tales To Terrify" ],
]})
}
BEGIN {
require 'main'
require 'find'
require 'pathname'
require 'smart_colored/extend'
require 'map'
require 'ruby-progressbar'
require 'highline/import'
require 'fileutils'
require 'pp'
# our own
require 'ipod_db'
require 'ipod/track'
require 'pretty'
require 'spread'
require 'fuzzy_locale'
I18n.locale = :fuzzy
}