#!/usr/bin/python
# -*- coding: latin1 -*-
# Copyright Thomas Backman, 2007
# Written by Thomas Backman (serenity@exscape.org), 2010-07-27 - 2007-08-03, 2010-08-01, more earlier dates
# Originally written for Python 2.4.4, tested on 2.6.5

# Legal stuff:
# This script is licensed under a New BSD license. Basically, do whatever you want, except modify it and then take credit for writing it (though you MAY take credit for your modifications!). Nor may you remove any copyright markings or such.

import os.path, sys, re, getopt
from commands import getstatusoutput as do, getoutput
from threading import _Timer

version = '0.18'

class _RepeatingTimer(_Timer):
	""" Reusable timer class """
	def __init__(self, interval, function, args=[], kwargs={}):
		_Timer.__init__(self, interval, function, args, kwargs)

	def run(self):
		while not self.finished.isSet():
			self._run()

	def _run(self):
		if not self.finished.isSet():
			self.finished.wait(self.interval)
			self.function(*self.args, **self.kwargs)

def RepeatingTimer(*args, **kwargs):
	""" Creates a _RepeatingTimer instance """
	return _RepeatingTimer(*args, **kwargs)

class Spinner(object):
	""" Represents a spinning icon used to indicate progress.

	Keyword arguments:
	str --	the string to display, eg. "Doing something..."
	init --	(True/False) - whether to start automatically or not. Used to create a "dummy" spinner. Defaults to True.
	Usage example:
	spinner = Spinner("Doing a long-winded operation...")
	<do the operation>
	spinner.stop()"""
	# Slightly borrowed from Gentoo's emerge (5 lines or so stolen, sorry!)
	twirl_sequence = "/-\\|/-\\|/-\\|/-\\|\\-/|\\-/|\\-/|\\-/|"

	def __init__(self, s="", init=True):
		self.spinpos = 0
		self.running = False
		self.msg = s
		if init:
			self.start()

	def start(self):
		""" Start spinning! \o/ """
		print self.msg,

		try:
			def update():
				try:
					self.spinpos = (self.spinpos + 1) % len(self.twirl_sequence)
					sys.stdout.write("\b\b " + self.twirl_sequence[self.spinpos])
					sys.stdout.flush()
				except:
					pass

			t = RepeatingTimer(0.1, update)
			t.setDaemon(True)
			self._timer = t
			print '  ',
			self.running = True
			t.start()

		# Ugly remedy to a python bug. Sometimes when exiting,
		# the thread will keep running when the python interpreter is shutting down
		# causing ugly errors that we don't want to show to the user.
		except Exception, ex:
			if ((isinstance(ex, AttributeError) or isinstance(ex, TypeError)) and "NoneType" in ex[0]):
				pass
			else:
				raise

	def setmessage(self, str):
		""" Sets the message this spinner will print while working """
		self.msg = str

	def stop(self, interrupted=False):
		""" Stop spinning!
		Keyword arguments:
		interrupted -- optional, True/False -- if True, the Spinner() object will print 'Interrupted!' rather than 'Done!' when stopping. Defaults to False."""
		if hasattr(self, "_timer"):
			self._timer.cancel()
			self._timer.join()
			del self._timer
		self.running = False
		if not interrupted:
			print '\b\b\b Done!'
		else:
			print '\b\b\b Interrupted!'

# More than slighty borrowed/stolen from Gentoo's emerge - please mail before sueing! ;)
try:
	import signal

	def exithandler(signum,frame):
		global spinner
		signal.signal(signal.SIGINT, signal.SIG_IGN)
		signal.signal(signal.SIGTERM, signal.SIG_IGN)
		if spinner.running:
			spinner.stop(True)
		sys.exit(1)
	
	signal.signal(signal.SIGINT, exithandler)
	signal.signal(signal.SIGTERM, exithandler)
	signal.signal(signal.SIGPIPE, signal.SIG_DFL)

except KeyboardInterrupt:
	global spinner
	if spinner.running:
		spinner.stop(True)
	sys.exit(1)

def panic(str):
	""" Prints a message and quits """
	global spinner
	if spinner.running:
		spinner.stop(True)
	print >> sys.stderr, str
	sys.exit(1)

def escape(str):
	""" Escapes a path to make sure that everything works out with the shell etc """
	def rep(text):
		dic = {
		'\\': '\\\\',
		' ': '\\ ',
		':': '\\:',
		'(': '\\(',
		"'": "\\'",
		')': '\\)',
		'"': '\\"',
		}
		rc = re.compile('|'.join(map(re.escape, dic)))
		def translate(match):
			return dic[match.group(0)]
		return rc.sub(translate, text)
	return rep(str)

