From 88929d4821cd1cf0de688cc4eb38a62f3749afa6 Mon Sep 17 00:00:00 2001 From: John Garland Date: Sun, 21 Feb 2010 17:20:34 +1100 Subject: [PATCH] Rewrite tracker_icons.py (#995) --- ChangeLog | 1 + deluge/ui/tracker_icons.py | 606 +++++++++++++++++++++++++++--------- tests/deluge.png | Bin 0 -> 722 bytes tests/google.ico | Bin 0 -> 1150 bytes tests/test_tracker_icons.py | 38 +++ 5 files changed, 497 insertions(+), 148 deletions(-) create mode 100644 tests/deluge.png create mode 100644 tests/google.ico create mode 100644 tests/test_tracker_icons.py 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 0000000000000000000000000000000000000000..e39cd0c7eebdf68688339b82aebdeecfe9f5fc9c GIT binary patch literal 722 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG zu$OrHy0TyA2i0va=Bllk>Z!5G+A5@bwbqMQbrR42p4zF}>u@#F;OoD~ z=D*_ZuRAtrExY=o>0X>#*o5QH zmkBG{mpY4ZcL~KmMER};V)?FxOSib7rf%CidKI=H{aCEu6Q0+~R+rkzm4i-_CMTS#8XKrY* ztbF!t^Jmef+St7tYxLrui&k_jYAA4NRH{_`sj+7Y`&#h_f~jks@SK=bu+>Q+=oS}4 zTPky3^MWU>ZU-cp@0jrX|LqzW59GtNn?thg9fNAhmw)~@(o$MUj7%Jp(n+?Isfss;rPH-+Xb zvNN@?h`3eKd-F*8JBR4!N1GC_{SPxfkz%?eSo6s>;|;mosqKw<5dxlqljH60{WrF@ z+VfTR!k(}{Tdl9%dHL{eisRXYEWe{gMZBlIJ=ZLsoZ#(x#;2y<*yzgwc?Yf)e|#Jb zuiUtFtMDb?d%;=7vknIKOUunG<_gpNUR?C%>6_)pYZ=lQBCfJXA7uQ%xZwXT6NV>@ ePqsDHGfsQ|E_q9=;${X01_n=8KbLh*2~7aATuW5| literal 0 HcmV?d00001 diff --git a/tests/google.ico b/tests/google.ico new file mode 100644 index 0000000000000000000000000000000000000000..ee7c943abf1064e3110161deb3a6477fcc7789b5 GIT binary patch literal 1150 zcmZQzU}Ruq5D);-3Je)63=Con3=A3!3=9Gc3=9ek5OD@P;LVrc|7(uE{_nr-{eRbu zAOHJqd;fp(xo`i!{r>s?-ouOkJu77Xn^ZCUx36LP?@+_=-=>=3zhf1{e~^*?{`~$Q zzy1Dyp5<@;i*5YyUv$&Q|I(Yk{Fhtx@qf*ccmMzY|M!3Usg?f?%b5RLH#7dXZ({y$ zSIh9X&jzhxW4f5%p)|2Az5{~a2b{@YeF{QvU%$A9x>XZ|y6dHU*Ry*Awb|L@H8zilVef4f$O z|8|WG|1a!b_W$p@*I<8M{qXDmhPyBRPdk6_|Etg6|AWBqKfnIxF7y1a)yw$bVlu;j z>j@11ExVci+jKDew`pbg-{-0He{P(`|4nsa{|`*a`~U9N`Tzg^|NZ~>|G)pQKfV5+ zy~6FkZZG41n`sRHtwH)HGyJ#hXZUa1#qi&@h2j6ANcI0qLS_EX^WykF!$a`@#~1g( zetz=i;s3x{YX5boF#NZi&h+1UI@5oPDGdLuCo%oE?PvUN+s*XfqLbnOiZIdtbNmJV zFUxoOzi&|s*uTI3|NnpT*8czU>HG}!T`809c(wP1qiDLSH zt}pyQC>%g>7!Z8-KMTW`{|t=(|1&fE{m;Po`9A~0)Bns&w?NYWfB*jde_M{q{}ag! z|Ieo}{684U@c+{KW>C@xr#&~f2mcuve*R}*{_~%a@#lYLh7bQ47+?QqV7UMP|L;Fw zf9_}w_`frp;s2RrrvH1A`2K%-b_X06SFe2g&&BinKLf+x|ICd4{xdLw?Em(kh2hPA z2Brt#Jox|5@Bfc(pZUKmUg7^jAC~_Y*R=ir^Z)Pv_aA@$x3WJB_9qk5FR&R*VE2Ff S&%p2wtha#?l#9v3AiV%6LWsHm literal 0 HcmV?d00001 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