+++ /dev/null
-#! /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()