From 6e28eb1713cf1d76249d7854faa3d1ce9f6e3241 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 13 May 2011 05:01:03 -0600 Subject: [PATCH 01/21] Added src/calibre_plugins/ to ignore list --- .bzrignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.bzrignore b/.bzrignore index aa6637bc7a..005391bf46 100644 --- a/.bzrignore +++ b/.bzrignore @@ -31,3 +31,4 @@ nbproject/ .pydevproject .settings/ *.DS_Store +calibre_plugins/ \ No newline at end of file From 654d55c3493b704c6626bc112c6b01e3062edde8 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 18 May 2011 08:30:46 -0600 Subject: [PATCH 02/21] revised logic in _update_iTunes_metadata() to properly utilized title_sort plugboard transform values. If present, overrides setting title_sort to index/series when USE_SERIES_AS_CATEGORY is true. Fixes bug #783229 --- src/calibre/devices/apple/driver.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index b7d5ac36d2..4201215a0f 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -2754,8 +2754,7 @@ class ITUNES(DriverBase): lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) lb_added.enabled.set(True) lb_added.sort_artist.set(icu_title(metadata_x.author_sort)) - lb_added.sort_name.set(metadata.title_sort) - + lb_added.sort_name.set(metadata_x.title_sort) if db_added: db_added.name.set(metadata_x.title) @@ -2765,7 +2764,7 @@ class ITUNES(DriverBase): db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) db_added.enabled.set(True) db_added.sort_artist.set(icu_title(metadata_x.author_sort)) - db_added.sort_name.set(metadata.title_sort) + db_added.sort_name.set(metadata_x.title_sort) if metadata_x.comments: if lb_added: @@ -2785,6 +2784,7 @@ class ITUNES(DriverBase): # Set genre from series if available, else first alpha tag # Otherwise iTunes grabs the first dc:subject from the opf metadata + # If title_sort applied in plugboard, that overrides using series/index as title_sort if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]: if DEBUG: self.log.info(" ITUNES._update_iTunes_metadata()") @@ -2796,7 +2796,9 @@ class ITUNES(DriverBase): fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: - lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) + # If no title_sort plugboard tweak, create sort_name from series/index + if metadata.title_sort == metadata_x.title_sort: + lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) lb_added.episode_ID.set(metadata_x.series) lb_added.episode_number.set(metadata_x.series_index) @@ -2810,7 +2812,9 @@ class ITUNES(DriverBase): break if db_added: - db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) + # If no title_sort plugboard tweak, create sort_name from series/index + if metadata.title_sort == metadata_x.title_sort: + db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) db_added.episode_ID.set(metadata_x.series) db_added.episode_number.set(metadata_x.series_index) @@ -2845,7 +2849,7 @@ class ITUNES(DriverBase): lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) lb_added.Enabled = True lb_added.SortArtist = icu_title(metadata_x.author_sort) - lb_added.SortName = metadata.title_sort + lb_added.SortName = metadata_x.title_sort if db_added: db_added.Name = metadata_x.title @@ -2855,7 +2859,7 @@ class ITUNES(DriverBase): db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) db_added.Enabled = True db_added.SortArtist = icu_title(metadata_x.author_sort) - db_added.SortName = metadata.title_sort + db_added.SortName = metadata_x.title_sort if metadata_x.comments: if lb_added: @@ -2888,7 +2892,9 @@ class ITUNES(DriverBase): fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: - lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) + # If no title_sort plugboard tweak, create sort_name from series/index + if metadata.title_sort == metadata_x.title_sort: + lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) lb_added.EpisodeID = metadata_x.series try: @@ -2914,7 +2920,9 @@ class ITUNES(DriverBase): break if db_added: - db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) + # If no title_sort plugboard tweak, create sort_name from series/index + if metadata.title_sort == metadata_x.title_sort: + db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) db_added.EpisodeID = metadata_x.series try: From feb3b98a091fa9955dfb0483655fb988fb9525b4 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 18 May 2011 21:04:00 -0400 Subject: [PATCH 03/21] Store: Add OReilly plugin. --- src/calibre/customize/builtins.py | 8 ++- src/calibre/gui2/store/oreilly_plugin.py | 78 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/store/oreilly_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8e16d4c76c..b2d3a967df 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1199,6 +1199,11 @@ class StoreOpenLibraryStore(StoreBase): description = _('One web page for every book.') actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore' +class StoreOReillyStore(StoreBase): + name = 'OReilly' + description = _('DRM-Free tech ebooks.') + actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore' + class StoreSmashwordsStore(StoreBase): name = 'Smashwords' description = _('Your ebook. Your way.') @@ -1226,7 +1231,8 @@ plugins += [StoreArchiveOrgStore, StoreAmazonKindleStore, StoreAmazonDEKindleSto StoreEHarlequinStore, StoreFeedbooksStore, StoreFoylesUKStore, StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, - StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, StoreSmashwordsStore, + StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, + StoreOReillyStore, StoreSmashwordsStore, StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore] # }}} diff --git a/src/calibre/gui2/store/oreilly_plugin.py b/src/calibre/gui2/store/oreilly_plugin.py new file mode 100644 index 0000000000..b7e4e040f4 --- /dev/null +++ b/src/calibre/gui2/store/oreilly_plugin.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class OReillyStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://oreilly.com/ebooks/' + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://search.oreilly.com/?t1=Books&t2=Format&t3=Ebook&q=' + urllib.quote_plus(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="results"]/div[@class="result"]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[@class="title"]/a/@href')) + if not id: + continue + + cover_url = ''.join(data.xpath('.//div[@class="bigCover"]//img/@src')) + + title = ''.join(data.xpath('.//div[@class="title"]/a/text()')) + author = ''.join(data.xpath('.//div[@class="author"]/text()')) + author = author.split('By ')[-1].strip() + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url.strip() + s.title = title.strip() + s.author = author.strip() + s.detail_item = id.strip() + s.drm = SearchResult.DRM_UNLOCKED + + yield s + + def get_details(self, search_result, timeout): + br = browser() + with closing(br.open(search_result.detail_item, timeout=timeout)) as nf: + doc = html.fromstring(nf.read()) + + search_result.price = ''.join(doc.xpath('(//span[@class="price"])[1]/span//text()')).strip() + search_result.formats = ', '.join(doc.xpath('//div[@class="ebook_formats"]//a/text()')).upper() + + return True + From fa3f01c95066c431d72cfe4dcfb6d7d092269431 Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 19 May 2011 06:40:57 -0600 Subject: [PATCH 04/21] Further revisions after chaley's patch to library.save_to_disk:find_plugboard(). Fixes #783229 --- src/calibre/devices/apple/driver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 4201215a0f..42949215b2 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -941,7 +941,7 @@ class ITUNES(DriverBase): # declared in use_plugboard_ext and a device name of ITUNES if DEBUG: self.log.info("ITUNES.set_plugboard()") - #self.log.info(' using plugboard %s' % plugboards) + #self.log.info(' plugboard: %s' % plugboards) self.plugboards = plugboards self.plugboard_func = pb_func @@ -1052,7 +1052,6 @@ class ITUNES(DriverBase): 'title': metadata[i].title, 'uuid': metadata[i].uuid } - # Report progress if self.report_progress is not None: self.report_progress((i+1)/file_count, _('%d of %d') % (i+1, file_count)) @@ -2744,7 +2743,7 @@ class ITUNES(DriverBase): # Update metadata from plugboard # If self.plugboard is None (no transforms), original metadata is returned intact metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format) - + self.log("metadata.title_sort: %s metadata_x.title_sort: %s" % (metadata.title_sort, metadata_x.title_sort)) if isosx: if lb_added: lb_added.name.set(metadata_x.title) @@ -2983,6 +2982,9 @@ class ITUNES(DriverBase): newmi.publisher if book.publisher != newmi.publisher else '')) self.log.info(" tags: %s %s" % (book.tags, ">>> %s" % newmi.tags if book.tags != newmi.tags else '')) + else: + self.log(" matching plugboard not found") + else: newmi = book return newmi From ed3742f45e079e8ba240e97a24646eb4148adb27 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 09:30:15 -0600 Subject: [PATCH 05/21] National GEographic by gagsays --- recipes/icons/natgeo.png | Bin 0 -> 247 bytes recipes/natgeo.recipe | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 recipes/icons/natgeo.png create mode 100644 recipes/natgeo.recipe diff --git a/recipes/icons/natgeo.png b/recipes/icons/natgeo.png new file mode 100644 index 0000000000000000000000000000000000000000..fe898bcd5d3d8f5f49876ce53b5d3bcdeac74c4a GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9oB=)|u0Z<#D~A97|FhQaP5^Qk zOM?7@862M7NCR<_yxm=x(zt6;OkH}&m?E%JaC$sH<3<*yc#}JO|$q5XM$1Vi0 zYdk#Qp~ZNen~5PRpX1=J+0CIqHL4}95hW>!C8<`)MX5lF!N|bSP}jgz*U&P=z|zXd z+{(~M*TBrmz+jF_86S#<-29Zxv`S Date: Thu, 19 May 2011 09:58:40 -0600 Subject: [PATCH 06/21] Update Zeroconf library --- src/calibre/utils/Zeroconf.py | 627 +++++++++++++++++----------------- 1 file changed, 316 insertions(+), 311 deletions(-) diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py index 2b3661162f..bdb0b1826d 100755 --- a/src/calibre/utils/Zeroconf.py +++ b/src/calibre/utils/Zeroconf.py @@ -1,10 +1,12 @@ -''' Multicast DNS Service Discovery for Python, v0.12 +""" Multicast DNS Service Discovery for Python Copyright (C) 2003, Paul Scott-Murphy + Copyright (C) 2009, Alexander Solovyov This module provides a framework for the use of DNS Service Discovery using IP multicast. It has been tested against the JRendezvous - implementation from StrangeBerry, - and against the mDNSResponder from Mac OS X 10.3.8. + implementation from StrangeBerry, + against the mDNSResponder from Mac OS X 10.3.8, 10.5.6, and against + Avahi library under various Linux distributions. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -20,13 +22,16 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -''' +""" -'''0.12 update - allow selection of binding interface +"""0.13 update - fix IPv6 support + some cleanups in code""" + +"""0.12 update - allow selection of binding interface typo fix - Thanks A. M. Kuchlingi - removed all use of word 'Rendezvous' - this is an API change''' + removed all use of word 'Rendezvous' - this is an API change""" -'''0.11 update - correction to comments for addListener method +"""0.11 update - correction to comments for addListener method support for new record types seen from OS X - IPv6 address - hostinfo @@ -34,9 +39,9 @@ fixes to name decoding works alongside other processes using port 5353 (e.g. on Mac OS X) tested against Mac OS X 10.3.2's mDNSResponder - corrections to removal of list entries for service browser''' + corrections to removal of list entries for service browser""" -'''0.10 update - Jonathon Paisley contributed these corrections: +"""0.10 update - Jonathon Paisley contributed these corrections: always multicast replies, even when query is unicast correct a pointer encoding problem can now write records in any order @@ -45,38 +50,38 @@ server is now separate from name can cancel a service browser - modified some unit tests to accommodate these changes''' + modified some unit tests to accommodate these changes""" -'''0.09 update - remove all records on service unregistration - fix DOS security problem with readName''' +"""0.09 update - remove all records on service unregistration + fix DOS security problem with readName""" -'''0.08 update - changed licensing to LGPL''' +"""0.08 update - changed licensing to LGPL""" -'''0.07 update - faster shutdown on engine +"""0.07 update - faster shutdown on engine pointer encoding of outgoing names ServiceBrowser now works - new unit tests''' + new unit tests""" -'''0.06 update - small improvements with unit tests +"""0.06 update - small improvements with unit tests added defined exception types new style objects fixed hostname/interface problem fixed socket timeout problem fixed addServiceListener() typo bug using select() for socket reads - tested on Debian unstable with Python 2.2.2''' + tested on Debian unstable with Python 2.2.2""" -'''0.05 update - ensure case insensitivty on domain names - support for unicast DNS queries''' +"""0.05 update - ensure case insensitivty on domain names + support for unicast DNS queries""" -'''0.04 update - added some unit tests +"""0.04 update - added some unit tests added __ne__ adjuncts where required ensure names end in '.local.' - timeout on receiving socket for clean shutdown''' + timeout on receiving socket for clean shutdown""" -__author__ = 'Paul Scott-Murphy' -__email__ = 'paul at scott dash murphy dot com' -__version__ = '0.12' +__author__ = "Paul Scott-Murphy" +__email__ = "paul at scott dash murphy dot com" +__version__ = "0.12" import string import time @@ -86,7 +91,7 @@ import threading import select import traceback -__all__ = ['Zeroconf', 'ServiceInfo', 'ServiceBrowser'] +__all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"] # hook for threads @@ -154,39 +159,48 @@ _TYPE_ANY = 255 # Mapping constants to names -_CLASSES = { _CLASS_IN : 'in', - _CLASS_CS : 'cs', - _CLASS_CH : 'ch', - _CLASS_HS : 'hs', - _CLASS_NONE : 'none', - _CLASS_ANY : 'any' } +_CLASSES = { _CLASS_IN : "in", + _CLASS_CS : "cs", + _CLASS_CH : "ch", + _CLASS_HS : "hs", + _CLASS_NONE : "none", + _CLASS_ANY : "any" } -_TYPES = { _TYPE_A : 'a', - _TYPE_NS : 'ns', - _TYPE_MD : 'md', - _TYPE_MF : 'mf', - _TYPE_CNAME : 'cname', - _TYPE_SOA : 'soa', - _TYPE_MB : 'mb', - _TYPE_MG : 'mg', - _TYPE_MR : 'mr', - _TYPE_NULL : 'null', - _TYPE_WKS : 'wks', - _TYPE_PTR : 'ptr', - _TYPE_HINFO : 'hinfo', - _TYPE_MINFO : 'minfo', - _TYPE_MX : 'mx', - _TYPE_TXT : 'txt', - _TYPE_AAAA : 'quada', - _TYPE_SRV : 'srv', - _TYPE_ANY : 'any' } +_TYPES = { _TYPE_A : "a", + _TYPE_NS : "ns", + _TYPE_MD : "md", + _TYPE_MF : "mf", + _TYPE_CNAME : "cname", + _TYPE_SOA : "soa", + _TYPE_MB : "mb", + _TYPE_MG : "mg", + _TYPE_MR : "mr", + _TYPE_NULL : "null", + _TYPE_WKS : "wks", + _TYPE_PTR : "ptr", + _TYPE_HINFO : "hinfo", + _TYPE_MINFO : "minfo", + _TYPE_MX : "mx", + _TYPE_TXT : "txt", + _TYPE_AAAA : "quada", + _TYPE_SRV : "srv", + _TYPE_ANY : "any" } # utility functions def currentTimeMillis(): - '''Current system time in milliseconds''' + """Current system time in milliseconds""" return time.time() * 1000 +def ntop(address): + """Convert address to its string representation""" + af = len(address) == 4 and socket.AF_INET or socket.AF_INET6 + return socket.inet_ntop(af, address) + +def address_type(address): + """Return appropriate record type for an address""" + return len(address) == 4 and _TYPE_A or _TYPE_AAAA + # Exceptions class NonLocalNameException(Exception): @@ -204,10 +218,17 @@ class AbstractMethodException(Exception): class BadTypeInNameException(Exception): pass +class BadDomainName(Exception): + def __init__(self, pos): + Exception.__init__(self, "at position " + str(pos)) + +class BadDomainNameCircular(BadDomainName): + pass + # implementation classes class DNSEntry(object): - '''A DNS entry''' + """A DNS entry""" def __init__(self, name, type, clazz): self.key = string.lower(name) @@ -217,62 +238,62 @@ class DNSEntry(object): self.unique = (clazz & _CLASS_UNIQUE) != 0 def __eq__(self, other): - '''Equality test on name, type, and class''' + """Equality test on name, type, and class""" if isinstance(other, DNSEntry): return self.name == other.name and self.type == other.type and self.clazz == other.clazz return 0 def __ne__(self, other): - '''Non-equality test''' + """Non-equality test""" return not self.__eq__(other) def getClazz(self, clazz): - '''Class accessor''' + """Class accessor""" try: return _CLASSES[clazz] except: - return '?(%s)' % (clazz) + return "?(%s)" % (clazz) def getType(self, type): - '''Type accessor''' + """Type accessor""" try: return _TYPES[type] except: - return '?(%s)' % (type) + return "?(%s)" % (type) def toString(self, hdr, other): - '''String representation with additional information''' - result = '%s[%s,%s' % (hdr, self.getType(self.type), self.getClazz(self.clazz)) + """String representation with additional information""" + result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz)) if self.unique: - result += '-unique,' + result += "-unique," else: - result += ',' + result += "," result += self.name if other is not None: - result += ',%s]' % (other) + result += ",%s]" % (other) else: - result += ']' + result += "]" return result class DNSQuestion(DNSEntry): - '''A DNS question entry''' + """A DNS question entry""" def __init__(self, name, type, clazz): - if not name.endswith('.local.'): - raise NonLocalNameException('DNSQuestion: Not a local name '+name) + if not name.endswith(".local."): + raise NonLocalNameException(name) DNSEntry.__init__(self, name, type, clazz) def answeredBy(self, rec): - '''Returns true if the question is answered by the record''' + """Returns true if the question is answered by the record""" return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name def __repr__(self): - '''String representation''' - return DNSEntry.toString(self, 'question', None) + """String representation""" + return DNSEntry.toString(self, "question", None) class DNSRecord(DNSEntry): - '''A DNS record - like a DNS entry, but has a TTL''' + """A DNS record - like a DNS entry, but has a TTL""" def __init__(self, name, type, clazz, ttl): DNSEntry.__init__(self, name, type, clazz) @@ -280,84 +301,84 @@ class DNSRecord(DNSEntry): self.created = currentTimeMillis() def __eq__(self, other): - '''Tests equality as per DNSRecord''' + """Tests equality as per DNSRecord""" if isinstance(other, DNSRecord): return DNSEntry.__eq__(self, other) return 0 def suppressedBy(self, msg): - '''Returns true if any answer in a message can suffice for the - information held in this record.''' + """Returns true if any answer in a message can suffice for the + information held in this record.""" for record in msg.answers: if self.suppressedByAnswer(record): return 1 return 0 def suppressedByAnswer(self, other): - '''Returns true if another record has same name, type and class, - and if its TTL is at least half of this record's.''' + """Returns true if another record has same name, type and class, + and if its TTL is at least half of this record's.""" if self == other and other.ttl > (self.ttl / 2): return 1 return 0 def getExpirationTime(self, percent): - '''Returns the time at which this record will have expired - by a certain percentage.''' + """Returns the time at which this record will have expired + by a certain percentage.""" return self.created + (percent * self.ttl * 10) def getRemainingTTL(self, now): - '''Returns the remaining TTL in seconds.''' + """Returns the remaining TTL in seconds.""" return max(0, (self.getExpirationTime(100) - now) / 1000) def isExpired(self, now): - '''Returns true if this record has expired.''' + """Returns true if this record has expired.""" return self.getExpirationTime(100) <= now def isStale(self, now): - '''Returns true if this record is at least half way expired.''' + """Returns true if this record is at least half way expired.""" return self.getExpirationTime(50) <= now def resetTTL(self, other): - '''Sets this record's TTL and created time to that of - another record.''' + """Sets this record's TTL and created time to that of + another record.""" self.created = other.created self.ttl = other.ttl def write(self, out): - '''Abstract method''' + """Abstract method""" raise AbstractMethodException def toString(self, other): - '''String representation with addtional information''' - arg = '%s/%s,%s' % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other) - return DNSEntry.toString(self, 'record', arg) + """String representation with addtional information""" + arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other) + return DNSEntry.toString(self, "record", arg) class DNSAddress(DNSRecord): - '''A DNS address record''' + """A DNS address record""" def __init__(self, name, type, clazz, ttl, address): DNSRecord.__init__(self, name, type, clazz, ttl) self.address = address def write(self, out): - '''Used in constructing an outgoing packet''' + """Used in constructing an outgoing packet""" out.writeString(self.address, len(self.address)) def __eq__(self, other): - '''Tests equality on address''' + """Tests equality on address""" if isinstance(other, DNSAddress): return self.address == other.address return 0 def __repr__(self): - '''String representation''' + """String representation""" try: - return socket.inet_ntoa(self.address) + return 'record[%s]' % ntop(self.address) except: - return self.address + return 'record[%s]' % self.address class DNSHinfo(DNSRecord): - '''A DNS host information record''' + """A DNS host information record""" def __init__(self, name, type, clazz, ttl, cpu, os): DNSRecord.__init__(self, name, type, clazz, ttl) @@ -365,67 +386,67 @@ class DNSHinfo(DNSRecord): self.os = os def write(self, out): - '''Used in constructing an outgoing packet''' + """Used in constructing an outgoing packet""" out.writeString(self.cpu, len(self.cpu)) out.writeString(self.os, len(self.os)) def __eq__(self, other): - '''Tests equality on cpu and os''' + """Tests equality on cpu and os""" if isinstance(other, DNSHinfo): return self.cpu == other.cpu and self.os == other.os return 0 def __repr__(self): - '''String representation''' - return self.cpu + ' ' + self.os + """String representation""" + return self.cpu + " " + self.os class DNSPointer(DNSRecord): - '''A DNS pointer record''' + """A DNS pointer record""" def __init__(self, name, type, clazz, ttl, alias): DNSRecord.__init__(self, name, type, clazz, ttl) self.alias = alias def write(self, out): - '''Used in constructing an outgoing packet''' + """Used in constructing an outgoing packet""" out.writeName(self.alias) def __eq__(self, other): - '''Tests equality on alias''' + """Tests equality on alias""" if isinstance(other, DNSPointer): return self.alias == other.alias return 0 def __repr__(self): - '''String representation''' + """String representation""" return self.toString(self.alias) class DNSText(DNSRecord): - '''A DNS text record''' + """A DNS text record""" def __init__(self, name, type, clazz, ttl, text): DNSRecord.__init__(self, name, type, clazz, ttl) self.text = text def write(self, out): - '''Used in constructing an outgoing packet''' + """Used in constructing an outgoing packet""" out.writeString(self.text, len(self.text)) def __eq__(self, other): - '''Tests equality on text''' + """Tests equality on text""" if isinstance(other, DNSText): return self.text == other.text return 0 def __repr__(self): - '''String representation''' + """String representation""" if len(self.text) > 10: - return self.toString(self.text[:7] + '...') + return self.toString(self.text[:7] + "...") else: return self.toString(self.text) class DNSService(DNSRecord): - '''A DNS service record''' + """A DNS service record""" def __init__(self, name, type, clazz, ttl, priority, weight, port, server): DNSRecord.__init__(self, name, type, clazz, ttl) @@ -435,27 +456,27 @@ class DNSService(DNSRecord): self.server = server def write(self, out): - '''Used in constructing an outgoing packet''' + """Used in constructing an outgoing packet""" out.writeShort(self.priority) out.writeShort(self.weight) out.writeShort(self.port) out.writeName(self.server) def __eq__(self, other): - '''Tests equality on priority, weight, port and server''' + """Tests equality on priority, weight, port and server""" if isinstance(other, DNSService): return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server return 0 def __repr__(self): - '''String representation''' - return self.toString('%s:%s' % (self.server, self.port)) + """String representation""" + return self.toString("%s:%s" % (self.server, self.port)) class DNSIncoming(object): - '''Object representation of an incoming DNS packet''' + """Object representation of an incoming DNS packet""" def __init__(self, data): - '''Constructor from string holding bytes of packet''' + """Constructor from string holding bytes of packet""" self.offset = 0 self.data = data self.questions = [] @@ -470,7 +491,7 @@ class DNSIncoming(object): self.readOthers() def readHeader(self): - '''Reads header portion of packet''' + """Reads header portion of packet""" format = '!HHHHHH' length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) @@ -484,7 +505,7 @@ class DNSIncoming(object): self.numAdditionals = info[5] def readQuestions(self): - '''Reads questions section of packet''' + """Reads questions section of packet""" format = '!HH' length = struct.calcsize(format) for i in range(0, self.numQuestions): @@ -492,11 +513,14 @@ class DNSIncoming(object): info = struct.unpack(format, self.data[self.offset:self.offset+length]) self.offset += length - question = DNSQuestion(name, info[0], info[1]) - self.questions.append(question) + try: + question = DNSQuestion(name, info[0], info[1]) + self.questions.append(question) + except NonLocalNameException: + pass def readInt(self): - '''Reads an integer from the packet''' + """Reads an integer from the packet""" format = '!I' length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) @@ -504,13 +528,13 @@ class DNSIncoming(object): return info[0] def readCharacterString(self): - '''Reads a character string from the packet''' + """Reads a character string from the packet""" length = ord(self.data[self.offset]) self.offset += 1 return self.readString(length) def readString(self, len): - '''Reads a string of a given length from the packet''' + """Reads a string of a given length from the packet""" format = '!' + str(len) + 's' length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) @@ -518,7 +542,7 @@ class DNSIncoming(object): return info[0] def readUnsignedShort(self): - '''Reads an unsigned short from the packet''' + """Reads an unsigned short from the packet""" format = '!H' length = struct.calcsize(format) info = struct.unpack(format, self.data[self.offset:self.offset+length]) @@ -526,7 +550,7 @@ class DNSIncoming(object): return info[0] def readOthers(self): - '''Reads the answers, authorities and additionals section of the packet''' + """Reads the answers, authorities and additionals section of the packet""" format = '!HHiH' length = struct.calcsize(format) n = self.numAnswers + self.numAuthorities + self.numAdditionals @@ -549,34 +573,26 @@ class DNSIncoming(object): elif info[0] == _TYPE_AAAA: rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16)) else: - # Try to ignore types we don't know about - # this may mean the rest of the name is - # unable to be parsed, and may show errors - # so this is left for debugging. New types - # encountered need to be parsed properly. - # - #print 'UNKNOWN TYPE = ' + str(info[0]) - #raise BadTypeInNameException - pass + # Skip unknown record type (using DNS length field) + self.offset += info[3] if rec is not None: self.answers.append(rec) def isQuery(self): - '''Returns true if this is a query''' + """Returns true if this is a query""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY def isResponse(self): - '''Returns true if this is a response''' + """Returns true if this is a response""" return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE def readUTF(self, offset, len): - '''Reads a UTF-8 string of a given length from the packet''' - result = self.data[offset:offset+len].decode('utf-8') - return result + """Reads a UTF-8 string of a given length from the packet""" + return self.data[offset:offset+len].decode('utf-8') def readName(self): - '''Reads a domain name from the packet''' + """Reads a domain name from the packet""" result = '' off = self.offset next = -1 @@ -596,11 +612,10 @@ class DNSIncoming(object): next = off + 1 off = ((len & 0x3F) << 8) | ord(self.data[off]) if off >= first: - raise ValueError('Bad domain name (circular) at ' + - str(off)) + raise BadDomainNameCircular(off) first = off else: - raise ValueError('Bad domain name at ' + str(off)) + raise BadDomainName(off) if next >= 0: self.offset = next @@ -611,7 +626,7 @@ class DNSIncoming(object): class DNSOutgoing(object): - '''Object representation of an outgoing packet''' + """Object representation of an outgoing packet""" def __init__(self, flags, multicast = 1): self.finished = 0 @@ -628,60 +643,60 @@ class DNSOutgoing(object): self.additionals = [] def addQuestion(self, record): - '''Adds a question''' + """Adds a question""" self.questions.append(record) def addAnswer(self, inp, record): - '''Adds an answer''' + """Adds an answer""" if not record.suppressedBy(inp): self.addAnswerAtTime(record, 0) def addAnswerAtTime(self, record, now): - '''Adds an answer if if does not expire by a certain time''' + """Adds an answer if if does not expire by a certain time""" if record is not None: if now == 0 or not record.isExpired(now): self.answers.append((record, now)) def addAuthorativeAnswer(self, record): - '''Adds an authoritative answer''' + """Adds an authoritative answer""" self.authorities.append(record) def addAdditionalAnswer(self, record): - '''Adds an additional answer''' + """Adds an additional answer""" self.additionals.append(record) def writeByte(self, value): - '''Writes a single byte to the packet''' + """Writes a single byte to the packet""" format = '!c' self.data.append(struct.pack(format, chr(value))) self.size += 1 def insertShort(self, index, value): - '''Inserts an unsigned short in a certain position in the packet''' + """Inserts an unsigned short in a certain position in the packet""" format = '!H' self.data.insert(index, struct.pack(format, value)) self.size += 2 def writeShort(self, value): - '''Writes an unsigned short to the packet''' + """Writes an unsigned short to the packet""" format = '!H' self.data.append(struct.pack(format, value)) self.size += 2 def writeInt(self, value): - '''Writes an unsigned integer to the packet''' + """Writes an unsigned integer to the packet""" format = '!I' - self.data.append(struct.pack(format, value)) + self.data.append(struct.pack(format, int(value))) self.size += 4 def writeString(self, value, length): - '''Writes a string to the packet''' + """Writes a string to the packet""" format = '!' + str(length) + 's' self.data.append(struct.pack(format, value)) self.size += length def writeUTF(self, s): - '''Writes a UTF-8 string of a given length to the packet''' + """Writes a UTF-8 string of a given length to the packet""" utfstr = s.encode('utf-8') length = len(utfstr) if length > 64: @@ -690,7 +705,7 @@ class DNSOutgoing(object): self.writeString(utfstr, length) def writeName(self, name): - '''Writes a domain name to the packet''' + """Writes a domain name to the packet""" try: # Find existing instance of this name in packet @@ -716,14 +731,14 @@ class DNSOutgoing(object): self.writeByte(index) def writeQuestion(self, question): - '''Writes a question to the packet''' + """Writes a question to the packet""" self.writeName(question.name) self.writeShort(question.type) self.writeShort(question.clazz) def writeRecord(self, record, now): - '''Writes a record (answer, authoritative answer, additional) to - the packet''' + """Writes a record (answer, authoritative answer, additional) to + the packet""" self.writeName(record.name) self.writeShort(record.type) if record.unique and self.multicast: @@ -745,10 +760,10 @@ class DNSOutgoing(object): self.insertShort(index, length) # Here is the short we adjusted for def packet(self): - '''Returns a string containing the packet's bytes + """Returns a string containing the packet's bytes No further parts should be added to the packet once this - is done.''' + is done.""" if not self.finished: self.finished = 1 for question in self.questions: @@ -773,13 +788,13 @@ class DNSOutgoing(object): class DNSCache(object): - '''A cache of DNS entries''' + """A cache of DNS entries""" def __init__(self): self.cache = {} def add(self, entry): - '''Adds an entry''' + """Adds an entry""" try: list = self.cache[entry.key] except: @@ -787,7 +802,7 @@ class DNSCache(object): list.append(entry) def remove(self, entry): - '''Removes an entry''' + """Removes an entry""" try: list = self.cache[entry.key] list.remove(entry) @@ -795,8 +810,8 @@ class DNSCache(object): pass def get(self, entry): - '''Gets an entry by key. Will return None if there is no - matching entry.''' + """Gets an entry by key. Will return None if there is no + matching entry.""" try: list = self.cache[entry.key] return list[list.index(entry)] @@ -804,20 +819,20 @@ class DNSCache(object): return None def getByDetails(self, name, type, clazz): - '''Gets an entry by details. Will return None if there is - no matching entry.''' + """Gets an entry by details. Will return None if there is + no matching entry.""" entry = DNSEntry(name, type, clazz) return self.get(entry) def entriesWithName(self, name): - '''Returns a list of entries whose key matches the name.''' + """Returns a list of entries whose key matches the name.""" try: return self.cache[name] except: return [] def entries(self): - '''Returns a list of all entries''' + """Returns a list of all entries""" def add(x, y): return x+y try: return reduce(add, self.cache.values()) @@ -826,7 +841,7 @@ class DNSCache(object): class Engine(threading.Thread): - '''An engine wraps read access to sockets, allowing objects that + """An engine wraps read access to sockets, allowing objects that need to receive data from sockets to be called back when the sockets are ready. @@ -835,7 +850,7 @@ class Engine(threading.Thread): Writers are not implemented here, because we only send short packets. - ''' + """ def __init__(self, zeroconf): threading.Thread.__init__(self) @@ -863,11 +878,6 @@ class Engine(threading.Thread): for socket in rr: try: self.readers[socket].handle_read() - except NonLocalNameException as err: - print err - except UnicodeDecodeError: - if DEBUG: - traceback.print_exc() except: if DEBUG: traceback.print_exc() @@ -875,7 +885,6 @@ class Engine(threading.Thread): pass def getReaders(self): - result = [] self.condition.acquire() result = self.readers.keys() self.condition.release() @@ -899,12 +908,12 @@ class Engine(threading.Thread): self.condition.release() class Listener(object): - '''A Listener is used by this module to listen on the multicast + """A Listener is used by this module to listen on the multicast group to which DNS messages are sent, allowing the implementation to cache information as it arrives. It requires registration with an Engine object in order to have - the read() method called when a socket is availble for reading.''' + the read() method called when a socket is availble for reading.""" def __init__(self, zeroconf): self.zeroconf = zeroconf @@ -930,13 +939,13 @@ class Listener(object): class Reaper(threading.Thread): - '''A Reaper is used by this module to remove cache entries that - have expired.''' + """A Reaper is used by this module to remove cache entries that + have expired.""" def __init__(self, zeroconf): threading.Thread.__init__(self) + self.setDaemon(True) # By Kovid self.zeroconf = zeroconf - self.setDaemon(True) self.start() def run(self): @@ -956,14 +965,14 @@ class Reaper(threading.Thread): class ServiceBrowser(threading.Thread): - '''Used to browse for a service of a specific type. + """Used to browse for a service of a specific type. The listener object will have its addService() and removeService() methods called when this browser - discovers changes in the services availability.''' + discovers changes in the services availability.""" def __init__(self, zeroconf, type, listener): - '''Creates a browser for a specific type''' + """Creates a browser for a specific type""" threading.Thread.__init__(self) self.zeroconf = zeroconf self.type = type @@ -979,9 +988,9 @@ class ServiceBrowser(threading.Thread): self.start() def updateRecord(self, zeroconf, now, record): - '''Callback invoked by Zeroconf when new information arrives. + """Callback invoked by Zeroconf when new information arrives. - Updates information required by browser in the Zeroconf cache.''' + Updates information required by browser in the Zeroconf cache.""" if record.type == _TYPE_PTR and record.name == self.type: expired = record.isExpired(now) try: @@ -1035,11 +1044,10 @@ class ServiceBrowser(threading.Thread): class ServiceInfo(object): - '''Service information''' + """Service information""" - def __init__(self, type, name, address=None, port=None, weight=0, - priority=0, properties=None, server=None): - '''Create a service description. + def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None): + """Create a service description. type: fully qualified service type name name: fully qualified service name @@ -1048,13 +1056,15 @@ class ServiceInfo(object): weight: weight of the service priority: priority of the service properties: dictionary of properties (or a string holding the bytes for the text field) - server: fully qualified name for service host (defaults to name)''' + server: fully qualified name for service host (defaults to name)""" if not name.endswith(type): raise BadTypeInNameException self.type = type self.name = name self.address = address + if address: + self.ip_type = address_type(address) self.port = port self.weight = weight self.priority = priority @@ -1065,7 +1075,7 @@ class ServiceInfo(object): self.setProperties(properties) def setProperties(self, properties): - '''Sets properties and text of this info from a dictionary''' + """Sets properties and text of this info from a dictionary""" if isinstance(properties, dict): self.properties = properties list = [] @@ -1073,16 +1083,13 @@ class ServiceInfo(object): for key in properties: value = properties[key] if value is None: - suffix = ''.encode('utf-8') + suffix = '' elif isinstance(value, str): - suffix = value.encode('utf-8') + suffix = value elif isinstance(value, int): - if value: - suffix = 'true' - else: - suffix = 'false' + suffix = value and 'true' or 'false' else: - suffix = ''.encode('utf-8') + suffix = '' list.append('='.join((key, suffix))) for item in list: result = ''.join((result, struct.pack('!c', chr(len(item))), item)) @@ -1091,7 +1098,7 @@ class ServiceInfo(object): self.text = properties def setText(self, text): - '''Sets properties and text given a text field''' + """Sets properties and text given a text field""" self.text = text try: result = {} @@ -1128,66 +1135,63 @@ class ServiceInfo(object): self.properties = None def getType(self): - '''Type accessor''' + """Type accessor""" return self.type def getName(self): - '''Name accessor''' - if self.type is not None and self.name.endswith('.' + self.type): + """Name accessor""" + if self.type is not None and self.name.endswith("." + self.type): return self.name[:len(self.name) - len(self.type) - 1] return self.name def getAddress(self): - '''Address accessor''' + """Address accessor""" return self.address def getPort(self): - '''Port accessor''' + """Port accessor""" return self.port def getPriority(self): - '''Pirority accessor''' + """Pirority accessor""" return self.priority def getWeight(self): - '''Weight accessor''' + """Weight accessor""" return self.weight def getProperties(self): - '''Properties accessor''' + """Properties accessor""" return self.properties def getText(self): - '''Text accessor''' + """Text accessor""" return self.text def getServer(self): - '''Server accessor''' + """Server accessor""" return self.server def updateRecord(self, zeroconf, now, record): - '''Updates service information from a DNS record''' - if record is not None and not record.isExpired(now): - if record.type == _TYPE_A: - #if record.name == self.name: - if record.name == self.server: - self.address = record.address - elif record.type == _TYPE_SRV: - if record.name == self.name: - self.server = record.server - self.port = record.port - self.weight = record.weight - self.priority = record.priority - #self.address = None - self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) - elif record.type == _TYPE_TXT: - if record.name == self.name: - self.setText(record.text) + """Updates service information from a DNS record""" + if record is None or record.isExpired(now): + return + if (record.type in (_TYPE_A, _TYPE_AAAA) and + record.name == self.server): + self.address = record.address + elif record.type == _TYPE_SRV and record.name == self.name: + self.server = record.server + self.port = record.port + self.weight = record.weight + self.priority = record.priority + self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN)) + elif record.type == _TYPE_TXT and record.name == self.name: + self.setText(record.text) def request(self, zeroconf, timeout): - '''Returns true if the service could be discovered on the + """Returns true if the service could be discovered on the network, and updates this object with details discovered. - ''' + """ now = currentTimeMillis() delay = _LISTENER_TIME next = now + delay @@ -1220,37 +1224,37 @@ class ServiceInfo(object): return result def __eq__(self, other): - '''Tests equality of service name''' + """Tests equality of service name""" if isinstance(other, ServiceInfo): return other.name == self.name return 0 def __ne__(self, other): - '''Non-equality test''' + """Non-equality test""" return not self.__eq__(other) def __repr__(self): - '''String representation''' - result = 'service[%s,%s:%s,' % (self.name, socket.inet_ntoa(self.getAddress()), self.port) + """String representation""" + result = "service[%s,%s:%s," % (self.name, ntop(self.getAddress()), self.port) if self.text is None: - result += 'None' + result += "None" else: if len(self.text) < 20: result += self.text else: - result += self.text[:17] + '...' - result += ']' + result += self.text[:17] + "..." + result += "]" return result class Zeroconf(object): - '''Implementation of Zeroconf Multicast DNS Service Discovery + """Implementation of Zeroconf Multicast DNS Service Discovery Supports registration, unregistration, queries and browsing. - ''' + """ def __init__(self, bindaddress=None): - '''Creates an instance of the Zeroconf class, establishing - multicast communications, listening and reaping threads.''' + """Creates an instance of the Zeroconf class, establishing + multicast communications, listening and reaping threads.""" globals()['_GLOBAL_DONE'] = 0 if bindaddress is None: self.intf = socket.gethostbyname(socket.gethostname()) @@ -1263,8 +1267,8 @@ class Zeroconf(object): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except: # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, 'TCP/IP Illustrated, - # Volume 2'), but some BSD-derived systems require + # multicast UDP sockets (p 731, "TCP/IP Illustrated, + # Volume 2"), but some BSD-derived systems require # SO_REUSEPORT to be specified explicity. Also, not all # versions of Python have SO_REUSEPORT available. So # if you're on a BSD-based system, and haven't upgraded @@ -1298,52 +1302,52 @@ class Zeroconf(object): self.reaper = Reaper(self) def isLoopback(self): - return self.intf.startswith('127.0.0.1') + return self.intf.startswith("127.0.0.1") def isLinklocal(self): - return self.intf.startswith('169.254.') + return self.intf.startswith("169.254.") def wait(self, timeout): - '''Calling thread waits for a given number of milliseconds or - until notified.''' + """Calling thread waits for a given number of milliseconds or + until notified.""" self.condition.acquire() self.condition.wait(timeout/1000) self.condition.release() def notifyAll(self): - '''Notifies all waiting threads''' + """Notifies all waiting threads""" self.condition.acquire() self.condition.notifyAll() self.condition.release() def getServiceInfo(self, type, name, timeout=3000): - '''Returns network's service information for a particular + """Returns network's service information for a particular name and type, or None if no service matches by the timeout, - which defaults to 3 seconds.''' + which defaults to 3 seconds.""" info = ServiceInfo(type, name) if info.request(self, timeout): return info return None def addServiceListener(self, type, listener): - '''Adds a listener for a particular service type. This object + """Adds a listener for a particular service type. This object will then have its updateRecord method called when information - arrives for that type.''' + arrives for that type.""" self.removeServiceListener(listener) self.browsers.append(ServiceBrowser(self, type, listener)) def removeServiceListener(self, listener): - '''Removes a listener from the set that is currently listening.''' + """Removes a listener from the set that is currently listening.""" for browser in self.browsers: if browser.listener == listener: browser.cancel() del(browser) def registerService(self, info, ttl=_DNS_TTL): - '''Registers service information to the network with a default TTL + """Registers service information to the network with a default TTL of 60 seconds. Zeroconf will then respond to requests for information for that service. The name of the service may be - changed if needed to make it unique on the network.''' + changed if needed to make it unique on the network.""" self.checkService(info) self.services[info.name.lower()] = info if self.servicetypes.has_key(info.type): @@ -1363,13 +1367,13 @@ class Zeroconf(object): out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0) out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0) if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0) + out.addAnswerAtTime(DNSAddress(info.server, info.ip_type, _CLASS_IN, ttl, info.address), 0) self.send(out) i += 1 nextTime += _REGISTER_TIME def unregisterService(self, info): - '''Unregister a service.''' + """Unregister a service.""" try: del(self.services[info.name.lower()]) if self.servicetypes[info.type]>1: @@ -1391,36 +1395,37 @@ class Zeroconf(object): out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0) out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0) + out.addAnswerAtTime(DNSAddress(info.server, info.ip_type, _CLASS_IN, 0, info.address), 0) self.send(out) i += 1 nextTime += _UNREGISTER_TIME def unregisterAllServices(self): - '''Unregister all registered services.''' - if len(self.services) > 0: - now = currentTimeMillis() - nextTime = now - i = 0 - while i < 3: - if now < nextTime: - self.wait(nextTime - now) - now = currentTimeMillis() - continue - out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - for info in self.services.values(): - out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) - out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0) - out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) - if info.address: - out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0) - self.send(out) - i += 1 - nextTime += _UNREGISTER_TIME + """Unregister all registered services.""" + if not self.services: + return + now = currentTimeMillis() + nextTime = now + i = 0 + while i < 3: + if now < nextTime: + self.wait(nextTime - now) + now = currentTimeMillis() + continue + out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) + for info in self.services.values(): + out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0) + out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0) + out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0) + if info.address: + out.addAnswerAtTime(DNSAddress(info.server, info.ip_type, _CLASS_IN, 0, info.address), 0) + self.send(out) + i += 1 + nextTime += _UNREGISTER_TIME def checkService(self, info): - '''Checks the network for a unique service name, modifying the - ServiceInfo passed in if it is not unique.''' + """Checks the network for a unique service name, modifying the + ServiceInfo passed in if it is not unique.""" now = currentTimeMillis() nextTime = now i = 0 @@ -1428,7 +1433,7 @@ class Zeroconf(object): for record in self.cache.entriesWithName(info.type): if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name: if (info.name.find('.') < 0): - info.name = info.name + '.[' + info.address + ':' + info.port + '].' + info.type + info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type self.checkService(info) return raise NonUniqueNameException @@ -1445,9 +1450,9 @@ class Zeroconf(object): nextTime += _CHECK_TIME def addListener(self, listener, question): - '''Adds a listener for a given question. The listener will have + """Adds a listener for a given question. The listener will have its updateRecord method called when information is available to - answer the question.''' + answer the question.""" now = currentTimeMillis() self.listeners.append(listener) if question is not None: @@ -1457,7 +1462,7 @@ class Zeroconf(object): self.notifyAll() def removeListener(self, listener): - '''Removes a listener.''' + """Removes a listener.""" try: self.listeners.remove(listener) self.notifyAll() @@ -1465,15 +1470,15 @@ class Zeroconf(object): pass def updateRecord(self, now, rec): - '''Used to notify listeners of new information that has updated - a record.''' + """Used to notify listeners of new information that has updated + a record.""" for listener in self.listeners: listener.updateRecord(self, now, rec) self.notifyAll() def handleResponse(self, msg): - '''Deal with incoming response packets. All answers - are held in the cache, and listeners are notified.''' + """Deal with incoming response packets. All answers + are held in the cache, and listeners are notified.""" now = currentTimeMillis() for record in msg.answers: expired = record.isExpired(now) @@ -1491,8 +1496,8 @@ class Zeroconf(object): self.updateRecord(now, record) def handleQuery(self, msg, addr, port): - '''Deal with incoming query packets. Provides a response if - possible.''' + """Deal with incoming query packets. Provides a response if + possible.""" out = None # Support unicast client responses @@ -1504,11 +1509,11 @@ class Zeroconf(object): for question in msg.questions: if question.type == _TYPE_PTR: - if question.name == '_services._dns-sd._udp.local.': + if question.name == "_services._dns-sd._udp.local.": for stype in self.servicetypes.keys(): if out is None: out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) - out.addAnswer(msg, DNSPointer('_services._dns-sd._udp.local.', _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype)) + out.addAnswer(msg, DNSPointer("_services._dns-sd._udp.local.", _TYPE_PTR, _CLASS_IN, _DNS_TTL, stype)) for service in self.services.values(): if question.name == service.type: if out is None: @@ -1520,10 +1525,10 @@ class Zeroconf(object): out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA) # Answer A record queries for any service addresses we know - if question.type == _TYPE_A or question.type == _TYPE_ANY: + if question.type in (_TYPE_A, _TYPE_AAAA, _TYPE_ANY): for service in self.services.values(): if service.server == question.name.lower(): - out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + out.addAnswer(msg, DNSAddress(question.name, address_type(service.address), _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) service = self.services.get(question.name.lower(), None) if not service: continue @@ -1533,7 +1538,7 @@ class Zeroconf(object): if question.type == _TYPE_TXT or question.type == _TYPE_ANY: out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) if question.type == _TYPE_SRV: - out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) + out.addAdditionalAnswer(DNSAddress(service.server, address_type(service.address), _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) except: traceback.print_exc() @@ -1542,7 +1547,7 @@ class Zeroconf(object): self.send(out, addr, port) def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT): - '''Sends an outgoing packet.''' + """Sends an outgoing packet.""" # This is a quick test to see if we can parse the packets we generate #temp = DNSIncoming(out.packet()) try: @@ -1552,8 +1557,8 @@ class Zeroconf(object): pass def close(self): - '''Ends the background threads, and prevent this instance from - servicing further queries.''' + """Ends the background threads, and prevent this instance from + servicing further queries.""" if globals()['_GLOBAL_DONE'] == 0: globals()['_GLOBAL_DONE'] = 1 self.notifyAll() @@ -1566,21 +1571,21 @@ class Zeroconf(object): # query (for Zoe), and service unregistration. if __name__ == '__main__': - print 'Multicast DNS Service Discovery for Python, version', __version__ + print "Multicast DNS Service Discovery for Python, version", __version__ r = Zeroconf() - print '1. Testing registration of a service...' + print "1. Testing registration of a service..." desc = {'version':'0.10','a':'test value', 'b':'another value'} - info = ServiceInfo('_http._tcp.local.', 'My Service Name._http._tcp.local.', socket.inet_aton('127.0.0.1'), 1234, 0, 0, desc) - print ' Registering service...' + info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc) + print " Registering service..." r.registerService(info) - print ' Registration done.' - print '2. Testing query of service information...' - print ' Getting ZOE service:', str(r.getServiceInfo('_http._tcp.local.', 'ZOE._http._tcp.local.')) - print ' Query done.' - print '3. Testing query of own service...' - print ' Getting self:', str(r.getServiceInfo('_http._tcp.local.', 'My Service Name._http._tcp.local.')) - print ' Query done.' - print '4. Testing unregister of service information...' + print " Registration done." + print "2. Testing query of service information..." + print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local.")) + print " Query done." + print "3. Testing query of own service..." + print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.")) + print " Query done." + print "4. Testing unregister of service information..." r.unregisterService(info) - print ' Unregister done.' + print " Unregister done." r.close() From 385b4472ca9f9b2dee869e6fa69e038ea8ea9a5f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 10:15:19 -0600 Subject: [PATCH 07/21] Dilema Veche by Silviu Cotoara --- recipes/dilemaveche.recipe | 55 ++++++++++++++++++++++++++++++++++ recipes/icons/dilemaveche.png | Bin 0 -> 558 bytes 2 files changed, 55 insertions(+) create mode 100644 recipes/dilemaveche.recipe create mode 100644 recipes/icons/dilemaveche.png diff --git a/recipes/dilemaveche.recipe b/recipes/dilemaveche.recipe new file mode 100644 index 0000000000..0d5013b287 --- /dev/null +++ b/recipes/dilemaveche.recipe @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2011, Silviu Cotoar\u0103' +''' +dilemaveche.ro +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class DilemaVeche(BasicNewsRecipe): + title = u'Dilema Veche' + __author__ = u'Silviu Cotoar\u0103' + description = u'Sunt vechi, domnule!' + publisher = u'Dilema Veche' + oldest_article = 50 + language = 'ro' + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + category = 'Ziare' + encoding = 'utf-8' + cover_url = 'http://www.dilemaveche.ro/sites/all/themes/dilema/theme/dilema_two/layouter/dilema_two_homepage/logo.png' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [ + dict(name='h1', attrs={'class':'art_title'}) + , dict(name='h1', attrs={'class':'art_title online'}) + , dict(name='div', attrs={'class':'item'}) + , dict(name='div', attrs={'class':'art_content'}) + ] + + remove_tags = [ + dict(name='div', attrs={'class':['article_details']}) + , dict(name='div', attrs={'class':['controale']}) + , dict(name='div', attrs={'class':['art_related_left']}) + ] + + remove_tags_after = [ + dict(name='div', attrs={'class':['article_details']}) + ] + + feeds = [ + (u'Feeds', u'http://www.dilemaveche.ro/rss.xml') + ] + + def preprocess_html(self, soup): + return self.adeify_images(soup) diff --git a/recipes/icons/dilemaveche.png b/recipes/icons/dilemaveche.png new file mode 100644 index 0000000000000000000000000000000000000000..dd29eb828630ad781c551d7a1e1a41728012261a GIT binary patch literal 558 zcmV+}0@3}6P)USUwL&buqgp>{lcBHPM zQW-K-e1LoeONFLRn#O4xIerXH-IO}q?cVq9x#t|+E6#>PwbN+ec^;akaTQ8IGMU7( zEEXOtP*_|fl}hn%2-kJh0D>SO zoldi~v=qHaN-0d!L`sR{I7lf&2|@_^Cnu~_Dt!6&bvA=uuZIc(5{U#th)}=hdEC$C zcvs&<)3j&+fLJUBu~?V~L4Y?Hkh5(*?=;Bg^8n~dDF&yf^!xqDV^R~tQmGUxl?t`@ wb!^+dW;^Sgue#$#9BY1_VzJ1>LgChcze@7QSRIV02LJ#707*qoM6N<$f|jiQH~;_u literal 0 HcmV?d00001 From b054dd5d9f2bae24a28eeb64a1d4f624b42638c9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 10:24:45 -0600 Subject: [PATCH 08/21] Fix menubar on OS X --- src/calibre/gui2/bars.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index d586bfe1c3..7dc0567d95 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -246,6 +246,8 @@ class BarsManager(QObject): self.main_bars = tuple(bars[:2]) self.child_bars = tuple(bars[2:]) + self.menu_bar = MenuBar(self.location_manager, self.parent()) + self.parent().setMenuBar(self.menu_bar) self.apply_settings() self.init_bars() @@ -295,11 +297,9 @@ class BarsManager(QObject): if child_bar.added_actions: child_bar.setVisible(True) - self.menu_bar = MenuBar(self.location_manager, self.parent()) self.menu_bar.init_bar(self.bar_actions[4 if showing_device else 3]) self.menu_bar.update_lm_actions() self.menu_bar.setVisible(bool(self.menu_bar.added_actions)) - self.parent().setMenuBar(self.menu_bar) def apply_settings(self): sz = gprefs['toolbar_icon_size'] From 697b535f97b9257fb5c3276e3b01e95edbc4d48a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 11:24:00 -0600 Subject: [PATCH 09/21] When deleting large numbers of books, give the user the option to skip the Recycle Bin, since sending lots of files to the recycle bin can be very slow. Fixes #784987 (When deleting books the processor goes to 100%) --- src/calibre/gui2/actions/delete.py | 15 +++++++++++++-- src/calibre/library/database2.py | 6 +++--- src/calibre/utils/recycle_bin.py | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index a1fddd84b8..718f0737b3 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -9,11 +9,12 @@ from functools import partial from PyQt4.Qt import QMenu, QObject, QTimer -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, question_dialog from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete_location import confirm_location from calibre.gui2.actions import InterfaceAction +from calibre.utils.recycle_bin import can_recycle single_shot = partial(QTimer.singleShot, 10) @@ -24,6 +25,15 @@ class MultiDeleter(QObject): QObject.__init__(self, gui) self.model = gui.library_view.model() self.ids = ids + self.permanent = False + if can_recycle and len(ids) > 100: + if question_dialog(gui, _('Are you sure?'), '

