cleanthumbs.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = "Markus Stumpf <mailto:srv-src-cleanthumbs@maexotic.de>"
__source__ = "http://software.maexotic.de/python/cleanthumbs/>"
__version = "1.0"
__license__ = """Copyright (c) 2010 Markus Stumpf. All rights reserved.
See http://software.maexotic.de/python/cleanthumbs/license.txt for
the full text of the license."""

import os
import sys
import getopt
import Image
from urlparse import urlparse
from urllib import unquote

# known software that creates the thumbnails - currently unused
KNOWN_SOFTWARE = ( 'GNOME::ThumbnailFactory', 'GIMP 2.6.8' )

# directories with thumbnails to check
dirs_to_check = (
    ".thumbnails",
    )

# default controls
term_color = {
    "bold"		: "\033[1m",
    "norm"		: "\033[m",
    }

# options with defaults
opts = {
    "always-remove"	: None,
    "dry-run"		: False,
    "stderr-terminal"	: True,
    "keep-nonlocal"	: False,
    "verbose"		: 1,
    }

# holds the number of thumbnails that where chacked / deleted
thumbs_checked = 0
thumbs_removed = 0

# ------------------------------------------------------------------------
def help():
    hmsg = """
options are:
        --always-remove=PATH	always remove thumbnails of local images
				whose location starts with PATH. Be sure
				to add a trailing "/" if you want a directory.
				May be specified more than once.
				    Eg. --always-remove=/media/cdrom/
    -h, --help			display this message
    -k, --keep-nonlocal		don't delete thumbnails that are not
				associated with local files (eg. http://..)
    -q, --quiet			suppress non-error messages
    -n, --dry-run		trial run, don't delete any thumbnails.
				Most useful with increased verbosity
    -v, --verbose		increase verbosity. May be specified more
				than once (more than 2 currently useless).
"""
    sys.stdout.write(hmsg)
    sys.stdout.flush()
    sys.exit(0)

# ------------------------------------------------------------------------
def warn(m):
    """write messages to stderr eventually highlighting them"""

    if opts['stderr-terminal']:
	sys.stderr.write(term_color["bold"])
    sys.stderr.write(m.rstrip("\r\n"))
    if opts['stderr-terminal']:
	sys.stderr.write(term_color["norm"])
    sys.stderr.write("\n")
    sys.stderr.flush()
    return

# ------------------------------------------------------------------------
def termsetup():
    """try to setup enhanced output mode for highlighting messages"""
    # if stdout and stderr are both to a terminal use
    # terminal control codes to make warnings bold

    try:
	import curses
    except Exception, e:
	opts["stderr-terminal"] = False
	if 1 < opts["verbose"]:
	    warn("Warning: %s: disabling enhanced terminal support" % e)
	return

    # got curses, now try to setup the terminal and get control codes
    try:
	curses.setupterm()
	term_color["bold"] = curses.tigetstr("bold")
	term_color["norm"] = curses.tigetstr("sgr0")
    except Exception, e:
	opts["stderr-terminal"] = False
	if 1 < opts["verbose"]:
	    warn("Warning: %s: disabling enhanced terminal support" % e)
	warn("Warning: %s: disabling enhanced terminal support" % e)
        return

    # check if stderr is to a terminal
    # if it is not, don't use codes
    try:
	os.ttyname(file.fileno(sys.stderr))
	opts["stderr-terminal"] = True
    except OSError:
	opts["stderr-terminal"] = False
	return

    # ok, stderr is to a terminal
    # now check if stdout is also to a terminal
    # if stdout is different from stderr, don't use codes
    try:
	os.ttyname(file.fileno(sys.stdout))
    except OSError:
	opts["stderr-terminal"] = False

    return

