#! /usr/bin/env ruby19
#
# (c) Erik Hvatum, 2009
#
require 'pathname'
require 'FileUtils'
require 'iTunesDB'
if ARGV.size != 2
abort "Usage: fix_iPod_filenames.rb <directory containing iPod_Control> <directory in which to place renamed music files>"
end
iPodRoot = Pathname.new(File.expand_path(ARGV[0]))
dbPath = iPodRoot + "iPod_Control/iTunes/iTunesDB"
if !dbPath.exist?
abort "iPod database \"#{dbPath}\" does not exist or is inaccessible."
end
destPath = Pathname.new(File.expand_path(ARGV[1]))
if !destPath.exist?
abort "Destination path \"#{destPath}\" does not exist or is inaccessible."
end
db = MiTunesDB::CiTunesDB.new
db.open(dbPath)
tracks = db.tracks
db.close
db = nil
tracks.sort! {|l, r| l.location <=> r.location}
tracks.each do |track|
needSep = false
dstFn = ""
doField = Proc.new do |field|
v = eval "track.#{field}"
if v != nil
if needSep
dstFn << "-"
else
needSep = true
end
dstFn << v.to_s
end
end
doField.call("album")
doField.call("discNumber")
doField.call("trackNumber")
doField.call("artist")
doField.call("title")
dstFn.gsub!(/[*!?\\\/:%]/, "_")
src = iPodRoot.to_s + track.location.gsub(/:/, "/")
dst = destPath.to_s + "/" + dstFn[0, 250] + "." + track.format.downcase.gsub(/ $/, "")
FileUtils.mv(src, dst)
end
# (c) Erik Hvatum, 2009
module MiTunesDB
# Reads the binary DB written by iTunes Windows to an iPod circa late 2008
class CiTunesDB
Track = Struct.new(:title,
:location,
:album,
:artist,
:genre,
:fileType,
:comment,
:composer,
:grouping,
:description,
:albumArtist,
:format,
:trackNumber,
:discNumber)
attr :mhbd
attr :file
def initialize
@mhbd = nil
@file = nil
end
# Opens the iTunesDB specified by filename, loading its contents into memory
def open(fileName)
@file = File.open(fileName, "rb")
@mhbd = Cmhbd.new(self)
end
# Closes the iTunesDB
def close()
@mhbd = nil
@file = nil
end
# Returns all known tracks as an array of Track structures (defined above)
def tracks()
ts = Array.new
if @mhbd
@mhbd.children.each do |mhsd|
if mhsd.children
mhsd.children.each do |sdChild|
if sdChild.is_a? Cmhlt
sdChild.children.each do |mhit|
t = Track.new
ts << t
mhit.children.each do |mhod|
if Cmhod::Types.has_key? mhod.type
sym = (Cmhod::Types[mhod.type].to_s + '=').to_sym
t.method(sym).call(mhod.str)
end
end
t.format = mhit.format
t.trackNumber = mhit.trackNumber
t.discNumber = mhit.discNumber
end
end
end
end
end
end
return ts
end
# Base class for objects in the iTunes DB
class Cmh_base
attr :addr
attr :depth
attr :len
attr :recordName
attr :children
def initialize(addr, db, depth)
@addr = addr
@depth = depth
@len = nil
@db = db
@children = nil
load()
end
def to_s()
indent = "\t" * @depth
return "#{indent}#{@recordName}:\n#{indent} len: #@len\n"
end
protected
def load(loadLen = true)
if @db.file.tell != @addr
@db.file.seek(@addr)
end
recordName = @db.file.read(@recordName.length())
if recordName != @recordName
raise "Invalid record identifier in DB: expected \"#@recordName\" but read \"#{recordName}\"."
end
@len = readUInt32() if loadLen
end
def loadChildren()
@children = []
end
def readUInt32()
return @db.file.read(4).unpack("V")[0]
end
def children_to_s()
ret = String.new
if @children
@children.each { |child| ret << child.to_s() }
end
return ret
end
end
# Record describing the database. This is the first record in the database and is located at the beginning
# of the database file.
class Cmhbd < Cmh_base
attr :dbVersion
attr :dbLen
def initialize(file)
@dbVersion = nil
@dbLen = nil
@recordName = "mhbd"
super(0, file, 0)
end
def to_s()
indent = "\t" * @depth
return super() << "#{indent} dbVersion: #@dbVersion\n#{indent} dbLen: #@dbLen\n" << children_to_s()
end
protected
def load()
super
seekTo = @addr + @recordName.length() + 4
if @db.file.tell != seekTo
@db.file.seek(seekTo)
end
@dbLen = readUInt32()
@dbVersion = Array.new(3) { readUInt32() }
loadChildren()
end
def loadChildren()
super
childAddr = @addr + @len
while childAddr < @dbLen
@children << Cmhsd.new(childAddr, @db, @depth + 1)
childAddr += @children[-1].childSize
end
end
end
# Record describing a dataset.
class Cmhsd < Cmh_base
# Adding childSize to @addr gives the addr of the next mhsd record
attr :childSize
attr :childType
def initialize(addr, db, depth)
@recordName = "mhsd"
@childSize = nil
@childType = nil
super
end
def to_s()
indent = "\t" * @depth
return super() << "#{indent} childSize: #@childSize\n#{indent} childType: #@childType\n" << children_to_s()
end
protected
def load()
super
seekTo = @addr + @recordName.length() + 4
if @db.file.tell != seekTo
@db.file.seek(seekTo)
end
@childSize = readUInt32()
@childType = readUInt32()
loadChildren()
end
def loadChildren()
super
if @childType == 1
@children << Cmhlt.new(@addr + @len, @db, @depth + 1)
end
end
end
# Record describing a track list
class Cmhlt < Cmh_base
attr :numChildren
def initialize(addr, db, depth)
@numChildren = nil
@recordName = "mhlt"
super
end
def to_s()
indent = "\t" * @depth
return super() << "#{indent} numChildren: #@numChildren\n" << children_to_s()
end
protected
def load()
super
seekTo = @addr + @recordName.length() + 4
@db.file.seek(seekTo)
@numChildren = readUInt32()
loadChildren()
end
def loadChildren()
super
addr = @addr + @len
@numChildren.times do
mhit = Cmhit.new(addr, @db, depth + 1)
@children << mhit
addr += mhit.totalLen
end
end
end
# Record describing a track item
class Cmhit < Cmh_base
attr :totalLen
attr :numStrMhods
attr :id
attr :format
attr :trackNumber
attr :discNumber
def initialize(addr, db, depth)
@totalLen = nil
@numStrMhods = nil
@id = nil
@format = nil
@recordName = "mhit"
@trackNumber = nil
@discNumber = nil
super
end
def to_s()
indent = "\t" * @depth
str = super
str << "#{indent} totalLen: #@totalLen\n"
str << "#{indent} numStrMhods: #@numStrMhods\n"
str << "#{indent} id: #@id\n"
str << "#{indent} format: #@format\n"
str << "#{indent} trackNumber: #@trackNumber\n"
str << "#{indent} discNumber: #@discNumber\n"
str << children_to_s()
return str
end
protected
def load()
super
seekTo = @addr + @recordName.length() + 4
@db.file.seek(seekTo)
@totalLen = readUInt32()
@numStrMhods = readUInt32()
@id = readUInt32()
@db.file.seek(4, IO::SEEK_CUR)
@format = @db.file.read(4).reverse()
@db.file.seek(@addr + 44)
trackNumber = readUInt32()
@trackNumber = trackNumber if trackNumber > 0
discNumber = readUInt32()
@discNumber = discNumber if discNumber > 0
loadChildren()
end
def loadChildren()
super
addr = @addr + @len
numStrMhods.times do
mhod = Cmhod.new(addr, @db, depth + 1)
children << mhod
addr += mhod.len
end
end
end
# Record describing a data object
class Cmhod < Cmh_base
Types = {1 => :title,
2 => :location,
3 => :album,
4 => :artist,
5 => :genre,
6 => :fileType,
8 => :comment,
12 => :composer,
13 => :grouping,
14 => :description,
22 => :albumArtist}.freeze
attr :headerLen
attr :type
attr :strLen
attr :str
def initialize(addr, db, depth)
@headerLen = nil
@type = nil
@str = nil
@strLen = nil
@recordName = "mhod"
super
end
def to_s()
indent = "\t" * @depth
str = super
str << "#{indent} headerLen: #@headerLen\n"
str << "#{indent} type: #@type\n"
str << "#{indent} strLen: #@strLen\n"
str << "#{indent} str: #@str\n" if @str
return str
end
protected
def load()
super(false)
@db.file.seek(@addr + @recordName.length())
@headerLen = readUInt32()
@len = readUInt32()
@type = readUInt32()
@db.file.seek(12, IO::SEEK_CUR)
@strLen = readUInt32()
if @strLen > 0 && @strLen % 2 == 0
@db.file.seek(8, IO::SEEK_CUR)
begin
str = @db.file.read(@strLen).force_encoding("UTF-16LE").encode("US-ASCII")
rescue
else
@str = str
end
end
end
end
end
end
Labels: coding, iPod, iTunesDB, ruby
#! /usr/bin/env ruby
#
# (c) Erik Hvatum 2009
#
require 'rubygems'
require 'optparse'
require 'open4'
#* Parse command line options
options = {}
optparser = OptionParser.new
optparser.banner = "Usage: #{$0} [options]"
options[:dryrun] = false
optparser.on('-d', '--dry', 'Dry run; print the calls to gem that would be executed') do
options[:dryrun] = true
end
options[:gemexec] = 'gem'
optparser.on('-g', '--gem EXECUTABLE', 'Specify gem executable name; ie, gem19 or /usr/local/bin/gem19') do |gem|
options[:gemexec] = gem
end
optparser.on('-h', '--help', 'Display help information') do
puts optparser
exit
end
options[:verbose] = false
optparser.on('-v', '--verbose', 'Output more information') do
options[:verbose] = true
end
begin
optparser.parse! ARGV
rescue OptionParser::ParseError => e
abort e
end
if ARGV.size > 0
abort "invalid option: #{ARGV[0]}"
end
#* Make list of outdated gems to remove
GemInfo = Struct.new(:nameAndVersion, :name, :version, :path)
gemsToUninstall = []
gemCleanDryCmd = "#{options[:gemexec]} cleanup -d"
if options[:verbose]
puts "To determine which gems are outdated and should be removed, executing shell command: #{gemCleanDryCmd}"
end
pid, stdin, stdout, stderr = Open4::popen4(gemCleanDryCmd)
ignored, status = Process::waitpid2(pid)
if status != 0
abort "Error: #{gemCleanDryCmd} returned #{status} instead of zero, with stdout output\n#{stdout.read.strip}\nand stderr output\n#{stderr.read.strip}"
end
l = stdout.readline()
unless l =~ /^\s*Cleaning up installed gems\.\.\.\s*$/i
abort "Error: First line of #{gemCleanDryCmd} stdout is not \"Cleaning up installed gems...\""
end
while 1
l = stdout.readline()
if l == nil
abort "Error: Premature end of output from #{gemCleanDryCmd}"
end
if l =~ /^\s*Clean Up Complete\s*$/i
break
end
if l =~ /^\s*Dry Run Mode: Would uninstall (\S+)\s*$/i
if options[:verbose]
puts "Found obsolete gem to uninstall: #{$1}"
end
gemsToUninstall << GemInfo.new($1)
else
abort "Error: Failed to parse output from #{gemCleanDryCmd}: #{l}"
end
end
for gem in gemsToUninstall
if gem.nameAndVersion =~ /^(.*)-((?:\d+\.?)+)$/
gem.name, gem.version = $1, $2
else
abort "Error: Failed to parse gem name: #{gem.nameAndVersion}"
end
end
#* Get paths of identified gems
for gem in gemsToUninstall
cmd = "#{options[:gemexec]} list -d -l #{gem.name}"
if options[:verbose]
puts cmd
end
pid, stdin, stdout, stderr = Open4::popen4(cmd)
ignored, status = Process::waitpid2(pid)
if status != 0
abort "Error: #{cmd} returned #{status} instead of zero, with stdout output\n#{stdout.read.strip}\nand stderr output\n#{stderr.read.strip}"
end
o = stdout.read()
unless o =~ /Installed at /
abort "Error: failed to parse output of #{cmd}"
end
while $' =~ /\s*\(((?:\d+\.?)+)\): (.*)$/
if $1 == gem.version
gem.path = $2
break
end
end
end
#* Perform removal
for gem in gemsToUninstall
cmd = "#{options[:gemexec]} uninstall -I --no-executables
--install-dir #{gem.path} --version \"=#{gem.version}\" #{gem.name}"
if options[:dryrun] || options[:verbose]
puts cmd
end
unless options[:dryrun]
system(cmd)
end
end
July 2009 August 2009 September 2009 October 2009 November 2009 December 2009 January 2010 September 2010 December 2010 January 2011 February 2011 April 2011 June 2011 August 2011 February 2012 June 2012 July 2012 August 2012 October 2012 November 2012 January 2014 April 2014 June 2014 August 2014 September 2014 October 2014 January 2015 March 2015 April 2015 June 2015 November 2015 December 2015 January 2016 June 2016 August 2016 January 2017 March 2017 April 2018 April 2019 June 2019 January 2020
Subscribe to Posts [Atom]