#! /usr/bin/python # -*- coding: utf-8 -*- import sys as SYS import os as OS from cStringIO import StringIO import gobject as G import gtk as GTK GDK = GTK.gdk import cairo as XR import urllib as U import urllib2 as U2 import json as JS THUMBSZ = 96 class ImageCache (object): THRESH = 128*1024*1024 def __init__(me): me._total = 0 me._first = me._last = None def add(me, img): me._total += img.size while me._first and me._total > me.THRESH: me._first.evict() img._prev = me._last img._next = None if me._last: me._last._next = img else: me._first = img me._last = img def rm(me, img): if img._prev: img._prev._next = img._next else: me._first = img._next if img._next: img._next._prev = img._prev else: img._last = img._prev me._total -= img.size CACHE = ImageCache() class CacheableImage (object): def __init__(me): me._pixbuf = None me._prev = me._next = None me._thumb = None @property def pixbuf(me): if not me._pixbuf: me._pixbuf = me._acquire() me.size = me._pixbuf.get_pixels_array().nbytes CACHE.add(me) return me._pixbuf def evict(me): me._pixbuf = None CACHE.rm(me) def flush(me): me.evict() me._thumb = None @property def thumbnail(me): if not me._thumb: me._thumb = Thumbnail(me) return me._thumb class Thumbnail (object): def __init__(me, img): pix = img.pixbuf wd, ht = pix.get_width(), pix.get_height() m = max(wd, ht) if m <= THUMBSZ: me.pixbuf = pix else: twd, tht = [(x*THUMBSZ + m//2)//m for x in [wd, ht]] me.pixbuf = pix.scale_simple(twd, tht, GDK.INTERP_HYPER) class NullImage (CacheableImage): MAP = {} def __init__(me, size, text): CacheableImage.__init__(me) me._size = size me._text = text @staticmethod def get(cls, size): try: return cls.MAP[size] except KeyError: img = cls.MAP[size] = cls(size) return img def _acquire(me): surf = XR.ImageSurface(XR.FORMAT_ARGB32, me._size, me._size) xr = XR.Context(surf) xr.set_source_rgb(0.3, 0.3, 0.3) xr.paint() xr.move_to(me._size/2.0, me._size/2.0) xr.select_font_face('sans-serif', XR.FONT_SLANT_NORMAL, XR.FONT_WEIGHT_BOLD) xb, yb, wd, ht, xa, ya = xr.text_extents(me._text) m = max(wd, ht) z = me._size/float(m) * 2.0/3.0 xr.scale(z, z) xr.set_source_rgb(0.8, 0.8, 0.8) xr.move_to(3.0*m/4.0 - wd/2.0 - xb, 3.0*m/4.0 - ht/2.0 - yb) xr.show_text(me._text) surf.flush() pix = GDK.pixbuf_new_from_data(surf.get_data(), GDK.COLORSPACE_RGB, True, 8, me._size, me._size, surf.get_stride()) return pix class FileImage (CacheableImage): def __init__(me, file): CacheableImage.__init__(me) me._file = file def _acquire(me): return GDK.pixbuf_new_from_file(me._file) def fetch_url(url): out = StringIO() with U.urlopen(url) as u: while True: stuff = u.read(16384) if not stuff: break out.write(stuff) return out.getvalue() def fix_background(w): style = w.get_style().copy() style.base[GTK.STATE_NORMAL] = BLACK style.bg[GTK.STATE_NORMAL] = BLACK style.text[GTK.STATE_NORMAL] = WHITE w.set_style(style) class BaseCoverViewer (object): def __init__(me): me.scr = GTK.ScrolledWindow() me.scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC) me.iv = GTK.IconView() me.iv.connect('item-activated', lambda iv, p: me.activate(me._frompath(p))) me.iv.connect('selection-changed', me._select) me.iv.set_pixbuf_column(0) me.iv.set_text_column(1) me.iv.set_orientation(GTK.ORIENTATION_VERTICAL) me.iv.set_item_width(THUMBSZ + 32) fix_background(me.iv) me.scr.add(me.iv) me.reset() def reset(me): me.list = GTK.ListStore(GDK.Pixbuf, G.TYPE_STRING, G.TYPE_PYOBJECT) me.iv.set_model(me.list) me.iv.unselect_all() def add(me, item): item.it = me.list.append([item.img.thumbnail.pixbuf, item.text, item]) def _frompath(me, path): return me.list[path][2] def _select(me, iv): sel = me.iv.get_selected_items() if len(sel) != 1: me.select(None) else: me.select(me._frompath(sel[0])) class SearchCover (object): def __init__(me, img): me.img = img pix = img.pixbuf me.text = '%d×%d*' % (pix.get_width(), pix.get_height()) class SearchViewer (BaseCoverViewer): def __init__(me, chooser): BaseCoverViewer.__init__(me) me._chooser = chooser def switch(me, current): me.reset() if current: cov = SearchCover(current) me.add(cov) me.iv.select_path(me.list.get_path(cov.it)) def activate(me, cov): me._chooser.activated(cov) def select(me, cov): me._chooser.selected(cov) class RemoteImage (CacheableImage): ERRIMG = NullImage(256, '!') def __init__(me, url, ref = None): CacheableImage.__init__(me) me._url = url me._ref = ref me._data = None def _fetch(me): if me._data: return d = StringIO() rq = U2.Request(me._url) if me._ref: rq.add_header('Referer', me._ref) rs = U2.urlopen(rq) while True: stuff = rs.read(16384) if not stuff: break d.write(stuff) me._data = d.getvalue() ld = GDK.PixbufLoader() try: o = 0 n = len(me._data) while True: if o >= n: raise ValueError, 'not going to work' l = min(n, o + 16384) ld.write(me._data[o:l]) o = l f = ld.get_format() if f: break me._format = f if 'image/gif' in f['mime_types']: raise ValueError, 'boycotting GIF image' finally: try: ld.close() except G.GError: pass def _acquire(me): try: me._fetch() ld = GDK.PixbufLoader() try: ld.write(me._data) finally: ld.close() return ld.get_pixbuf() except Exception, e: print e return me.ERRIMG.pixbuf @property def ext(me): exts = me._format['extensions'] for i in ['jpg']: if i in exts: return i return exts[0] class SearchImage (RemoteImage): def __init__(me, url, ref, tburl): RemoteImage.__init__(me, url, ref) me._tburl = tburl @property def thumbnail(me): if not me._thumb: me._thumb = Thumbnail(RemoteImage(me._tburl)) return me._thumb class SearchResult (SearchCover): def __init__(me, r): w = int(r['width']) h = int(r['height']) url = r['unescapedUrl'] ref = r['originalContextUrl'] tburl = r['tbUrl'] me.img = SearchImage(url, ref, tburl) me.text = '%d×%d' % (w, h) class SearchFail (Exception): pass class CoverChooser (object): SEARCHURL = \ 'http://ajax.googleapis.com/ajax/services/search/images?v=1.0&rsz=8&q=' def __init__(me): me.win = GTK.Window() box = GTK.VBox() top = GTK.HBox() me.query = GTK.Entry() top.pack_start(me.query, True, True, 2) srch = GTK.Button('_Search') srch.set_flags(GTK.CAN_DEFAULT) srch.connect('clicked', me.search) top.pack_start(srch, False, False, 2) box.pack_start(top, False, False, 2) me.sv = SearchViewer(me) panes = GTK.HPaned() panes.pack1(me.sv.scr, False, True) scr = GTK.ScrolledWindow() scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC) me.img = GTK.Image() evb = GTK.EventBox() evb.add(me.img) fix_background(evb) scr.add_with_viewport(evb) panes.pack2(scr, True, True) panes.set_position(THUMBSZ + 64) box.pack_start(panes, True, True, 0) me.win.add(box) me.win.connect('destroy', me.destroyed) me.win.set_default_size(800, 550) srch.grab_default() def update(me, view, which, dir, current): me.view = view me.dir = dir me.which = which me.current = current me.img.clear() me.sv.switch(current) me.query.set_text(me.makequery(dir)) me.win.show_all() def search(me, w): q = me.query.get_text() try: try: rq = U2.Request(me.SEARCHURL + U.quote_plus(q), None, { 'Referer': 'http://www.distorted.org.uk/~mdw/coverart' }) rs = U2.urlopen(rq) except U2.URLError, e: raise SearchFail(e.reason) result = JS.load(rs) if result['responseStatus'] != 200: raise SearchFail('%s (status = %d)' % (result['responseDetails'], result['responseStatus'])) d = result['responseData'] me.sv.switch(me.current) for r in d['results']: try: me.sv.add(SearchResult(r)) except (U2.URLError, U2.HTTPError): pass except SearchFail, e: print e.args[0] def makequery(me, path): bits = path.split(OS.path.sep) return ' '.join(['"%s"' % p for p in bits[-2:]]) def selected(me, cov): if cov: me.img.set_from_pixbuf(cov.img.pixbuf) else: me.img.clear() def activated(me, cov): if isinstance(cov, SearchCover): me.view.replace(me.which, cov.img) def destroyed(me, w): global CHOOSER CHOOSER = None CHOOSER = None class ViewCover (object): NULLIMG = NullImage(THUMBSZ, '?') def __init__(me, dir, path, leaf): me.text = dir me.path = path me.leaf = leaf if me.leaf: me.img = me.covimg = FileImage(OS.path.join(me.path, me.leaf)) else: me.img = me.NULLIMG me.covimg = None class MainViewer (BaseCoverViewer): ITERATTR = 'vit' def __init__(me, root): BaseCoverViewer.__init__(me) me.root = root me.walk('') def walk(me, dir): leafp = True b = OS.path.join(me.root, dir) imgfile = None for l in sorted(OS.listdir(b)): if OS.path.isdir(OS.path.join(b, l)): leafp = False me.walk(OS.path.join(dir, l)) else: base, ext = OS.path.splitext(l) if base == 'cover' and ext in ['.jpg', '.png', '.gif']: imgfile = l if leafp: me.add(ViewCover(dir, OS.path.join(me.root, dir), imgfile)) def select(me, cov): pass def activate(me, cov): global CHOOSER if not CHOOSER: CHOOSER = CoverChooser() CHOOSER.update(me, cov, cov.text, cov.covimg) def replace(me, cov, img): leaf = 'cover.%s' % img.ext out = OS.path.join(cov.path, leaf) new = out + '.new' with open(new, 'wb') as f: f.write(img._data) OS.rename(new, out) if cov.leaf not in [None, leaf]: OS.unlink(OS.path.join(cov.path, cov.leaf)) ncov = ViewCover(cov.text, cov.path, leaf) ncov.it = cov.it me.list[ncov.it] = [ncov.img.thumbnail.pixbuf, ncov.text, ncov] me.activate(ncov) ROOT = SYS.argv[1] LOOP = G.MainLoop() BLACK = GDK.Color(0, 0, 0) WHITE = GDK.Color(65535, 65535, 65535) WIN = GTK.Window() VIEW = MainViewer(ROOT) WIN.add(VIEW.scr) WIN.set_default_size(814, 660) WIN.set_title('coverart') WIN.connect('destroy', lambda _: LOOP.quit()) WIN.show_all() LOOP.run()