def getdir(path):
	""" Returns a pretty representation of a path/directory.

	Example:
	/home/serenity/stuff/Blah-TEST/CD1/blah.rar -> Blah-TEST/CD1
	Paths given should NOT be escaped"""

	dir = os.path.dirname(path).split('/')[-1]
	if re.match('^(CD|Disc|DVD|Subs|Cover)', dir, re.I):
		# If this matched, the last dir was a CD, lets use the second last dir too (eg. dir1/CD1)
		dir = "/".join(os.path.dirname(path).split('/')[-2:])
	return dir

def unpack(source, destination, cur, count, create_dir=False):
	""" Does the actual work. Expects four or five arguments:
	source		-- the file (full path to a .rar) to unpack
	destination	-- the directory (again full path) where to save the unpacked file(s)
	cur		-- the current file we're unpacking, to show progress
	count		-- total number of files to unpack
	create_dir  -- create a directory with the release name to unpack files in
	The paths given should NOT be escaped."""

	global spinner

	# Whip up a nice string to display to the user
	# First, try the last directory of the path...
	dir = getdir(source)

# If speficied, create dir and unpack there
	if create_dir:
		if (destination[-1] != "/"):
			destination += "/"
		destination += dir
		os.makedirs(destination)
		
	source = escape(source)
	destination = escape(destination)

	if count == 1:
		spinner = Spinner('Unpacking %s...' % dir)
	else:
		spinner = Spinner('Unpacking %s (%d/%d)...' % (dir, cur, count))

	cmd = "unrar x -o- %s %s" % (source, destination)
#	cmd = "unrar lt %s %s" % (source, destination) # Debug line
	result = do(cmd)
	status = result[0]
	#print "COMMAND:", cmd
	#print result[1]
	spinner.stop()

	if status != 0:
		print >> sys.stderr, 'WARNING: %s did not unpack cleanly' % source
		return False
	
	return True

def find(path, type, maxdepth=0):
	""" Calls find(1) and returns a list of files/directories

	Keyword arguments:
	path --	the path to search
	type --	"dir" or "rar", depending on what to search for
	maxdepth -- (optional) only search to this depth. 0 for any depth.
			
	The paths given should NOT be escaped!"""
	global spinner

	path = escape(path)

	if type == 'dirs':
		if maxdepth == 0:
			results = getoutput('find %s -type d' % path).split('\n')
		else:
			results = getoutput('find %s -maxdepth %d -type d' % (path, maxdepth)).split('\n')
		results = filter(lambda x: not x.startswith('find:'), results)
		return results

	elif type == 'rars':
		if maxdepth == 0:
			results = getoutput('find %s -type f -name "*.rar" | sort | head -n 1' % path).split('\n')
		else:
			results = getoutput('find %s -maxdepth %d -type f -name "*.rar" | sort | head -n 1' % (path, maxdepth)).split('\n')
		results = filter(lambda x: not (x.startswith('find:') or x == ''), results)
		return results
	else:
		panic('Invalid argument given to find()! Exiting.')
	

