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