gremlin/gremlin.in: Remove execute bit from input file.
[autoys] / coverart / coverart
1 #! /usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 import sys as SYS
5 import os as OS
6 from cStringIO import StringIO
7
8 import gobject as G
9 import gtk as GTK
10 GDK = GTK.gdk
11 import cairo as XR
12
13 import urllib as U
14 import urllib2 as U2
15 import json as JS
16
17 THUMBSZ = 96
18
19 class ImageCache (object):
20
21 THRESH = 128*1024*1024
22
23 def __init__(me):
24 me._total = 0
25 me._first = me._last = None
26
27 def add(me, img):
28 me._total += img.size
29 while me._first and me._total > me.THRESH:
30 me._first.evict()
31 img._prev = me._last
32 img._next = None
33 if me._last:
34 me._last._next = img
35 else:
36 me._first = img
37 me._last = img
38
39 def rm(me, img):
40 if img._prev:
41 img._prev._next = img._next
42 else:
43 me._first = img._next
44 if img._next:
45 img._next._prev = img._prev
46 else:
47 img._last = img._prev
48 me._total -= img.size
49
50 CACHE = ImageCache()
51
52 class CacheableImage (object):
53
54 def __init__(me):
55 me._pixbuf = None
56 me._prev = me._next = None
57 me._thumb = None
58
59 @property
60 def pixbuf(me):
61 if not me._pixbuf:
62 me._pixbuf = me._acquire()
63 me.size = me._pixbuf.get_pixels_array().nbytes
64 CACHE.add(me)
65 return me._pixbuf
66
67 def evict(me):
68 me._pixbuf = None
69 CACHE.rm(me)
70
71 def flush(me):
72 me.evict()
73 me._thumb = None
74
75 @property
76 def thumbnail(me):
77 if not me._thumb:
78 me._thumb = Thumbnail(me)
79 return me._thumb
80
81 class Thumbnail (object):
82
83 def __init__(me, img):
84 pix = img.pixbuf
85 wd, ht = pix.get_width(), pix.get_height()
86 m = max(wd, ht)
87 if m <= THUMBSZ:
88 me.pixbuf = pix
89 else:
90 twd, tht = [(x*THUMBSZ + m//2)//m for x in [wd, ht]]
91 me.pixbuf = pix.scale_simple(twd, tht, GDK.INTERP_HYPER)
92
93 class NullImage (CacheableImage):
94
95 MAP = {}
96
97 def __init__(me, size, text):
98 CacheableImage.__init__(me)
99 me._size = size
100 me._text = text
101
102 @staticmethod
103 def get(cls, size):
104 try:
105 return cls.MAP[size]
106 except KeyError:
107 img = cls.MAP[size] = cls(size)
108 return img
109
110 def _acquire(me):
111
112 surf = XR.ImageSurface(XR.FORMAT_ARGB32, me._size, me._size)
113 xr = XR.Context(surf)
114
115 xr.set_source_rgb(0.3, 0.3, 0.3)
116 xr.paint()
117
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)
122 m = max(wd, ht)
123 z = me._size/float(m) * 2.0/3.0
124 xr.scale(z, z)
125
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)
129
130 surf.flush()
131 pix = GDK.pixbuf_new_from_data(surf.get_data(),
132 GDK.COLORSPACE_RGB, True, 8,
133 me._size, me._size, surf.get_stride())
134 return pix
135
136 class FileImage (CacheableImage):
137
138 def __init__(me, file):
139 CacheableImage.__init__(me)
140 me._file = file
141
142 def _acquire(me):
143 return GDK.pixbuf_new_from_file(me._file)
144
145 def fetch_url(url):
146 out = StringIO()
147 with U.urlopen(url) as u:
148 while True:
149 stuff = u.read(16384)
150 if not stuff:
151 break
152 out.write(stuff)
153 return out.getvalue()
154
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
160 w.set_style(style)
161
162 class BaseCoverViewer (object):
163
164 def __init__(me):
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)
176 me.scr.add(me.iv)
177 me.reset()
178
179 def reset(me):
180 me.list = GTK.ListStore(GDK.Pixbuf, G.TYPE_STRING, G.TYPE_PYOBJECT)
181 me.iv.set_model(me.list)
182 me.iv.unselect_all()
183
184 def add(me, item):
185 item.it = me.list.append([item.img.thumbnail.pixbuf,
186 item.text,
187 item])
188
189 def _frompath(me, path):
190 return me.list[path][2]
191
192 def _select(me, iv):
193 sel = me.iv.get_selected_items()
194 if len(sel) != 1:
195 me.select(None)
196 else:
197 me.select(me._frompath(sel[0]))
198
199 class SearchCover (object):
200 def __init__(me, img):
201 me.img = img
202 pix = img.pixbuf
203 me.text = '%d×%d*' % (pix.get_width(), pix.get_height())
204
205 class SearchViewer (BaseCoverViewer):
206
207 def __init__(me, chooser):
208 BaseCoverViewer.__init__(me)
209 me._chooser = chooser
210
211 def switch(me, current):
212 me.reset()
213 if current:
214 cov = SearchCover(current)
215 me.add(cov)
216 me.iv.select_path(me.list.get_path(cov.it))
217
218 def activate(me, cov):
219 me._chooser.activated(cov)
220
221 def select(me, cov):
222 me._chooser.selected(cov)
223
224 class RemoteImage (CacheableImage):
225
226 ERRIMG = NullImage(256, '!')
227
228 def __init__(me, url, ref = None):
229 CacheableImage.__init__(me)
230 me._url = url
231 me._ref = ref
232 me._data = None
233
234 def _fetch(me):
235 if me._data:
236 return
237 d = StringIO()
238 rq = U2.Request(me._url)
239 if me._ref:
240 rq.add_header('Referer', me._ref)
241 rs = U2.urlopen(rq)
242 while True:
243 stuff = rs.read(16384)
244 if not stuff:
245 break
246 d.write(stuff)
247 me._data = d.getvalue()
248 ld = GDK.PixbufLoader()
249 try:
250 o = 0
251 n = len(me._data)
252 while True:
253 if o >= n:
254 raise ValueError, 'not going to work'
255 l = min(n, o + 16384)
256 ld.write(me._data[o:l])
257 o = l
258 f = ld.get_format()
259 if f:
260 break
261 me._format = f
262 if 'image/gif' in f['mime_types']:
263 raise ValueError, 'boycotting GIF image'
264 finally:
265 try:
266 ld.close()
267 except G.GError:
268 pass
269
270 def _acquire(me):
271 try:
272 me._fetch()
273 ld = GDK.PixbufLoader()
274 try:
275 ld.write(me._data)
276 finally:
277 ld.close()
278 return ld.get_pixbuf()
279 except Exception, e:
280 print e
281 return me.ERRIMG.pixbuf
282
283 @property
284 def ext(me):
285 exts = me._format['extensions']
286 for i in ['jpg']:
287 if i in exts:
288 return i
289 return exts[0]
290
291 class SearchImage (RemoteImage):
292
293 def __init__(me, url, ref, tburl):
294 RemoteImage.__init__(me, url, ref)
295 me._tburl = tburl
296
297 @property
298 def thumbnail(me):
299 if not me._thumb:
300 me._thumb = Thumbnail(RemoteImage(me._tburl))
301 return me._thumb
302
303 class SearchResult (SearchCover):
304
305 def __init__(me, r):
306 w = int(r['width'])
307 h = int(r['height'])
308 url = r['unescapedUrl']
309 ref = r['originalContextUrl']
310 tburl = r['tbUrl']
311 me.img = SearchImage(url, ref, tburl)
312 me.text = '%d×%d' % (w, h)
313
314 class SearchFail (Exception):
315 pass
316
317 class CoverChooser (object):
318
319 SEARCHURL = \
320 'http://ajax.googleapis.com/ajax/services/search/images?v=1.0&rsz=8&q='
321
322 def __init__(me):
323 me.win = GTK.Window()
324 box = GTK.VBox()
325 top = GTK.HBox()
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)
334 panes = GTK.HPaned()
335 panes.pack1(me.sv.scr, False, True)
336 scr = GTK.ScrolledWindow()
337 scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC)
338 me.img = GTK.Image()
339 evb = GTK.EventBox()
340 evb.add(me.img)
341 fix_background(evb)
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)
346 me.win.add(box)
347 me.win.connect('destroy', me.destroyed)
348 me.win.set_default_size(800, 550)
349 srch.grab_default()
350
351 def update(me, view, which, dir, current):
352 me.view = view
353 me.dir = dir
354 me.which = which
355 me.current = current
356 me.img.clear()
357 me.sv.switch(current)
358 me.query.set_text(me.makequery(dir))
359 me.win.show_all()
360
361 def search(me, w):
362 q = me.query.get_text()
363 try:
364 try:
365 rq = U2.Request(me.SEARCHURL + U.quote_plus(q),
366 None,
367 { 'Referer':
368 'http://www.distorted.org.uk/~mdw/coverart' })
369 rs = U2.urlopen(rq)
370 except U2.URLError, e:
371 raise SearchFail(e.reason)
372 result = JS.load(rs)
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']:
380 try:
381 me.sv.add(SearchResult(r))
382 except (U2.URLError, U2.HTTPError):
383 pass
384 except SearchFail, e:
385 print e.args[0]
386
387 def makequery(me, path):
388 bits = path.split(OS.path.sep)
389 return ' '.join(['"%s"' % p for p in bits[-2:]])
390
391 def selected(me, cov):
392 if cov:
393 me.img.set_from_pixbuf(cov.img.pixbuf)
394 else:
395 me.img.clear()
396
397 def activated(me, cov):
398 if isinstance(cov, SearchCover):
399 me.view.replace(me.which, cov.img)
400
401 def destroyed(me, w):
402 global CHOOSER
403 CHOOSER = None
404
405 CHOOSER = None
406
407 class ViewCover (object):
408
409 NULLIMG = NullImage(THUMBSZ, '?')
410
411 def __init__(me, dir, path, leaf):
412 me.text = dir
413 me.path = path
414 me.leaf = leaf
415 if me.leaf:
416 me.img = me.covimg = FileImage(OS.path.join(me.path, me.leaf))
417 else:
418 me.img = me.NULLIMG
419 me.covimg = None
420
421 class MainViewer (BaseCoverViewer):
422
423 ITERATTR = 'vit'
424
425 def __init__(me, root):
426 BaseCoverViewer.__init__(me)
427 me.root = root
428 me.walk('')
429
430 def walk(me, dir):
431 leafp = True
432 b = OS.path.join(me.root, dir)
433 imgfile = None
434 for l in sorted(OS.listdir(b)):
435 if OS.path.isdir(OS.path.join(b, l)):
436 leafp = False
437 me.walk(OS.path.join(dir, l))
438 else:
439 base, ext = OS.path.splitext(l)
440 if base == 'cover' and ext in ['.jpg', '.png', '.gif']:
441 imgfile = l
442 if leafp:
443 me.add(ViewCover(dir, OS.path.join(me.root, dir), imgfile))
444
445 def select(me, cov):
446 pass
447
448 def activate(me, cov):
449 global CHOOSER
450 if not CHOOSER:
451 CHOOSER = CoverChooser()
452 CHOOSER.update(me, cov, cov.text, cov.covimg)
453
454 def replace(me, cov, img):
455 leaf = 'cover.%s' % img.ext
456 out = OS.path.join(cov.path, leaf)
457 new = out + '.new'
458 with open(new, 'wb') as f:
459 f.write(img._data)
460 OS.rename(new, out)
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)
464 ncov.it = cov.it
465 me.list[ncov.it] = [ncov.img.thumbnail.pixbuf, ncov.text, ncov]
466 me.activate(ncov)
467
468 ROOT = SYS.argv[1]
469
470 LOOP = G.MainLoop()
471
472 BLACK = GDK.Color(0, 0, 0)
473 WHITE = GDK.Color(65535, 65535, 65535)
474
475 WIN = GTK.Window()
476 VIEW = MainViewer(ROOT)
477 WIN.add(VIEW.scr)
478 WIN.set_default_size(814, 660)
479 WIN.set_title('coverart')
480 WIN.connect('destroy', lambda _: LOOP.quit())
481 WIN.show_all()
482
483 LOOP.run()