# ------------------------------------------------------------------------
def parse_options():
    longopts = [
	"always-remove=",
	"dry-run",
	"help",
	"keep-nonlocal",
	"quiet",
	"verbose="
	]
    try:
        gopts, cmds = getopt.getopt(sys.argv[1:], 'hknqv', longopts)
    except getopt.GetoptError, m:
	sys.stderr.write("Error: %s\n" % m)
	sys.stderr.flush()
	sys.exit(1)

    if cmds:
        sys.stderr.write("Error: extraneous parameter(s): %s\n" %
		(", ".join(cmds)))
	sys.stderr.flush()
        sys.exit(1)

    for o, a in gopts:
	if   o == "--always-remove":
	    if None is opts["always-remove"]:
		opts["always-remove"] = [ a ]
	    else: opts["always-remove"].append(a)
	elif o == "-h" or o == "--help":
	    help()
	elif o == "-k" or o == "--keep-nonlocal":
	    opts["keep-nonlocal"] = True
	elif o == "-n" or o == "--dry-run":
	    opts["dry-run"] = True
	elif o == "-q" or o == "--quiet":
	    opts["verbose"] = 0
	elif o == "-v":
	    opts["verbose"] += 1
	elif o == "--verbose":
	    opts["verbose"] = int(a)
	else:
	    sys.stderr.write("Internal error: cannot handle option '%s'\n" % o)
	    sys.stderr.flush()
	    sys.exit(1)
    return

# ------------------------------------------------------------------------
def have_imagefile(uri):
    """parse the URI found in the PNG file and check if image exists"""
    (scheme, netloc, path, params, query, fragment) = urlparse(unquote(uri))

    # check if the local image still exists
    if "file" == scheme:
	if opts["always-remove"]:
	    for p in opts["always-remove"]:
		if path.startswith(p):
		    return(False)
	return os.path.isfile(path)

    # schemes that are not "file" (like "http") cannot be checked easily
    return opts["keep-nonlocal"]

# ------------------------------------------------------------------------
def handle_dir(dir):
    global thumbs_removed
    global thumbs_checked

    if 1 < opts["verbose"]:
        print "cleaning directory '%s'" % dir

    for fn in os.listdir(dir):
	ifn = os.path.join(dir, fn)

        if 2 < opts["verbose"]:
	    print "checking '%s'" % ifn

	if not os.path.isfile(ifn):
	    handle_dir(ifn)   
	    continue

        thumbs_checked += 1

	if not ifn.endswith(".png"):
	    warn("Warning: does not end with .png: '%s'" % ifn)
	    continue

	try:
	    img = Image.open(ifn)
	except Exception, e:
	    warn("Warning: Image.open() failed: %s" % e)
	    warn("    for file '%s'" % ifn)
	    continue

#        if img.info.has_key("Software"):
#	    software = img.info["Software"]
#	    if "GNOME::ThumbnailFactory" != software:
#	        print "Info: creator '%s'" % software

	# can only do the job if there is info about
	# the image the thumb is created from
	if img.info.has_key("Thumb::URI"):
	    uri = img.info["Thumb::URI"]
	    un_uri = unquote(uri)
	    if 2 < opts["verbose"]:
		print "    uri '%s'" % uri
	    if not have_imagefile(uri):
		if not opts["dry-run"]:
		    if 1 < opts["verbose"]:
			print "removing '%s' (%s)" % (ifn, un_uri)
		    try:
		        os.unlink(ifn)
		        thumbs_removed += 1
		    except Exception, e:
			warn("Error: %s: file '%s'" % (e, ifn))
		else:
		    if 1 < opts["verbose"]:
			print "dry-run: not removing '%s' (%s)" % (ifn, un_uri)
	else:
	    warn("Warning: Thumb::URI not found in '%s'" % ifn)

# ------------------------------------------------------------------------
if __name__ == "__main__":

    parse_options()
    termsetup()
	
    homedir = os.path.expanduser("~")
    for d in dirs_to_check:
	dir = os.path.join(homedir, d)
	if not os.path.exists(dir):
	    warn("Warning: no such file or directory: '%s'" % dir)
	    continue
	if not os.path.isdir(dir):
	    warn("Warning: not a directory: '%s'" % dir)
	    continue
	handle_dir(dir)

    if 0 < opts["verbose"]:
        print "%d of %d thumbnail image(s) have been removed." % \
		(thumbs_removed, thumbs_checked)

    sys.exit(0)