def main():
	global spinner

	# Init spinner, in case user presses ^C and the interrupt handler
	# tries to access spinner.running
	spinner = Spinner(init=False)

	def printhelp():
		panic("""Usage: %s [-rpc] <to unpack 1> [to unpack 2] ... <destination path>
Options:
\t-r (or -R)\tRecursive - unpack everything found in all subdirectories of the given path - use with care, searching is slow!
\t-p (or -P)\tPretend - only show what would be unpacked, but don't actually do anything
\t-c (or -C)\tCreate parent directories - unpack to <destination>/<release_name>, i.e. everything gets its own subdirectory

Limitations: This utility only unpacks RAR files, and only the first (by name) found in a single directory.
This is by design, though.

Version: %s""" % (sys.argv[0], version))

	try:
		opts, in_paths = getopt.getopt(sys.argv[1:], 'rpRPcC')
	except getopt.GetoptError:
		printhelp()

	if len(in_paths) < 2:
		printhelp()
			
	errors = 0 # Number of given paths that didn't unpack (they might have unpacked partially)
	successfully_unpacked = 0 # Number of given paths that unpacked successfully
	to_unpack = [] # A list of the directories given to unpack
	destination = "" # The directory we're unpacking to
	recursive = False # Will be set to True if the -r or -R option was given
	pretend = False # Will be set to True if the -p or -P option was given
	create_dir = False # Will be set to True of the -c or -C option was given

	for o, a in opts:
		if o in ('-p', '-P'):
			pretend = True
		if o in ('-r', '-R'):
			recursive = True
		if o in ('-c', '-C'):
			create_dir = True

	for path in in_paths:
		if not os.path.exists(path):
			# We need all paths to exist
			panic("ERROR: Path not found: %s\nExiting..." % path)
		elif os.path.abspath(path) not in to_unpack:
			to_unpack.append(os.path.abspath(path))

	# The last path entered is the destination
	destination = os.path.abspath(in_paths[-1])

	if len(to_unpack) != 1:
		to_unpack.remove(destination)

	if recursive:
		# User specified the -R option
		found_rars = [] # Used later

		spinner = Spinner('Scanning for directories...')
		found_dirs = []
		for path in to_unpack:
			# Initial scan... Finds all directories below the given path(s)
			dirs = find(path, type="dirs")
			found_dirs.extend(dirs)

		spinner.stop()

		if len(found_dirs) == 0:
			# This probably never happens, since the paths specified
			# will likely be part of the results, but still.
			panic('Nothing found, whatsoever!')

		spinner = Spinner('Scanning for RARs in the directories found...')

		for dir in found_dirs:
			# Step two: Check for RAR files inside the directories, only one level in
			results = find(dir, type="rars", maxdepth=1)
			found_rars.extend(results)

		spinner.stop()

		if len(found_rars) == 0:
			panic('!!! Found nothing to unpack! Exiting.')

	else:
		# User did NOT specify the -R option
		found_rars = [] # Used later

		spinner = Spinner("Looking for stuff to unpack...")
		for path in to_unpack:
			# First, lets look for .rar files inside the directory specified
			results = find(path, type="rars", maxdepth=1)
			if len(results) == 0:
				# Nothing found... Keep trying though, this time we'll look for directories,
				# in case there are dirs like "CD1" or "DVD1" inside
				results = find(path, type="dirs", maxdepth=1)
				if len(results) == 0:
					# Still nothing? OK, give up...
					print >> sys.stderr, 'Warning: Nothing found to unpack in %s' % path
					continue

				# OK, if we get here we found something. Lets filter it out a bit...
				results = filter(lambda x: re.search('\/(Disc|CD|DVD)\d+\/?$', x, re.I), results)

				# We now have the paths to the CDs/whatever, lets find the RARs and add them to the
				# to-unpack list
				for cd in results:
					rars = find(cd, type="rars", maxdepth=1)
					found_rars.extend(rars)

			else:
				# We found a RAR (no multiple CDs), add it to the list
				found_rars.extend(results)

		spinner.stop()
	
	# Unpack what we found
	# This code is the same for both recursive and non-resursive operations
	found_rars.sort()

	if not pretend:
		cur = 0
		for rar in found_rars:
			cur += 1 # Keep track so that we can tell unpack() the progress
			if not unpack(rar, destination, cur, len(found_rars), create_dir):
				errors += 1
				print >> sys.stderr, '!!! %s failed to unpack!' % (getdir(rar)) 
			else:
				successfully_unpacked += 1
	else:
		# User specified -p option, only show what we'd unpack
		if len(found_rars) == 0:
			panic('Found nothing to unpack!')

		print 'OK, this is what I would unpack, would you not have used the pretend option:'
		for rar in found_rars:
			print '\t+ %s' % getdir(rar) 
		print 'I would unpack them to:'
		print '\t- %s' % destination
		if create_dir:
			print '\t\tNOTE: the -c option was used; this pretend output is inaccurate as it doesn\'t factor that in.'
		panic('')
	
	# All done! Lets print out what happened before exiting.

	if not errors and successfully_unpacked > 0: # No errors, stuff unpacked
		print 'All OK!'
		sys.exit(0)
	elif errors and successfully_unpacked > 0: # Errors, some stuff unpacked
		print >> sys.stderr, '!!! Done, but some files did not unpack correctly'
		sys.exit(1)
	elif errors and successfully_unpacked == 0: # Errors, nothing unpacked
		print >> sys.stderr, '!!! Nothing unpacked successfully.'
		sys.exit(2)
	else: # No errors, nothing unpacked = nothing found
		print >> sys.stderr, '!!! Nothing found to unpack!'
		sys.exit(4)

if __name__=='__main__':
	main()