'+ + _('You are trying to delete %d books. ' + 'Sending so many files to the Recycle' + ' Bin can be slow. Should calibre skip the' + ' Recycle Bin? If you click Yes the files' + ' will be permanently deleted.')%len(ids)): + self.permanent = True self.gui = gui self.failures = [] self.deleted_ids = [] @@ -44,7 +54,8 @@ class MultiDeleter(QObject): title_ = self.model.db.title(id_, index_is_id=True) if title_: title = title_ - self.model.db.delete_book(id_, notify=False, commit=False) + self.model.db.delete_book(id_, notify=False, commit=False, + permanent=self.permanent) self.deleted_ids.append(id_) except: import traceback diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8ac33d4b40..9a740a08b7 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1145,7 +1145,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) return True - def delete_book(self, id, notify=True, commit=True): + def delete_book(self, id, notify=True, commit=True, permanent=False): ''' Removes book from the result cache and the underlying database. If you set commit to False, you must call clean() manually afterwards @@ -1155,10 +1155,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: path = None if path and os.path.exists(path): - self.rmtree(path) + self.rmtree(path, permanent=permanent) parent = os.path.dirname(path) if len(os.listdir(parent)) == 0: - self.rmtree(parent) + self.rmtree(parent, permanent=permanent) self.conn.execute('DELETE FROM books WHERE id=?', (id,)) if commit: self.conn.commit() diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py index df6016d796..7d3d268553 100644 --- a/src/calibre/utils/recycle_bin.py +++ b/src/calibre/utils/recycle_bin.py @@ -24,6 +24,7 @@ elif isosx: path = path.decode(filesystem_encoding) u.send2trash(path) +can_recycle = callable(recycle) def delete_file(path): if callable(recycle): From 27dbb4421466596202137f4555b4688415fdb1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Thu, 19 May 2011 20:51:35 +0200 Subject: [PATCH 10/21] fix kath.net recipe --- recipes/kath_net.recipe | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/recipes/kath_net.recipe b/recipes/kath_net.recipe index 933876c2a8..3b883fde54 100644 --- a/recipes/kath_net.recipe +++ b/recipes/kath_net.recipe @@ -3,8 +3,9 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1295262156(BasicNewsRecipe): title = u'kath.net' __author__ = 'Bobus' + description = u'Katholische Nachrichten' oldest_article = 7 - language = 'en' + language = 'de' max_articles_per_feed = 100 feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')] From da5a1a1b22a24df35d4156e2eafc481036e6d267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Thu, 19 May 2011 22:37:30 +0200 Subject: [PATCH 11/21] Polish stores --- src/calibre/customize/builtins.py | 10 ++- src/calibre/gui2/store/gandalf_plugin.py | 78 ++++++++++++++++++++++++ src/calibre/gui2/store/nexto_plugin.py | 1 + 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/store/gandalf_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index b2d3a967df..5c93711e7d 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1164,6 +1164,12 @@ class StoreFoylesUKStore(StoreBase): description = _('Foyles of London, online.') actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore' +class StoreGandalfStore(StoreBase): + name = 'Gandalf' + author = 'Tomasz Długosz' + description = _('Zaczarowany świat książek') + actual_plugin = 'calibre.gui2.store.gandalf_plugin:GandalfStore' + class StoreGoogleBooksStore(StoreBase): name = 'Google Books' description = _('Google Books') @@ -1191,6 +1197,7 @@ class StoreMobileReadStore(StoreBase): class StoreNextoStore(StoreBase): name = 'Nexto' + author = 'Tomasz Długosz' description = _('Audiobooki mp3, ebooki, prasa - księgarnia internetowa.') actual_plugin = 'calibre.gui2.store.nexto_plugin:NextoStore' @@ -1229,7 +1236,8 @@ plugins += [StoreArchiveOrgStore, StoreAmazonKindleStore, StoreAmazonDEKindleSto StoreBeamEBooksDEStore, StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, StoreEPubBuyDEStore, StoreEHarlequinStore, StoreFeedbooksStore, - StoreFoylesUKStore, StoreGoogleBooksStore, StoreGutenbergStore, + StoreFoylesUKStore, StoreGandalfStore, + StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, StoreOReillyStore, StoreSmashwordsStore, diff --git a/src/calibre/gui2/store/gandalf_plugin.py b/src/calibre/gui2/store/gandalf_plugin.py new file mode 100644 index 0000000000..192484d764 --- /dev/null +++ b/src/calibre/gui2/store/gandalf_plugin.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, Tomasz Długosz ' +__docformat__ = 'restructuredtext en' + +import re +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class GandalfStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://www.gandalf.com.pl/ebooks/' + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.gandalf.com.pl/s/' + values={ + 'search': query, + 'dzialx':'11' + } + + br = browser() + + counter = max_results + with closing(br.open(url, data=urllib.urlencode(values), timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="box"]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[@class="info"]/h3/a/@href')) + if not id: + continue + + cover_url = ''.join(data.xpath('.//img/@src')) + title = ''.join(data.xpath('.//div[@class="info"]/h3/a/@title')) + temp = title.split() + title = ' '.join(temp[0:-1]) + formats = temp[-1] + author = ''.join(data.xpath('.//div[@class="info"]/h4/text()')) + price = ''.join(data.xpath('.//h3[@class="promocja"]/text()')) + price = re.sub('PLN', 'zł', price) + price = re.sub('\.', ',', price) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = id.strip() + s.drm = SearchResult.DRM_UNKNOWN + s.formats = formats.upper().strip() + + yield s diff --git a/src/calibre/gui2/store/nexto_plugin.py b/src/calibre/gui2/store/nexto_plugin.py index d63cf18233..0009f39b1b 100644 --- a/src/calibre/gui2/store/nexto_plugin.py +++ b/src/calibre/gui2/store/nexto_plugin.py @@ -63,6 +63,7 @@ class NextoStore(BasicStoreConfig, StorePlugin): cover_url = ''.join(data.xpath('.//img[@class="cover"]/@src')) title = ''.join(data.xpath('.//a[@class="title"]/text()')) + title = re.sub(r' - ebook$', '', title) formats = ', '.join(data.xpath('.//ul[@class="formats_available"]/li//b/text()')) DrmFree = re.search(r'bez.DRM', formats) formats = re.sub(r'\(.+\)', '', formats) From 4257cf6e3b88123bee053288e4d4e0edaa2e471a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Thu, 19 May 2011 22:56:28 +0200 Subject: [PATCH 12/21] improved Gandlf recipe --- src/calibre/gui2/store/gandalf_plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/store/gandalf_plugin.py b/src/calibre/gui2/store/gandalf_plugin.py index 192484d764..de8896428d 100644 --- a/src/calibre/gui2/store/gandalf_plugin.py +++ b/src/calibre/gui2/store/gandalf_plugin.py @@ -56,10 +56,9 @@ class GandalfStore(BasicStoreConfig, StorePlugin): cover_url = ''.join(data.xpath('.//img/@src')) title = ''.join(data.xpath('.//div[@class="info"]/h3/a/@title')) - temp = title.split() - title = ' '.join(temp[0:-1]) - formats = temp[-1] - author = ''.join(data.xpath('.//div[@class="info"]/h4/text()')) + formats = title.split() + formats = formats[-1] + author = ''.join(data.xpath('.//div[@class="info"]/h4/text() | .//div[@class="info"]/h4/span/text()')) price = ''.join(data.xpath('.//h3[@class="promocja"]/text()')) price = re.sub('PLN', 'zł', price) price = re.sub('\.', ',', price) From 31197146b469350ba77cfd1abd8fd5fcc1f2522f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 15:10:18 -0600 Subject: [PATCH 13/21] Updated GoComics --- recipes/go_comics.recipe | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/recipes/go_comics.recipe b/recipes/go_comics.recipe index ac2c429931..a30ae1e94d 100644 --- a/recipes/go_comics.recipe +++ b/recipes/go_comics.recipe @@ -6,13 +6,13 @@ __copyright__ = 'Copyright 2010 Starson17' www.gocomics.com ''' from calibre.web.feeds.news import BasicNewsRecipe -import mechanize +import mechanize, re class GoComics(BasicNewsRecipe): title = 'GoComics' __author__ = 'Starson17' - __version__ = '1.03' - __date__ = '09 October 2010' + __version__ = '1.05' + __date__ = '19 may 2011' description = u'200+ Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.' category = 'news, comics' language = 'en' @@ -20,6 +20,7 @@ class GoComics(BasicNewsRecipe): no_stylesheets = True remove_javascript = True cover_url = 'http://paulbuckley14059.files.wordpress.com/2008/06/calvin-and-hobbes.jpg' + remove_attributes = ['style'] ####### USER PREFERENCES - COMICS, IMAGE SIZE AND NUMBER OF COMICS TO RETRIEVE ######## # num_comics_to_get - I've tried up to 99 on Calvin&Hobbes @@ -40,6 +41,8 @@ class GoComics(BasicNewsRecipe): remove_tags = [dict(name='a', attrs={'class':['beginning','prev','cal','next','newest']}), dict(name='div', attrs={'class':['tag-wrapper']}), + dict(name='a', attrs={'href':re.compile(r'.*mutable_[0-9]+', re.IGNORECASE)}), + dict(name='img', attrs={'src':re.compile(r'.*mutable_[0-9]+', re.IGNORECASE)}), dict(name='ul', attrs={'class':['share-nav','feature-nav']}), ] From 51146643c9d2432ff937748c38493ee63e773847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Thu, 19 May 2011 23:34:29 +0200 Subject: [PATCH 14/21] fix Gandalf's encoding --- src/calibre/gui2/store/gandalf_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/gandalf_plugin.py b/src/calibre/gui2/store/gandalf_plugin.py index de8896428d..4bd8e9e747 100644 --- a/src/calibre/gui2/store/gandalf_plugin.py +++ b/src/calibre/gui2/store/gandalf_plugin.py @@ -37,7 +37,7 @@ class GandalfStore(BasicStoreConfig, StorePlugin): def search(self, query, max_results=10, timeout=60): url = 'http://www.gandalf.com.pl/s/' values={ - 'search': query, + 'search': query.encode('iso8859_2'), 'dzialx':'11' } From bd781f047ce716bdf2d045c310f2d4cf9bb185c7 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 19 May 2011 18:43:14 -0400 Subject: [PATCH 15/21] Store: Add Pragmatic Bookshelf store. --- src/calibre/customize/builtins.py | 46 +++++++--- .../gui2/store/pragmatic_bookshelf_plugin.py | 84 +++++++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 src/calibre/gui2/store/pragmatic_bookshelf_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 5c93711e7d..ac26544207 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1211,6 +1211,11 @@ class StoreOReillyStore(StoreBase): description = _('DRM-Free tech ebooks.') actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore' +class StorePragmaticBookshelfStore(StoreBase): + name = 'Pragmatic Bookshelf' + description = _('The Pragmatic Bookshelf') + actual_plugin = 'calibre.gui2.store.pragmatic_bookshelf_plugin:PragmaticBookshelfStore' + class StoreSmashwordsStore(StoreBase): name = 'Smashwords' description = _('Your ebook. Your way.') @@ -1231,16 +1236,35 @@ class StoreWizardsTowerBooksStore(StoreBase): description = 'Wizard\'s Tower Press.' actual_plugin = 'calibre.gui2.store.wizards_tower_books_plugin:WizardsTowerBooksStore' -plugins += [StoreArchiveOrgStore, StoreAmazonKindleStore, StoreAmazonDEKindleStore, - StoreAmazonUKKindleStore, StoreBaenWebScriptionStore, StoreBNStore, - StoreBeamEBooksDEStore, StoreBeWriteStore, - StoreDieselEbooksStore, StoreEbookscomStore, StoreEPubBuyDEStore, - StoreEHarlequinStore, StoreFeedbooksStore, - StoreFoylesUKStore, StoreGandalfStore, - StoreGoogleBooksStore, StoreGutenbergStore, - StoreKoboStore, StoreManyBooksStore, - StoreMobileReadStore, StoreNextoStore, StoreOpenLibraryStore, - StoreOReillyStore, StoreSmashwordsStore, - StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore] +plugins += [ + StoreArchiveOrgStore, + StoreAmazonKindleStore, + StoreAmazonDEKindleStore, + StoreAmazonUKKindleStore, + StoreBaenWebScriptionStore, + StoreBNStore, + StoreBeamEBooksDEStore, + StoreBeWriteStore, + StoreDieselEbooksStore, + StoreEbookscomStore, + StoreEPubBuyDEStore, + StoreEHarlequinStore, + StoreFeedbooksStore, + StoreFoylesUKStore, + StoreGandalfStore, + StoreGoogleBooksStore, + StoreGutenbergStore, + StoreKoboStore, + StoreManyBooksStore, + StoreMobileReadStore, + StoreNextoStore, + StoreOpenLibraryStore, + StoreOReillyStore, + StorePragmaticBookshelfStore, + StoreSmashwordsStore, + StoreWaterstonesUKStore, + StoreWeightlessBooksStore, + StoreWizardsTowerBooksStore +] # }}} diff --git a/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py b/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py new file mode 100644 index 0000000000..9521fbdb91 --- /dev/null +++ b/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class PragmaticBookshelfStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://weightlessbooks.com/' + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + ''' + OPDS based search. + + We really should get the catelog from http://pragprog.com/catalog.opds + and look for the application/opensearchdescription+xml entry. + Then get the opensearch description to get the search url and + format. However, we are going to be lazy and hard code it. + ''' + url = 'http://pragprog.com/catalog/search?q=' + urllib.quote_plus(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + # Use html instead of etree as html allows us + # to ignore the namespace easily. + doc = html.fromstring(f.read()) + for data in doc.xpath('//entry'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/acquisition/buy"]/@href')) + if not id: + continue + + price = ''.join(data.xpath('.//price/@currencycode')).strip() + price += ' ' + price += ''.join(data.xpath('.//price/text()')).strip() + if not price.strip(): + continue + + cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/cover"]/@href')) + + title = ''.join(data.xpath('.//title/text()')) + author = ''.join(data.xpath('.//author//text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = id.strip() + s.drm = SearchResult.DRM_UNLOCKED + s.formats = 'EPUB, PDF, MOBI' + + yield s From 2257b0b942722467321eb05aecf0b36d9f1ec68a Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 19 May 2011 18:45:26 -0400 Subject: [PATCH 16/21] Store: Fix Pragmatic Bookshelf url. --- src/calibre/gui2/store/pragmatic_bookshelf_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py b/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py index 9521fbdb91..f3803bbcea 100644 --- a/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py +++ b/src/calibre/gui2/store/pragmatic_bookshelf_plugin.py @@ -23,7 +23,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog class PragmaticBookshelfStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): - url = 'http://weightlessbooks.com/' + url = 'http://pragprog.com/' if external or self.config.get('open_external', False): open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) From 5c893aacb03a2fa1a008d8fa157dd40fdc5d841f Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 19 May 2011 18:48:51 -0400 Subject: [PATCH 17/21] Store: Change default for open external in search dialog. --- src/calibre/gui2/store/search/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index e0d0251f98..f9ac45e707 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -190,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog): else: self.resize_columns() - self.open_external.setChecked(self.config.get('open_external', False)) + self.open_external.setChecked(self.config.get('open_external', True)) store_check = self.config.get('store_checked', None) if store_check: From c01627b64eddf76848c75cb5af9a74d5a1efd725 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 17:33:37 -0600 Subject: [PATCH 18/21] More OS X hoops --- src/calibre/gui2/bars.py | 7 +++++++ src/calibre/gui2/layout.py | 4 ---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index 7dc0567d95..58711f9096 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -249,6 +249,10 @@ class BarsManager(QObject): self.menu_bar = MenuBar(self.location_manager, self.parent()) self.parent().setMenuBar(self.menu_bar) + parent.addToolBar(Qt.TopToolBarArea, self.main_bars[0]) + parent.addToolBar(Qt.BottomToolBarArea, self.main_bars[1]) + parent.addToolBar(Qt.BottomToolBarArea, self.child_bars[0]) + self.apply_settings() self.init_bars() @@ -288,12 +292,15 @@ class BarsManager(QObject): ''' showing_device = self.location_manager.has_device main_bar = self.main_bars[1 if showing_device else 0] + hidden_bar = self.main_bars[0 if showing_device else 1] + self.parent().addToolBar(Qt.BottomToolBarArea, hidden_bar) child_bar = self.child_bars[0] for bar in self.bars: bar.setVisible(False) bar.update_lm_actions() if main_bar.added_actions: main_bar.setVisible(True) + self.parent().addToolBar(Qt.TopToolBarArea, main_bar) if child_bar.added_actions: child_bar.setVisible(True) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 46b6356a6e..85e79d66d3 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -259,10 +259,6 @@ class MainWindowMixin(object): # {{{ self.search_bar = SearchBar(self) self.bars_manager = BarsManager(self.donate_button, self.location_manager, self) - for bar in self.bars_manager.main_bars: - self.addToolBar(Qt.TopToolBarArea, bar) - for bar in self.bars_manager.child_bars: - self.addToolBar(Qt.BottomToolBarArea, bar) self.bars_manager.update_bars() self.setUnifiedTitleAndToolBarOnMac(True) From 079306efdd68bb2881d4a2413545a15d2bb15e4d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 17:43:44 -0600 Subject: [PATCH 19/21] Axe the unified toolbar on OS X --- src/calibre/gui2/bars.py | 7 ------- src/calibre/gui2/layout.py | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index 58711f9096..7dc0567d95 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -249,10 +249,6 @@ class BarsManager(QObject): self.menu_bar = MenuBar(self.location_manager, self.parent()) self.parent().setMenuBar(self.menu_bar) - parent.addToolBar(Qt.TopToolBarArea, self.main_bars[0]) - parent.addToolBar(Qt.BottomToolBarArea, self.main_bars[1]) - parent.addToolBar(Qt.BottomToolBarArea, self.child_bars[0]) - self.apply_settings() self.init_bars() @@ -292,15 +288,12 @@ class BarsManager(QObject): ''' showing_device = self.location_manager.has_device main_bar = self.main_bars[1 if showing_device else 0] - hidden_bar = self.main_bars[0 if showing_device else 1] - self.parent().addToolBar(Qt.BottomToolBarArea, hidden_bar) child_bar = self.child_bars[0] for bar in self.bars: bar.setVisible(False) bar.update_lm_actions() if main_bar.added_actions: main_bar.setVisible(True) - self.parent().addToolBar(Qt.TopToolBarArea, main_bar) if child_bar.added_actions: child_bar.setVisible(True) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 85e79d66d3..281450ed30 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -259,8 +259,14 @@ class MainWindowMixin(object): # {{{ self.search_bar = SearchBar(self) self.bars_manager = BarsManager(self.donate_button, self.location_manager, self) + for bar in self.bars_manager.main_bars: + self.addToolBar(Qt.TopToolBarArea, bar) + for bar in self.bars_manager.child_bars: + self.addToolBar(Qt.BottomToolBarArea, bar) self.bars_manager.update_bars() - self.setUnifiedTitleAndToolBarOnMac(True) + # This is disabled because it introduces various toolbar related bugs + # The width of the toolbar becomes the sum of both toolbars + # self.setUnifiedTitleAndToolBarOnMac(True) l = self.centralwidget.layout() l.addWidget(self.search_bar) From 93f4ee8021b72a8630cba403d8efa92eec6109ed Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 19 May 2011 19:44:46 -0400 Subject: [PATCH 20/21] Store: Add calibre coupon code for 10% off purchases. --- src/calibre/gui2/store/oreilly_plugin.py | 35 +++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/store/oreilly_plugin.py b/src/calibre/gui2/store/oreilly_plugin.py index b7e4e040f4..602a98c68e 100644 --- a/src/calibre/gui2/store/oreilly_plugin.py +++ b/src/calibre/gui2/store/oreilly_plugin.py @@ -6,6 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' +import re import urllib from contextlib import closing @@ -25,6 +26,9 @@ class OReillyStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): url = 'http://oreilly.com/ebooks/' + if detail_item: + detail_item = 'https://epoch.oreilly.com/shop/cart.orm?prod=%s.EBOOK&p=CALIBRE' % detail_item + if external or self.config.get('open_external', False): open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) else: @@ -45,9 +49,11 @@ class OReillyStore(BasicStoreConfig, StorePlugin): if counter <= 0: break - id = ''.join(data.xpath('.//div[@class="title"]/a/@href')) - if not id: + full_id = ''.join(data.xpath('.//div[@class="title"]/a/@href')) + mo = re.search('\d+', full_id) + if not mo: continue + id = mo.group() cover_url = ''.join(data.xpath('.//div[@class="bigCover"]//img/@src')) @@ -55,6 +61,18 @@ class OReillyStore(BasicStoreConfig, StorePlugin): author = ''.join(data.xpath('.//div[@class="author"]/text()')) author = author.split('By ')[-1].strip() + # Get the detail here because we need to get the ebook id for the detail_item. + with closing(br.open(full_id, timeout=timeout)) as nf: + idoc = html.fromstring(nf.read()) + + price = ''.join(idoc.xpath('(//span[@class="price"])[1]/span//text()')) + formats = ', '.join(idoc.xpath('//div[@class="ebook_formats"]//a/text()')) + + eid = ''.join(idoc.xpath('(//a[@class="product_buy_link" and contains(@href, ".EBOOK")])[1]/@href')).strip() + mo = re.search('\d+', eid) + if mo: + id = mo.group() + counter -= 1 s = SearchResult() @@ -62,17 +80,8 @@ class OReillyStore(BasicStoreConfig, StorePlugin): s.title = title.strip() s.author = author.strip() s.detail_item = id.strip() + s.price = price.strip() s.drm = SearchResult.DRM_UNLOCKED + s.formats = formats.upper() yield s - - def get_details(self, search_result, timeout): - br = browser() - with closing(br.open(search_result.detail_item, timeout=timeout)) as nf: - doc = html.fromstring(nf.read()) - - search_result.price = ''.join(doc.xpath('(//span[@class="price"])[1]/span//text()')).strip() - search_result.formats = ', '.join(doc.xpath('//div[@class="ebook_formats"]//a/text()')).upper() - - return True - From 9c169f638ff393d9421ba8e4694a555e3a965bb2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 19 May 2011 17:51:08 -0600 Subject: [PATCH 21/21] Add a tweak to allow users to turn on the unified title/toolbar if they so desire --- resources/default_tweaks.py | 8 ++++++++ src/calibre/gui2/layout.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 691a82fc36..c215e9634d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -350,3 +350,11 @@ send_news_to_device_location = "main" # work on all operating systems) server_listen_on = '0.0.0.0' +#: Unified toolbar on OS X +# If you enable this option and restart calibre, the toolbar will be 'unified' +# with the titlebar as is normal for OS X applications. However, doing this has +# various bugs, for instance the minimum width of the toolbar becomes twice +# what it should be and it causes other random bugs on some systems, so turn it +# on at your own risk! +unified_title_toolbar_on_osx = False + diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 281450ed30..7d07463b87 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -17,6 +17,7 @@ from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.bars import BarsManager from calibre.gui2.widgets import ComboBoxWithHelp +from calibre.utils.config_base import tweaks from calibre import human_readable class LocationManager(QObject): # {{{ @@ -266,7 +267,8 @@ class MainWindowMixin(object): # {{{ self.bars_manager.update_bars() # This is disabled because it introduces various toolbar related bugs # The width of the toolbar becomes the sum of both toolbars - # self.setUnifiedTitleAndToolBarOnMac(True) + if tweaks['unified_title_toolbar_on_osx']: + self.setUnifiedTitleAndToolBarOnMac(True) l = self.centralwidget.layout() l.addWidget(self.search_bar)