#! @PYTHON_CMD@ # Copyright (c) 2003,2004 Guilherme Salgado # All rights reserved. # # This file is part of coverhunter. # # coverhunter is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # coverhunter is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with coverhunter; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author: Guilherme Salgado # import os, sys, tempfile, threading, traceback, urllib2 DEBUG = False sys.path.append('@PKGLIBDIR@') import Image import pygtk ; pygtk.require('2.0') import gobject, gtk from gtk import glade, gdk from album import Album, guess_album_info from amazon import Bag import config from libhunter import * from source import AlbumSearch COVER_DISPLAY_SIZE = (300, 300) THREADS = 5 condition = threading.Condition() scheduled_searches = {} album_search = AlbumSearch() empty_album = Album('', '', '') _version = "0.2" class MainWindow: def __init__(self): # start all worker threads. these threads will continuously pop an # item from scheduled_searches and do the search. if # scheduled_searches is empty, all threads will wait() until we add # new items. for x in range(THREADS): w = Worker(self.append_found_cover, self.search_finished) w.setDaemon(True) w.start() # the TreeIter that points to the selected album. self.selected_album = None # the TreeIter that points to the selected cover. self.selected_cover = None gladefile = find_datafile('CoverHunter.glade') self.wtree = glade.XML(gladefile) widgets = ['cover_image', 'artist', 'album_name', 'hpaned', 'main_window', 'fetch', 'set_cover', 'fetch_for_all', 'image_eventbox', 'next', 'previous'] map(lambda w: setattr(self, w, self.wtree.get_widget(w)), widgets) self.selection_sensitive = [self.next, self.previous, self.fetch] self.image_eventbox.connect('drag-data-get', self.drag_data_get) self.image_eventbox.drag_source_set(gdk.BUTTON1_MASK, [('text/plain', 0, 0)], gdk.ACTION_COPY) self.hpaned.set_position(340) self.wtree.signal_autoconnect(self) self.set_cover.set_sensitive(False) for w in self.selection_sensitive: w.set_sensitive(False) self.attach_lists() def drag_data_get(self, widget, context, selection, info, timestamp): cover = self.cover_list.get_cover(self.selected_cover) url = "file://" + cover.coverfile selection.set(selection.target, 8, url) def attach_lists(self): self.album_list = AlbumList() selection = self.album_list.get_selection() selection.connect('changed', self.on_album_selected) self.album_list.connect('row-activated', self.on_fetch_clicked) placeholder = self.wtree.get_widget('albums_placeholder') placeholder.add(self.album_list) self.cover_list = CoverList() selection = self.cover_list.get_selection() selection.connect('changed', self.on_cover_selected) placeholder = self.wtree.get_widget('covers_placeholder') placeholder.add(self.cover_list) def run(self): self.main_window.show_all() gdk.threads_init() gdk.threads_enter() gtk.main() gdk.threads_leave() def visit(self, root, dirname, names): while gtk.events_pending(): gtk.main_iteration() path = dirname dirname = dirname.replace(root, "") dirname = dirname[1:] # remove starting / artist, album_name = guess_album_info(dirname) for name in names: if os.path.isdir(os.path.join(path, name)): if not (artist and album_name): # this is probably a dir with some albums inside it. return self.album_list.append(Album(path, album_name, artist)) def set_artist_and_album(self, album): if not album: self.artist.set_text('') self.album_name.set_text('') else: self.artist.set_text(album.artist) self.album_name.set_text(album.name) def search_finished(self, iter, album): # this method is a callback and will be called by workers to # notify us that the search for one specific album was finished. gdk.threads_enter() if album.has_cover(): coverfile = album.get_cover() pixbuf = gdk.pixbuf_new_from_file(coverfile) pixbuf = pixbuf.scale_simple(24, 24, gdk.INTERP_BILINEAR) else: coverfile = find_datafile('picture.png') pixbuf = gdk.pixbuf_new_from_file(coverfile) self.album_list.set_row_icon(iter, pixbuf) # check if the selection changed. if self.album_list.iter_is_selected(iter): self.fetch.set_sensitive(True) gdk.threads_leave() def schedule_search(self, iter): album = self.album_list.get_album(iter) condition.acquire() if album in scheduled_searches: condition.release() return scheduled_searches.update({album: iter}) condition.notify() condition.release() self.cover_list.clear() if album.has_cover(): self.cover_list.append(album.cover) self.cover_list.select_path(0) album.clear_tmp_covers() pixbuf = gdk.pixbuf_new_from_file(find_datafile('searching.png')) self.album_list.set_row_icon(iter, pixbuf) def append_found_cover(self, iter, cover): # this method will append the found cover to self.cover_list if the # selected row is the row pointed by iter. # worker threads will call it for every cover they found. that's # why we need to call threads_enter()/leave(). gdk.threads_enter() if self.album_list.iter_is_selected(iter): self.cover_list.append(cover) gdk.threads_leave() # # signal handler methods # def on_album_selected(self, selection): model, iter = selection.get_selected() self.cover_image.set_from_file(None) self.cover_list.clear() self.selected_album = iter if not iter: # no row is selected self.set_artist_and_album(empty_album) for w in self.selection_sensitive: w.set_sensitive(False) return for w in self.selection_sensitive: w.set_sensitive(True) album = self.album_list.get_album(iter) self.set_artist_and_album(album) self.cover_list.fill(album.get_all_covers()) def on_cover_selected(self, selection): model, iter = selection.get_selected() if not iter: self.set_cover.set_sensitive(False) self.cover_image.set_from_file(None) self.selected_cover = None return self.set_cover.set_sensitive(True) cover = self.cover_list.get_cover(iter) self.selected_cover = iter pixbuf = gdk.pixbuf_new_from_file(cover.coverfile) size = (pixbuf.get_width(), pixbuf.get_height()) if size > COVER_DISPLAY_SIZE: pixbuf = pixbuf.scale_simple(COVER_DISPLAY_SIZE[0], COVER_DISPLAY_SIZE[1], gtk.gdk.INTERP_BILINEAR) self.cover_image.set_from_pixbuf(pixbuf) def on_artist_changed(self, widget): if self.selected_album: album = self.album_list.get_album(self.selected_album) album.artist = widget.get_text() def on_album_name_changed(self, widget): if self.selected_album: album = self.album_list.get_album(self.selected_album) album.name = widget.get_text() def on_open_activate(self, widget): ds = DirSelection(self.main_window) ds.run() if ds.root is not None: # OK clicked self.album_list.clear() os.path.walk(ds.root, self.visit, ds.root) def on_set_amazon_token_activate(self, widget): d = SetTokenDialog(self.main_window) if d.run() == gtk.RESPONSE_OK and d.token: config.save_amazon_token(d.token) d.destroy() def on_fetch_clicked(self, *args): assert self.selected_album self.fetch.set_sensitive(False) self.schedule_search(self.selected_album) def on_fetch_for_all_clicked(self, widget): self.album_list.model.foreach(lambda m, p, i: self.schedule_search(i)) def on_set_cover_clicked(self, widget): assert self.selected_cover, self.selected_album album = self.album_list.get_album(self.selected_album) cover = self.cover_list.get_cover(self.selected_cover) album.set_cover(cover) pixbuf = gdk.pixbuf_new_from_file(cover.coverfile) pixbuf = pixbuf.scale_simple(24, 24, gdk.INTERP_BILINEAR) self.album_list.set_row_icon(self.selected_album, pixbuf) def on_previous_clicked(self, widget): # go to previous album without cover set (node,) = self.album_list.model.get_path(self.selected_album) while node: node -= 1 iter = self.album_list.model.get_iter((node,)) if not self.album_list.get_album(iter).has_cover(): self.album_list.scroll_to_cell(node) self.album_list.select_iter(iter) return def on_next_clicked(self, widget): # go to next album without cover set (node,) = self.album_list.model.get_path(self.selected_album) node += 1 rows = len(self.album_list.model) while node < rows: iter = self.album_list.model.get_iter(node) if not self.album_list.get_album(iter).has_cover(): self.album_list.scroll_to_cell(node) self.album_list.select_iter(iter) return node += 1 def on_about_activate(self, widget): about_dialog(self.main_window, _version) def quit(self, *args): # remove all temporary cover files map(lambda a: a.clear_tmp_covers(), self.album_list.dump_albums()) gtk.main_quit() class Worker(threading.Thread): def __init__(self, callback, finish_cb): threading.Thread.__init__(self) # this callback will be called for each cover we find... self.callback = callback # ... and this one will be called when the search for an album is # finished. self.finish_cb = finish_cb def check_coverfile(self, coverfile): if not coverfile: return False try: # Check if image is not null. i = Image.open(coverfile) if i.size not in [(1, 1), (0, 0)]: return True except: pass return False def run(self): while True: condition.acquire() while len(scheduled_searches) < 1: condition.wait() album, iter = scheduled_searches.popitem() condition.release() try: # do the search for cover in album_search.find(album): cover.get_image() if self.check_coverfile(cover.coverfile): album.found_covers.append(cover) self.callback(iter, cover) else: os.unlink(cover.coverfile) except: # do not let this thread die with an uncaught exception. if DEBUG: print "Shoot me again / I ain't dead yet" traceback.print_exc() self.finish_cb(iter, album) self.finish_cb(iter, album) class CoverList(ListView): COLUMN_NAME = 0 COLUMN_COVER = 1 def __init__(self): # This list stores a string with artist and album, an amazon.Bag # instance which has the URL of the cover image, and the filename # of the downloaded cover (when it was downloaded) ListView.__init__(self, True, str, object) self.nocover = Bag() self.nocover.ProductName = 'No Cover Found' self.nocover.coverfile = find_datafile('nocover.gif') def fill(self, covers): if not covers: self.append(self.nocover) else: map(lambda c: self.append(c), covers) self.scroll_to_cell(0) self.select_path(0) def append(self, cover): self.model.append((cover.get_full_name(), cover)) def create_columns(self): cell = gtk.CellRendererText() column = gtk.TreeViewColumn("Found Covers", cell, text=self.COLUMN_NAME) column.set_sort_column_id(self.COLUMN_NAME) self.append_column(column) def get_cover(self, iter): return self.model.get_value(iter, self.COLUMN_COVER) class AlbumList(ListView): COLUMN_ICON = 0 COLUMN_NAME = 1 COLUMN_ALBUM = 2 def __init__(self): ListView.__init__(self, True, gdk.Pixbuf, str, object) self.enable_model_drag_dest([('text/plain', 0, 0), ('TEXT', 0, 1), ('STRING', 0, 2), ('text/uri-list', 0, 3)], gtk.gdk.ACTION_COPY) self.connect("drag-data-received", self.drag_data_received) def append(self, album): icon = None if album.has_cover(): icon = gdk.pixbuf_new_from_file(album.get_cover()) icon = icon.scale_simple(24, 24, gtk.gdk.INTERP_BILINEAR) self.model.append((icon, album.get_full_str(), album)) def create_columns(self): cell = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn("", cell, pixbuf=self.COLUMN_ICON) self.append_column(column) cell = gtk.CellRendererText() column = gtk.TreeViewColumn("Album", cell, text=self.COLUMN_NAME) column.set_sort_column_id(self.COLUMN_NAME) self.append_column(column) def drag_data_received(self, treeview, context, x, y, selection, info, timestamp): dummy, path = tempfile.mkstemp() img = open(path, 'wb') sock = urllib2.urlopen(selection.data) img.write(sock.read()) sock.close() img.close() row, pos = self.get_dest_row_at_pos(x, y) iter = self.model.get_iter(row) album = self.get_album(iter) bag = Bag() bag.ProductName = album.get_full_str() bag.coverfile = path album.found_covers.append(bag) if self.path_is_selected(row): # By emiting the 'changed' signal, we make the cover list be # updated, and then show the dropped image. self.get_selection().emit('changed') def dump_albums(self): return self.dump_column(self.COLUMN_ALBUM) def get_album(self, iter): assert iter return self.model.get_value(iter, self.COLUMN_ALBUM) def set_row_icon(self, iter, pixbuf): self.model.set_value(iter, self.COLUMN_ICON, pixbuf) def find_datafile(filename): return os.path.join('@DATADIR@', filename) if __name__ == "__main__": MainWindow().run()