1__license__ = 'GPL v3' 2__copyright__ = '2010-2012, , Timothy Legge <timlegge at gmail.com> and David Forrester <davidfor@internode.on.net>' 3__docformat__ = 'restructuredtext en' 4 5import os, time, sys 6from functools import cmp_to_key 7 8from calibre.constants import preferred_encoding, DEBUG 9from calibre import isbytestring 10 11from calibre.ebooks.metadata.book.base import Metadata 12from calibre.devices.usbms.books import Book as Book_, CollectionsBookList, none_cmp 13from calibre.utils.config_base import prefs 14from calibre.devices.usbms.driver import debug_print 15from calibre.ebooks.metadata import author_to_author_sort 16 17 18class Book(Book_): 19 20 def __init__(self, prefix, lpath, title=None, authors=None, mime=None, date=None, ContentType=None, 21 thumbnail_name=None, size=None, other=None): 22 from calibre.utils.date import parse_date 23# debug_print('Book::__init__ - title=', title) 24 show_debug = title is not None and title.lower().find("xxxxx") >= 0 25 if other is not None: 26 other.title = title 27 other.published_date = date 28 if show_debug: 29 debug_print("Book::__init__ - title=", title, 'authors=', authors) 30 debug_print("Book::__init__ - other=", other) 31 super().__init__(prefix, lpath, size, other) 32 33 if title is not None and len(title) > 0: 34 self.title = title 35 36 if authors is not None and len(authors) > 0: 37 self.authors_from_string(authors) 38 if self.author_sort is None or self.author_sort == "Unknown": 39 self.author_sort = author_to_author_sort(authors) 40 41 self.mime = mime 42 43 self.size = size # will be set later if None 44 45 if ContentType == '6' and date is not None: 46 try: 47 self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") 48 except: 49 try: 50 self.datetime = time.strptime(date.split('+')[0], "%Y-%m-%dT%H:%M:%S") 51 except: 52 try: 53 self.datetime = time.strptime(date.split('+')[0], "%Y-%m-%d") 54 except: 55 try: 56 self.datetime = parse_date(date, 57 assume_utc=True).timetuple() 58 except: 59 try: 60 self.datetime = time.gmtime(os.path.getctime(self.path)) 61 except: 62 self.datetime = time.gmtime() 63 64 self.kobo_metadata = Metadata(title, self.authors) 65 self.contentID = None 66 self.current_shelves = [] 67 self.kobo_collections = [] 68 self.can_put_on_shelves = True 69 self.kobo_series = None 70 self.kobo_series_number = None # Kobo stores the series number as string. And it can have a leading "#". 71 self.kobo_series_id = None 72 self.kobo_subtitle = None 73 74 if thumbnail_name is not None: 75 self.thumbnail = ImageWrapper(thumbnail_name) 76 77 if show_debug: 78 debug_print("Book::__init__ end - self=", self) 79 debug_print("Book::__init__ end - title=", title, 'authors=', authors) 80 81 @property 82 def is_sideloaded(self): 83 # If we don't have a content Id, we don't know what type it is. 84 return self.contentID and self.contentID.startswith("file") 85 86 @property 87 def has_kobo_series(self): 88 return self.kobo_series is not None 89 90 @property 91 def is_purchased_kepub(self): 92 return self.contentID and not self.contentID.startswith("file") 93 94 def __str__(self): 95 ''' 96 A string representation of this object, suitable for printing to 97 console 98 ''' 99 ans = ["Kobo metadata:"] 100 101 def fmt(x, y): 102 ans.append('%-20s: %s'%(str(x), str(y))) 103 104 if self.contentID: 105 fmt('Content ID', self.contentID) 106 if self.kobo_series: 107 fmt('Kobo Series', self.kobo_series + ' #%s'%self.kobo_series_number) 108 if self.kobo_series_id: 109 fmt('Kobo Series ID', self.kobo_series_id) 110 if self.kobo_subtitle: 111 fmt('Subtitle', self.kobo_subtitle) 112 if self.mime: 113 fmt('MimeType', self.mime) 114 115 ans.append(str(self.kobo_metadata)) 116 117 ans = '\n'.join(ans) 118 119 return super().__str__() + "\n" + ans 120 121 122class ImageWrapper: 123 124 def __init__(self, image_path): 125 self.image_path = image_path 126 127 128class KTCollectionsBookList(CollectionsBookList): 129 130 def __init__(self, oncard, prefix, settings): 131 super().__init__(oncard, prefix, settings) 132 self.set_device_managed_collections([]) 133 134 def get_collections(self, collection_attributes): 135 debug_print("KTCollectionsBookList:get_collections - start - collection_attributes=", collection_attributes) 136 137 collections = {} 138 139 ca = [] 140 for c in collection_attributes: 141 ca.append(c.lower()) 142 collection_attributes = ca 143 debug_print("KTCollectionsBookList:get_collections - collection_attributes=", collection_attributes) 144 145 for book in self: 146 tsval = book.get('title_sort', book.title) 147 if tsval is None: 148 tsval = book.title 149 150 show_debug = self.is_debugging_title(tsval) or tsval is None 151 if show_debug: # or len(book.device_collections) > 0: 152 debug_print('KTCollectionsBookList:get_collections - tsval=', tsval, "book.title=", book.title, "book.title_sort=", book.title_sort) 153 debug_print('KTCollectionsBookList:get_collections - book.device_collections=', book.device_collections) 154# debug_print(book) 155 # Make sure we can identify this book via the lpath 156 lpath = getattr(book, 'lpath', None) 157 if lpath is None: 158 continue 159 # If the book is not in the current library, we don't want to use the metadtaa for the collections 160 if book.application_id is None: 161 # debug_print("KTCollectionsBookList:get_collections - Book not in current library") 162 continue 163 # Decide how we will build the collections. The default: leave the 164 # book in all existing collections. Do not add any new ones. 165 attrs = ['device_collections'] 166 if getattr(book, '_new_book', False): 167 if prefs['manage_device_metadata'] == 'manual': 168 # Ensure that the book is in all the book's existing 169 # collections plus all metadata collections 170 attrs += collection_attributes 171 else: 172 # For new books, both 'on_send' and 'on_connect' do the same 173 # thing. The book's existing collections are ignored. Put 174 # the book in collections defined by its metadata. 175 attrs = collection_attributes 176 elif prefs['manage_device_metadata'] == 'on_connect': 177 # For existing books, modify the collections only if the user 178 # specified 'on_connect' 179 attrs = collection_attributes 180 for cat_name in self.device_managed_collections: 181 if cat_name in book.device_collections: 182 if cat_name not in collections: 183 collections[cat_name] = {} 184 if show_debug: 185 debug_print("KTCollectionsBookList:get_collections - Device Managed Collection:", cat_name) 186 if lpath not in collections[cat_name]: 187 collections[cat_name][lpath] = (book, tsval, tsval) 188 if show_debug: 189 debug_print("KTCollectionsBookList:get_collections - Device Managed Collection -added book to cat_name", cat_name) 190 book.device_collections = [] 191 if show_debug: 192 debug_print("KTCollectionsBookList:get_collections - attrs=", attrs) 193 194 for attr in attrs: 195 attr = attr.strip() 196 if show_debug: 197 debug_print("KTCollectionsBookList:get_collections - attr='%s'"%attr) 198 # If attr is device_collections, then we cannot use 199 # format_field, because we don't know the fields where the 200 # values came from. 201 if attr == 'device_collections': 202 doing_dc = True 203 val = book.device_collections # is a list 204 if show_debug: 205 debug_print("KTCollectionsBookList:get_collections - adding book.device_collections", book.device_collections) 206 # If the book is not in the current library, we don't want to use the metadtaa for the collections 207 elif book.application_id is None or not book.can_put_on_shelves: 208 # debug_print("KTCollectionsBookList:get_collections - Book not in current library") 209 continue 210 else: 211 doing_dc = False 212 ign, val, orig_val, fm = book.format_field_extended(attr) 213 val = book.get(attr, None) 214 if show_debug: 215 debug_print("KTCollectionsBookList:get_collections - not device_collections") 216 debug_print(' ign=', ign, ', val=', val, ' orig_val=', orig_val, 'fm=', fm) 217 debug_print(' val=', val) 218 if not val: 219 continue 220 if isbytestring(val): 221 val = val.decode(preferred_encoding, 'replace') 222 if isinstance(val, (list, tuple)): 223 val = list(val) 224# debug_print("KTCollectionsBookList:get_collections - val is list=", val) 225 elif fm is not None and fm['datatype'] == 'series': 226 val = [orig_val] 227 elif fm is not None and fm['datatype'] == 'rating': 228 val = [str(orig_val / 2.0)] 229 elif fm is not None and fm['datatype'] == 'text' and fm['is_multiple']: 230 if isinstance(orig_val, (list, tuple)): 231 val = orig_val 232 else: 233 val = [orig_val] 234 if show_debug: 235 debug_print("KTCollectionsBookList:get_collections - val is text and multiple", val) 236 elif fm is not None and fm['datatype'] == 'composite' and fm['is_multiple']: 237 if show_debug: 238 debug_print("KTCollectionsBookList:get_collections - val is compositeand multiple", val) 239 val = [v.strip() for v in 240 val.split(fm['is_multiple']['ui_to_list'])] 241 else: 242 val = [val] 243 if show_debug: 244 debug_print("KTCollectionsBookList:get_collections - val=", val) 245 246 for category in val: 247 # debug_print("KTCollectionsBookList:get_collections - category=", category) 248 is_series = False 249 if doing_dc: 250 # Attempt to determine if this value is a series by 251 # comparing it to the series name. 252 if category == book.series: 253 is_series = True 254 elif fm is not None and fm['is_custom']: # is a custom field 255 if fm['datatype'] == 'text' and len(category) > 1 and \ 256 category[0] == '[' and category[-1] == ']': 257 continue 258 if fm['datatype'] == 'series': 259 is_series = True 260 else: # is a standard field 261 if attr == 'tags' and len(category) > 1 and \ 262 category[0] == '[' and category[-1] == ']': 263 continue 264 if attr == 'series' or \ 265 ('series' in collection_attributes and 266 book.get('series', None) == category): 267 is_series = True 268 269 # The category should not be None, but, it has happened. 270 if not category: 271 continue 272 273 cat_name = str(category).strip(' ,') 274 275 if cat_name not in collections: 276 collections[cat_name] = {} 277 if show_debug: 278 debug_print("KTCollectionsBookList:get_collections - created collection for cat_name", cat_name) 279 if lpath not in collections[cat_name]: 280 if is_series: 281 if doing_dc: 282 collections[cat_name][lpath] = \ 283 (book, book.get('series_index', sys.maxsize), tsval) 284 else: 285 collections[cat_name][lpath] = \ 286 (book, book.get(attr+'_index', sys.maxsize), tsval) 287 else: 288 collections[cat_name][lpath] = (book, tsval, tsval) 289 if show_debug: 290 debug_print("KTCollectionsBookList:get_collections - added book to collection for cat_name", cat_name) 291 if show_debug: 292 debug_print("KTCollectionsBookList:get_collections - cat_name", cat_name) 293 294 # Sort collections 295 result = {} 296 297 for category, lpaths in collections.items(): 298 books = sorted(lpaths.values(), key=cmp_to_key(none_cmp)) 299 result[category] = [x[0] for x in books] 300 # debug_print("KTCollectionsBookList:get_collections - result=", result.keys()) 301 debug_print("KTCollectionsBookList:get_collections - end") 302 return result 303 304 def set_device_managed_collections(self, collection_names): 305 self.device_managed_collections = collection_names 306 307 def set_debugging_title(self, title): 308 self.debugging_title = title 309 310 def is_debugging_title(self, title): 311 if not DEBUG: 312 return False 313# debug_print("KTCollectionsBookList:is_debugging - title=", title, "self.debugging_title=", self.debugging_title) 314 is_debugging = self.debugging_title is not None and len(self.debugging_title) > 0 and title is not None and ( 315 title.lower().find(self.debugging_title.lower()) >= 0 or len(title) == 0) 316# debug_print("KTCollectionsBookList:is_debugging - is_debugging=", is_debugging) 317 318 return is_debugging 319