#!/usr/bin/env python
# -*- coding: iso-8859-1

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

__title__="KeyJ's iPod shuffle Database Builder"
__version__="1.0-rc1"
__author__="Martin Fiedler"
__email__="martin.fiedler@gmx.net"

""" VERSION HISTORY
1.0-rc1 (2006-04-26)
    * finally(!) added the often-requested auto-rename feature (-r option)
0.7-pre1 (2005-06-09)
    * 0.6-final was skipped because of the huge load of new (experimental)
      features in this version :)
    * rule files allow for nice and flexible fine tuning
    * fixed --nochdir and --nosmart bugs
    * numerical sorting (i.e. "track2.mp3" < "track10.mp3")
    * iTunesSD entries are now copied over from the old file if possible; this
      should preserve the keys for .aa files
0.6-pre2 (2005-05-02)
    * fixed file type bug (thanks to Nowhereman)
    * improved audio book support (thanks to Nowhereman)
    * files called $foo.book.$type (like example.book.mp3) are stored as
      audio books now
    * the subdirectories of /iPod_Control/Music are merged as if they were a
      single directory
0.6-pre1 (2005-04-23)
    * always starts from the directory of the executable now
    * output logging
    * generating iTunesPState and iTunesStats so that the iPod won't overwrite
      iTunesShuffle anymore
    * -> return of smart shuffle ;)
    * directory display order is identical to playback ordernow
    * command line options and help
    * interactive mode, configurable playback volume, directory limitation
0.5 (2005-04-15)
    * major code refactoring (thanks to Andre Kloss)
    * removed "smart shuffle" again -- the iPod deleted the file anyway :(
    * common errors are now reported more concisely
    * dot files (e.g. ".hidden_file") are now ignored while browsing the iPod
0.4 (2005-03-20)
    * fixed iPod crashes after playing the shuffle playlist to the end
    * fixed incorrect databse entries for non-MP3 files
0.3 (2005-03-18)
    * Python version now includes a "smart shuffle" feature
0.2 (2005-03-15)
    * added Python version
0.1 (2005-03-13)
    * initial public release, Win32 only
"""


import sys,os,os.path,array,getopt,random,types,fnmatch,operator,string,eyed3

KnownProps=('filename','size','ignore','type','shuffle','reuse','bookmark')
Rules=[
  ([('filename','~','*.mp3')],          {'type':1, 'shuffle':1, 'bookmark':0}),
  ([('filename','~','*.m4?')],          {'type':2, 'shuffle':1, 'bookmark':0}),
  ([('filename','~','*.m4b')],          {          'shuffle':0, 'bookmark':1}),
  ([('filename','~','*.aa')],           {'type':1, 'shuffle':0, 'bookmark':1, 'reuse':1}),
  ([('filename','~','*.wav')],          {'type':4, 'shuffle':0, 'bookmark':0}),
  ([('filename','~','*.book.???')],     {          'shuffle':0, 'bookmark':1}),
  ([('filename','~','*.announce.???')], {          'shuffle':0, 'bookmark':0}),
  ([('filename','~','/recycled/*')],    {'ignore':1}),
]

Options={
  "volume":None,
  "interactive":False,
  "smart":True,
  "home":True,
  "logging":True,
  "reuse":1,
  "logfile":"rebuild_db.log.txt",
  "rename":False
}
domains=[]
total_count=0
KnownEntries={}


################################################################################


def open_log():
  global logfile
  if Options['logging']:
    try:
      logfile=file(Options['logfile'],"w")
    except IOError:
      logfile=None
  else:
   logfile=None


def log(line="",newline=True):
  global logfile
  if newline:
    print line
    line+="\n"
  else:
    print line,
    line+=" "
  if logfile:
    try:
      logfile.write(line)
    except IOError:
      pass


def close_log():
  global logfile
  if logfile:
    logfile.close()


def go_home():
  if Options['home']:
    try:
      os.chdir(os.path.split(sys.argv[0])[0])
    except OSError:
      pass


def filesize(filename):
  try:
    return os.stat(filename)[6]
  except OSError:
    return None


################################################################################


