2 # -*- coding: utf-8 -*-
6 from cStringIO import StringIO
19 class ImageCache (object):
21 THRESH = 128*1024*1024
25 me._first = me._last = None
29 while me._first and me._total > me.THRESH:
41 img._prev._next = img._next
45 img._next._prev = img._prev
52 class CacheableImage (object):
56 me._prev = me._next = None
62 me._pixbuf = me._acquire()
63 me.size = me._pixbuf.get_pixels_array().nbytes
78 me._thumb = Thumbnail(me)
81 class Thumbnail (object):
83 def __init__(me, img):
85 wd, ht = pix.get_width(), pix.get_height()
90 twd, tht = [(x*THUMBSZ + m//2)//m for x in [wd, ht]]
91 me.pixbuf = pix.scale_simple(twd, tht, GDK.INTERP_HYPER)
93 class NullImage (CacheableImage):
97 def __init__(me, size, text):
98 CacheableImage.__init__(me)
107 img = cls.MAP[size] = cls(size)
112 surf = XR.ImageSurface(XR.FORMAT_ARGB32, me._size, me._size)
113 xr = XR.Context(surf)
115 xr.set_source_rgb(0.3, 0.3, 0.3)
118 xr.move_to(me._size/2.0, me._size/2.0)
119 xr.select_font_face('sans-serif',
120 XR.FONT_SLANT_NORMAL, XR.FONT_WEIGHT_BOLD)
121 xb, yb, wd, ht, xa, ya = xr.text_extents(me._text)
123 z = me._size/float(m) * 2.0/3.0
126 xr.set_source_rgb(0.8, 0.8, 0.8)
127 xr.move_to(3.0*m/4.0 - wd/2.0 - xb, 3.0*m/4.0 - ht/2.0 - yb)
128 xr.show_text(me._text)
131 pix = GDK.pixbuf_new_from_data(surf.get_data(),
132 GDK.COLORSPACE_RGB, True, 8,
133 me._size, me._size, surf.get_stride())
136 class FileImage (CacheableImage):
138 def __init__(me, file):
139 CacheableImage.__init__(me)
143 return GDK.pixbuf_new_from_file(me._file)
147 with U.urlopen(url) as u:
149 stuff = u.read(16384)
153 return out.getvalue()
155 def fix_background(w):
156 style = w.get_style().copy()
157 style.base[GTK.STATE_NORMAL] = BLACK
158 style.bg[GTK.STATE_NORMAL] = BLACK
159 style.text[GTK.STATE_NORMAL] = WHITE
162 class BaseCoverViewer (object):
165 me.scr = GTK.ScrolledWindow()
166 me.scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC)
167 me.iv = GTK.IconView()
168 me.iv.connect('item-activated',
169 lambda iv, p: me.activate(me._frompath(p)))
170 me.iv.connect('selection-changed', me._select)
171 me.iv.set_pixbuf_column(0)
172 me.iv.set_text_column(1)
173 me.iv.set_orientation(GTK.ORIENTATION_VERTICAL)
174 me.iv.set_item_width(THUMBSZ + 32)
175 fix_background(me.iv)
180 me.list = GTK.ListStore(GDK.Pixbuf, G.TYPE_STRING, G.TYPE_PYOBJECT)
181 me.iv.set_model(me.list)
185 item.it = me.list.append([item.img.thumbnail.pixbuf,
189 def _frompath(me, path):
190 return me.list[path][2]
193 sel = me.iv.get_selected_items()
197 me.select(me._frompath(sel[0]))
199 class SearchCover (object):
200 def __init__(me, img):
203 me.text = '%d×%d*' % (pix.get_width(), pix.get_height())
205 class SearchViewer (BaseCoverViewer):
207 def __init__(me, chooser):
208 BaseCoverViewer.__init__(me)
209 me._chooser = chooser
211 def switch(me, current):
214 cov = SearchCover(current)
216 me.iv.select_path(me.list.get_path(cov.it))
218 def activate(me, cov):
219 me._chooser.activated(cov)
222 me._chooser.selected(cov)
224 class RemoteImage (CacheableImage):
226 ERRIMG = NullImage(256, '!')
228 def __init__(me, url, ref = None):
229 CacheableImage.__init__(me)
238 rq = U2.Request(me._url)
240 rq.add_header('Referer', me._ref)
243 stuff = rs.read(16384)
247 me._data = d.getvalue()
248 ld = GDK.PixbufLoader()
254 raise ValueError, 'not going to work'
255 l = min(n, o + 16384)
256 ld.write(me._data[o:l])
262 if 'image/gif' in f['mime_types']:
263 raise ValueError, 'boycotting GIF image'
273 ld = GDK.PixbufLoader()
278 return ld.get_pixbuf()
281 return me.ERRIMG.pixbuf
285 exts = me._format['extensions']
291 class SearchImage (RemoteImage):
293 def __init__(me, url, ref, tburl):
294 RemoteImage.__init__(me, url, ref)
300 me._thumb = Thumbnail(RemoteImage(me._tburl))
303 class SearchResult (SearchCover):
308 url = r['unescapedUrl']
309 ref = r['originalContextUrl']
311 me.img = SearchImage(url, ref, tburl)
312 me.text = '%d×%d' % (w, h)
314 class SearchFail (Exception):
317 class CoverChooser (object):
320 'http://ajax.googleapis.com/ajax/services/search/images?v=1.0&rsz=8&q='
323 me.win = GTK.Window()
326 me.query = GTK.Entry()
327 top.pack_start(me.query, True, True, 2)
328 srch = GTK.Button('_Search')
329 srch.set_flags(GTK.CAN_DEFAULT)
330 srch.connect('clicked', me.search)
331 top.pack_start(srch, False, False, 2)
332 box.pack_start(top, False, False, 2)
333 me.sv = SearchViewer(me)
335 panes.pack1(me.sv.scr, False, True)
336 scr = GTK.ScrolledWindow()
337 scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC)
342 scr.add_with_viewport(evb)
343 panes.pack2(scr, True, True)
344 panes.set_position(THUMBSZ + 64)
345 box.pack_start(panes, True, True, 0)
347 me.win.connect('destroy', me.destroyed)
348 me.win.set_default_size(800, 550)
351 def update(me, view, which, dir, current):
357 me.sv.switch(current)
358 me.query.set_text(me.makequery(dir))
362 q = me.query.get_text()
365 rq = U2.Request(me.SEARCHURL + U.quote_plus(q),
368 'http://www.distorted.org.uk/~mdw/coverart' })
370 except U2.URLError, e:
371 raise SearchFail(e.reason)
373 if result['responseStatus'] != 200:
374 raise SearchFail('%s (status = %d)' %
375 (result['responseDetails'],
376 result['responseStatus']))
377 d = result['responseData']
378 me.sv.switch(me.current)
379 for r in d['results']:
381 me.sv.add(SearchResult(r))
382 except (U2.URLError, U2.HTTPError):
384 except SearchFail, e:
387 def makequery(me, path):
388 bits = path.split(OS.path.sep)
389 return ' '.join(['"%s"' % p for p in bits[-2:]])
391 def selected(me, cov):
393 me.img.set_from_pixbuf(cov.img.pixbuf)
397 def activated(me, cov):
398 if isinstance(cov, SearchCover):
399 me.view.replace(me.which, cov.img)
401 def destroyed(me, w):
407 class ViewCover (object):
409 NULLIMG = NullImage(THUMBSZ, '?')
411 def __init__(me, dir, path, leaf):
416 me.img = me.covimg = FileImage(OS.path.join(me.path, me.leaf))
421 class MainViewer (BaseCoverViewer):
425 def __init__(me, root):
426 BaseCoverViewer.__init__(me)
432 b = OS.path.join(me.root, dir)
434 for l in sorted(OS.listdir(b)):
435 if OS.path.isdir(OS.path.join(b, l)):
437 me.walk(OS.path.join(dir, l))
439 base, ext = OS.path.splitext(l)
440 if base == 'cover' and ext in ['.jpg', '.png', '.gif']:
443 me.add(ViewCover(dir, OS.path.join(me.root, dir), imgfile))
448 def activate(me, cov):
451 CHOOSER = CoverChooser()
452 CHOOSER.update(me, cov, cov.text, cov.covimg)
454 def replace(me, cov, img):
455 leaf = 'cover.%s' % img.ext
456 out = OS.path.join(cov.path, leaf)
458 with open(new, 'wb') as f:
461 if cov.leaf not in [None, leaf]:
462 OS.unlink(OS.path.join(cov.path, cov.leaf))
463 ncov = ViewCover(cov.text, cov.path, leaf)
465 me.list[ncov.it] = [ncov.img.thumbnail.pixbuf, ncov.text, ncov]
472 BLACK = GDK.Color(0, 0, 0)
473 WHITE = GDK.Color(65535, 65535, 65535)
476 VIEW = MainViewer(ROOT)
478 WIN.set_default_size(814, 660)
479 WIN.set_title('coverart')
480 WIN.connect('destroy', lambda _: LOOP.quit())