diff --git a/ChangeLog b/ChangeLog index 3a9a743af..4b546ecf8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,7 @@ * #496: Remove deprecated functions in favour of get_session_status() * #1112: Fix renaming files in add torrent dialog * #1247: Fix deluge-gtk from hanging on shutdown + * #995: Rewrote tracker_icons ==== Blocklist ==== * Implement local blocklist support diff --git a/deluge/ui/tracker_icons.py b/deluge/ui/tracker_icons.py index 8d38c9c2c..168ebaa10 100644 --- a/deluge/ui/tracker_icons.py +++ b/deluge/ui/tracker_icons.py @@ -1,7 +1,7 @@ # # tracker_icons.py # -# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2010 John Garland # # Deluge is free software. # @@ -33,174 +33,484 @@ # # - - -import threading -from twisted.internet import reactor - -from urllib import urlopen -from deluge.log import LOG as log -from deluge.common import get_pixmap import os -import deluge.configmanager -import deluge.component as component +from HTMLParser import HTMLParser +from urlparse import urljoin, urlparse +from tempfile import mkstemp -#some servers don't have their favicon at the expected location -RENAMES = { - "legaltorrents.com":"beta.legaltorrents.com", - "aelitis.com":"www.vuze.com" - } +from twisted.internet import defer, threads +from twisted.web import error -VALID_ICO_TYPES = ["octet-stream", "x-icon", "image/vnd.microsoft.icon", "vnd.microsoft.icon", "plain"] -VALID_PNG_TYPES = ["octet-stream", "png"] +from deluge.component import Component +from deluge.configmanager import get_config_dir +from deluge.httpdownloader import download_file +from deluge.decorators import proxy +from deluge.log import LOG as log -def fetch_url(url, valid_subtypes=None): +try: + import PIL.Image as Image + import deluge.ui.Win32IconImagePlugin +except ImportError: + PIL_INSTALLED = False +else: + PIL_INSTALLED = True + +class TrackerIcon(object): """ - returns: data or None + Represents a tracker's icon """ - try: - url_file = urlopen(url) - data = url_file.read() - - #validate: - if valid_subtypes and (url_file.info().getsubtype() not in valid_subtypes): - raise Exception("Unexpected type for %s : %s" % (url, url_file.info().getsubtype())) - if not data: - raise Exception("No data") - except Exception, e: - log.debug("%s %s" % (url, e)) - return None - - return data - -class TrackerIcons(component.Component): - def __init__(self): - component.Component.__init__(self, "TrackerIcons") - #set image cache dir - self.image_dir = os.path.join(deluge.configmanager.get_config_dir(), "icons") - if not os.path.exists(self.image_dir): - os.makedirs(self.image_dir) - - #self.images : {tracker_host:filename} - self.images = {"DHT":get_pixmap("dht16.png" )} - - #load image-names in cache-dir - for icon in os.listdir(self.image_dir): - if icon.endswith(".ico"): - self.images[icon[:-4]] = os.path.join(self.image_dir, icon) - if icon.endswith(".png"): - self.images[icon[:-4]] = os.path.join(self.image_dir, icon) - - def _fetch_icon(self, tracker_host): + def __init__(self, filename): """ - returns (ext, data) + Initialises a new TrackerIcon object + + :param filename: the filename of the icon + :type filename: string """ - host_name = RENAMES.get(tracker_host, tracker_host) #HACK! - - ico = fetch_url("http://%s/favicon.ico" % host_name, VALID_ICO_TYPES) - if ico: - return ("ico", ico) - - png = fetch_url("http://%s/favicon.png" % host_name, VALID_PNG_TYPES) - if png: - return ("png", png) - - # FIXME: This should be cleaned up and not copy the top code - - try: - html = urlopen("http://%s/" % (host_name,)) - except Exception, e: - log.debug(e) - html = None - - if html: - icon_path = "" - line = html.readline() - while line: - if ' (16, 16): + new_filename = filename.rpartition('.')[0]+".png" + img = img.resize((16, 16), Image.ANTIALIAS) + img.save(new_filename) + if new_filename != filename: + os.remove(filename) + icon = TrackerIcon(new_filename) + return icon + + def store_icon(self, icon, host): + """ + Stores the icon for the given host + Callbacks any pending deferreds waiting on this icon + + :param icon: the icon to store + :type icon: TrackerIcon or None + :param host: the host to store it for + :type host: string + :returns: the stored icon + :rtype: TrackerIcon or None + """ + self.icons[host] = icon + for d in self.pending[host]: + d.callback(icon) + del self.pending[host] + return icon + +################################ HELPER CLASSES ############################### + +class FaviconParser(HTMLParser): + """ + A HTMLParser which extracts favicons from a HTML page + """ + def __init__(self): + self.icons = [] + HTMLParser.__init__(self) + + def handle_starttag(self, tag, attrs): + if tag == "link" and ("rel", "icon") in attrs or ("rel", "shortcut icon") in attrs: + href = None + type = None + for attr, value in attrs: + if attr == "href": + href = value + elif attr == "type": + type = value + if href and type: + self.icons.append((href, type)) + + def get_icons(self): + """ + Returns a list of favicons extracted from the HTML page + + :returns: a list of favicons + :rtype: list + """ + return self.icons + + +############################### HELPER FUNCTIONS ############################## + +def host_to_url(host): + """ + Given a host, returns the URL to fetch + + :param host: the tracker host + :type host: string + :returns: the url of the tracker + :rtype: string + """ + return "http://%s/" % host + +def url_to_host(url): + """ + Given a URL, returns the host it belongs to + + :param url: the URL in question + :type url: string + :returns: the host of the given URL + :rtype:string + """ + return urlparse(url).hostname + +def host_to_icon_name(host, mimetype): + """ + Given a host, returns the appropriate icon name + + :param host: the host in question + :type host: string + :param mimetype: the mimetype of the icon + :type mimetype: string + :returns: the icon's filename + :rtype: string + """ + return host+'.'+mimetype_to_ext(mimetype) + +def icon_name_to_host(icon): + """ + Given a host's icon name, returns the host name + + :param icon: the icon name + :type icon: string + :returns: the host name + :rtype: string + """ + return icon.rpartition('.')[0] + +def mimetype_to_ext(mimetype): + """ + Given a mimetype, returns the appropriate filename extension + + :param mimetype: the mimetype + :type mimetype: string + :returns: the filename extension for the given mimetype + :rtype: string + :raises KeyError: if given an invalid mimetype + """ + return { + "image/gif" : "gif", + "image/jpeg" : "jpg", + "image/png" : "png", + "image/vnd.microsoft.icon" : "ico", + "image/x-icon" : "ico" + }[mimetype] + +def ext_to_mimetype(ext): + """ + Given a filename extension, returns the appropriate mimetype + + :param ext: the filename extension + :type ext: string + :returns: the mimetype for the given filename extension + :rtype: string + :raises KeyError: if given an invalid filename extension + """ + return { + "gif" : "image/gif", + "jpg" : "image/jpeg", + "jpeg" : "image/jpeg", + "png" : "image/png", + "ico" : "image/vnd.microsoft.icon" + }[ext.lower()] diff --git a/tests/deluge.png b/tests/deluge.png new file mode 100644 index 000000000..e39cd0c7e Binary files /dev/null and b/tests/deluge.png differ diff --git a/tests/google.ico b/tests/google.ico new file mode 100644 index 000000000..ee7c943ab Binary files /dev/null and b/tests/google.ico differ diff --git a/tests/test_tracker_icons.py b/tests/test_tracker_icons.py new file mode 100644 index 000000000..9a68dc935 --- /dev/null +++ b/tests/test_tracker_icons.py @@ -0,0 +1,38 @@ +from twisted.trial import unittest + +from deluge.ui.tracker_icons import TrackerIcons, TrackerIcon +from deluge.log import setupLogger + +# Must come before import common +setupLogger("debug", "debug.log") + +import common + +common.set_tmp_config_dir() +icons = TrackerIcons() + +class TrackerIconsTestCase(unittest.TestCase): + def test_get_png(self): + # Deluge has a png favicon link + icon = TrackerIcon("../deluge.png") + d = icons.get("deluge-torrent.org") + d.addCallback(self.assertNotIdentical, None) + d.addCallback(self.assertEquals, icon) + return d + + def test_get_ico(self): + # Google doesn't have any icon links + # So instead we'll grab its favicon.ico + icon = TrackerIcon("../google.ico") + d = icons.get("www.google.com") + d.addCallback(self.assertNotIdentical, None) + d.addCallback(self.assertEquals, icon) + return d + + def test_get_ico_with_redirect(self): + # google.com redirects to www.google.com + icon = TrackerIcon("../google.ico") + d = icons.get("google.com") + d.addCallback(self.assertNotIdentical, None) + d.addCallback(self.assertEquals, icon) + return d