def MatchRule(props,rule):
  try:
    prop,op,ref=props[rule[0]],rule[1],rule[2]
  except KeyError:
    return False
  if rule[1]=='~':
    return fnmatch.fnmatchcase(prop.lower(),ref.lower())
  elif rule[1]=='=':
    return cmp(prop,ref)==0
  elif rule[1]=='>':
    return cmp(prop,ref)>0
  elif rule[1]=='<':
    return cmp(prop,ref)<0
  else:
    return False


def ParseValue(val):
  if len(val)>=2 and ((val[0]=="'" and val[-1]=="'") or (val[0]=='"' and val[-1]=='"')):
    return val[1:-1]
  try:
    return int(val)
  except ValueError:
    return val

def ParseRule(rule):
  sep_pos=min([rule.find(sep) for sep in "~=<>" if rule.find(sep)>0])
  prop=rule[:sep_pos].strip()
  if not prop in KnownProps:
    log("WARNING: unknown property `%s'"%prop)
  return (prop,rule[sep_pos],ParseValue(rule[sep_pos+1:].strip()))

def ParseAction(action):
  prop,value=map(string.strip,action.split('=',1))
  if not prop in KnownProps:
    log("WARNING: unknown property `%s'"%prop)
  return (prop,ParseValue(value))

def ParseRuleLine(line):
  line=line.strip()
  if not(line) or line[0]=="#":
    return None
  try:
    # split line into "ruleset: action"
    tmp=line.split(":")
    ruleset=map(string.strip,":".join(tmp[:-1]).split(","))
    actions=dict(map(ParseAction,tmp[-1].split(",")))
    if len(ruleset)==1 and not(ruleset[0]):
      return ([],actions)
    else:
      return (map(ParseRule,ruleset),actions)
  except OSError: #(ValueError,IndexError,KeyError):
    log("WARNING: rule `%s' is malformed, ignoring"%line)
    return None
  return None


################################################################################


def safe_char(c):
  if c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_":
    return c
  return "_"

def rename_safely(path,name):
  base,ext=os.path.splitext(name)
  newname=''.join(map(safe_char,base))
  if name==newname+ext:
    return name
  if os.path.exists("%s/%s%s"%(path,newname,ext)):
    i=0
    while os.path.exists("%s/%s_%d%s"%(path,newname,i,ext)):
      i+=1
    newname+="_%d"%i
  newname+=ext
  try:
    os.rename("%s/%s"%(path,name),"%s/%s"%(path,newname))
  except OSError:
    pass  # don't fail if the rename didn't work
  return newname


def write_to_db(filename):
  global iTunesSD,domains,total_count,KnownEntries,Rules

  # set default properties
  props={
    'filename': filename,
    'size': filesize(filename[1:]),
    'ignore': 0,
    'type': 1,
    'shuffle': 1,

    'reuse': Options['reuse'],
    'bookmark': 0
  }

  # check and apply rules
  for ruleset,action in Rules:
    if reduce(operator.__and__,[MatchRule(props,rule) for rule in ru
leset],True):
      props.update(action)
  if props['ignore']: return 0

  # retrieve entry from known entries or rebuild it
  entry=props['reuse'] and (filename in KnownEntries) and KnownEntries[filename]
  if not entry:
    header[29]=props['type']
    entry=header.tostring()+ \
      "".join([c+"\0" for c in filename[:261]])+ \
      "\0"*(525-2*len(filename))

  # write entry, modifying shuffleflag and bookmarkflag at least
  log("=> %s"%(filename))
  iTunesSD.write(entry[:555]+chr(props['shuffle'])+chr(props['bookmark'])+entry[557])
  if props['shuffle']: domains[-1].append(total_count)
  total_count+=1
  return 1


def make_key(s):
  if not s: return s
  s=s.lower()
  for i in xrange(len(s)):
    if s[i].isdigit(): break
  if not s[i].isdigit(): return s
  for j in xrange(i,len(s)):
    if not s[j].isdigit(): break
  if s[j].isdigit(): j+=1
  return (s[:i],int(s[i:j]),make_key(s[j:]))

def key_repr(x):
  if type(