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())