~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to lib/item.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
"""``miro.item`` -- Holds ``Item`` class and related things.
 
30
"""
 
31
 
 
32
from datetime import datetime, timedelta
 
33
from itertools import chain
 
34
from miro.gtcache import gettext as _
 
35
from miro.util import (check_u, returns_unicode, check_f, returns_filename,
 
36
                       quote_unicode_url, stringify, get_first_video_enclosure,
 
37
                       entity_replace)
 
38
from miro.plat.utils import filename_to_unicode, unicode_to_filename
 
39
import locale
 
40
import os.path
 
41
import traceback
 
42
 
 
43
from miro.download_utils import clean_filename, next_free_filename
 
44
from miro.feedparser import FeedParserDict
 
45
 
 
46
from miro.database import (DDBObject, ObjectNotFoundError,
 
47
                           DatabaseConstraintError)
 
48
from miro.databasehelper import make_simple_get_set
 
49
from miro import app
 
50
from miro import iconcache
 
51
from miro import databaselog
 
52
from miro import downloader
 
53
from miro import config
 
54
from miro import eventloop
 
55
from miro import prefs
 
56
from miro.plat import resources
 
57
from miro import util
 
58
from miro import moviedata
 
59
import logging
 
60
from miro import filetypes
 
61
from miro import searchengines
 
62
from miro import fileutil
 
63
from miro import search
 
64
from miro import models
 
65
 
 
66
_charset = locale.getpreferredencoding()
 
67
 
 
68
KNOWN_MIME_TYPES = (u'audio', u'video')
 
69
KNOWN_MIME_SUBTYPES = (
 
70
    u'mov', u'wmv', u'mp4', u'mp3',
 
71
    u'mpg', u'mpeg', u'avi', u'x-flv',
 
72
    u'x-msvideo', u'm4v', u'mkv', u'm2v', u'ogg'
 
73
    )
 
74
MIME_SUBSITUTIONS = {
 
75
    u'QUICKTIME': u'MOV',
 
76
}
 
77
 
 
78
class FeedParserValues(object):
 
79
    """Helper class to get values from feedparser entries
 
80
 
 
81
    FeedParserValues objects inspect the FeedParserDict for the entry
 
82
    attribute for various attributes using in Item (entry_title,
 
83
    rss_id, url, etc...).
 
84
    """
 
85
    def __init__(self, entry):
 
86
        self.entry = entry
 
87
        self.first_video_enclosure = get_first_video_enclosure(entry)
 
88
 
 
89
        self.data = {
 
90
            'license': entry.get("license"),
 
91
            'rss_id': entry.get('id'),
 
92
            'entry_title': self._calc_title(),
 
93
            'thumbnail_url': self._calc_thumbnail_url(),
 
94
            'entry_description': self._calc_raw_description(),
 
95
            'link': self._calc_link(),
 
96
            'payment_link': self._calc_payment_link(),
 
97
            'comments_link': self._calc_comments_link(),
 
98
            'url': self._calc_url(),
 
99
            'enclosure_size': self._calc_enclosure_size(),
 
100
            'enclosure_type': self._calc_enclosure_type(),
 
101
            'enclosure_format': self._calc_enclosure_format(),
 
102
            'releaseDateObj': self._calc_release_date(),
 
103
        }
 
104
 
 
105
    def update_item(self, item):
 
106
        for key, value in self.data.items():
 
107
            setattr(item, key, value)
 
108
 
 
109
    def compare_to_item(self, item):
 
110
        for key, value in self.data.items():
 
111
            if getattr(item, key) != value:
 
112
                return False
 
113
        return True
 
114
 
 
115
    def compare_to_item_enclosures(self, item):
 
116
        compare_keys = (
 
117
            'url', 'enclosure_size', 'enclosure_type',
 
118
            'enclosure_format'
 
119
            )
 
120
        for key in compare_keys:
 
121
            if getattr(item, key) != self.data[key]:
 
122
                return False
 
123
        return True
 
124
 
 
125
    def _calc_title(self):
 
126
        if hasattr(self.entry, "title"):
 
127
            # The title attribute shouldn't use entities, but some in
 
128
            # the wild do (#11413).  In that case, try to fix them.
 
129
            return entity_replace(self.entry.title)
 
130
 
 
131
        if ((self.first_video_enclosure
 
132
             and 'url' in self.first_video_enclosure)):
 
133
            return self.first_video_enclosure['url'].decode("ascii",
 
134
                                                                "replace")
 
135
        return None
 
136
 
 
137
    def _calc_thumbnail_url(self):
 
138
        """Returns a link to the thumbnail of the video.  """
 
139
 
 
140
        # Try to get the thumbnail specific to the video enclosure
 
141
        if self.first_video_enclosure is not None:
 
142
            url = self._get_element_thumbnail(self.first_video_enclosure)
 
143
            if url is not None:
 
144
                return url
 
145
 
 
146
        # Try to get any enclosure thumbnail
 
147
        if hasattr(self.entry, "enclosures"):
 
148
            for enclosure in self.entry.enclosures:
 
149
                url = self._get_element_thumbnail(enclosure)
 
150
                if url is not None:
 
151
                    return url
 
152
 
 
153
        # Try to get the thumbnail for our entry
 
154
        return self._get_element_thumbnail(self.entry)
 
155
 
 
156
    def _get_element_thumbnail(self, element):
 
157
        try:
 
158
            thumb = element["thumbnail"]
 
159
        except KeyError:
 
160
            return None
 
161
        if isinstance(thumb, str):
 
162
            return thumb
 
163
        elif isinstance(thumb, unicode):
 
164
            return thumb.decode('ascii', 'replace')
 
165
        try:
 
166
            return thumb["url"].decode('ascii', 'replace')
 
167
        except (KeyError, AttributeError):
 
168
            return None
 
169
 
 
170
    def _calc_raw_description(self):
 
171
        """Check the enclosure to see if it has a description first.
 
172
        If not, then grab the description from the entry.
 
173
 
 
174
        Both first_video_enclosure and entry are FeedParserDicts,
 
175
        which does some fancy footwork with normalizing feed entry
 
176
        data.
 
177
        """
 
178
        rv = None
 
179
        if self.first_video_enclosure:
 
180
            rv = self.first_video_enclosure.get("text", None)
 
181
        if not rv and self.entry:
 
182
            rv = self.entry.get("description", None)
 
183
        if not rv:
 
184
            return u''
 
185
        return rv
 
186
 
 
187
    def _calc_link(self):
 
188
        if hasattr(self.entry, "link"):
 
189
            link = self.entry.link
 
190
            if isinstance(link, dict):
 
191
                try:
 
192
                    link = link['href']
 
193
                except KeyError:
 
194
                    return u""
 
195
            if link is None:
 
196
                return u""
 
197
            if isinstance(link, unicode):
 
198
                return link
 
199
            try:
 
200
                return link.decode('ascii', 'replace')
 
201
            except UnicodeDecodeError:
 
202
                return link.decode('ascii', 'ignore')
 
203
        return u""
 
204
 
 
205
    def _calc_payment_link(self):
 
206
        try:
 
207
            return self.first_video_enclosure.payment_url.decode('ascii',
 
208
                                                                 'replace')
 
209
        except:
 
210
            try:
 
211
                return self.entry.payment_url.decode('ascii','replace')
 
212
            except:
 
213
                return u""
 
214
 
 
215
    def _calc_comments_link(self):
 
216
        return self.entry.get('comments', u"")
 
217
 
 
218
    def _calc_url(self):
 
219
        if (self.first_video_enclosure is not None and
 
220
                'url' in self.first_video_enclosure):
 
221
            url = self.first_video_enclosure['url'].replace('+', '%20')
 
222
            return quote_unicode_url(url)
 
223
        else:
 
224
            return u''
 
225
 
 
226
    def _calc_enclosure_size(self):
 
227
        enc = self.first_video_enclosure
 
228
        if enc is not None and "torrent" not in enc.get("type", ""):
 
229
            try:
 
230
                return int(enc['length'])
 
231
            except (KeyError, ValueError):
 
232
                return None
 
233
 
 
234
    def _calc_enclosure_type(self):
 
235
        if ((self.first_video_enclosure
 
236
             and self.first_video_enclosure.has_key('type'))):
 
237
            return self.first_video_enclosure['type']
 
238
        else:
 
239
            return None
 
240
 
 
241
    def _calc_enclosure_format(self):
 
242
        enclosure = self.first_video_enclosure
 
243
        if enclosure:
 
244
            try:
 
245
                extension = enclosure['url'].split('.')[-1]
 
246
                extension = extension.lower().encode('ascii', 'replace')
 
247
            except (SystemExit, KeyboardInterrupt):
 
248
                raise
 
249
            except KeyError:
 
250
                extension = u''
 
251
            # Hack for mp3s, "mpeg audio" isn't clear enough
 
252
            if extension.lower() == u'mp3':
 
253
                return u'.mp3'
 
254
            if enclosure.get('type'):
 
255
                enc = enclosure['type'].decode('ascii', 'replace')
 
256
                if "/" in enc:
 
257
                    mtype, subtype = enc.split('/', 1)
 
258
                    mtype = mtype.lower()
 
259
                    if mtype in KNOWN_MIME_TYPES:
 
260
                        format = subtype.split(';')[0].upper()
 
261
                        if mtype == u'audio':
 
262
                            format += u' AUDIO'
 
263
                        if format.startswith(u'X-'):
 
264
                            format = format[2:]
 
265
                        return (u'.%s' %
 
266
                                MIME_SUBSITUTIONS.get(format, format).lower())
 
267
 
 
268
            if extension in KNOWN_MIME_SUBTYPES:
 
269
                return u'.%s' % extension
 
270
        return None
 
271
 
 
272
    def _calc_release_date(self):
 
273
        try:
 
274
            return datetime(*self.first_video_enclosure.updated_parsed[0:7])
 
275
        except (SystemExit, KeyboardInterrupt):
 
276
            raise
 
277
        except:
 
278
            try:
 
279
                return datetime(*self.entry.updated_parsed[0:7])
 
280
            except (SystemExit, KeyboardInterrupt):
 
281
                raise
 
282
            except:
 
283
                return datetime.min
 
284
 
 
285
 
 
286
class Item(DDBObject, iconcache.IconCacheOwnerMixin):
 
287
    """An item corresponds to a single entry in a feed.  It has a
 
288
    single url associated with it.
 
289
    """
 
290
 
 
291
    ICON_CACHE_VITAL = False
 
292
 
 
293
    def setup_new(self, fp_values, linkNumber=0, feed_id=None, parent_id=None,
 
294
            eligibleForAutoDownload=True, channel_title=None):
 
295
        self.is_file_item = False
 
296
        self.feed_id = feed_id
 
297
        self.parent_id = parent_id
 
298
        self.channelTitle = channel_title
 
299
        self.isContainerItem = None
 
300
        self.seen = False
 
301
        self.autoDownloaded = False
 
302
        self.pendingManualDL = False
 
303
        self.downloadedTime = None
 
304
        self.watchedTime = None
 
305
        self.pendingReason = u""
 
306
        self.title = u""
 
307
        self.description = u""
 
308
        fp_values.update_item(self)
 
309
        self.expired = False
 
310
        self.keep = False
 
311
        self.filename = self.file_type = None
 
312
        self.eligibleForAutoDownload = eligibleForAutoDownload
 
313
        self.duration = None
 
314
        self.screenshot = None
 
315
        self.media_type_checked = False
 
316
        self.resumeTime = 0
 
317
        self.channelTitle = None
 
318
        self.downloader_id = None
 
319
        self.was_downloaded = False
 
320
        self.subtitle_encoding = None
 
321
        self.setup_new_icon_cache()
 
322
        # Initalize FileItem attributes to None
 
323
        self.deleted = self.shortFilename = self.offsetPath = None
 
324
 
 
325
        # linkNumber is a hack to make sure that scraped items at the
 
326
        # top of a page show up before scraped items at the bottom of
 
327
        # a page. 0 is the topmost, 1 is the next, and so on
 
328
        self.linkNumber = linkNumber
 
329
        self.creationTime = datetime.now()
 
330
        self._look_for_downloader()
 
331
        self.setup_common()
 
332
        self.split_item()
 
333
 
 
334
    def setup_restored(self):
 
335
        self.setup_common()
 
336
        self.setup_links()
 
337
 
 
338
    def setup_common(self):
 
339
        self.selected = False
 
340
        self.active = False
 
341
        self.expiring = None
 
342
        self.showMoreInfo = False
 
343
        self.updating_movie_info = False
 
344
 
 
345
    @classmethod
 
346
    def auto_pending_view(cls):
 
347
        return cls.make_view('feed.autoDownloadable AND '
 
348
                'NOT item.was_downloaded AND '
 
349
                '(item.eligibleForAutoDownload OR feed.getEverything)',
 
350
                joins={'feed': 'item.feed_id=feed.id'})
 
351
 
 
352
    @classmethod
 
353
    def manual_pending_view(cls):
 
354
        return cls.make_view('pendingManualDL')
 
355
 
 
356
    @classmethod
 
357
    def auto_downloads_view(cls):
 
358
        return cls.make_view("item.autoDownloaded AND "
 
359
                "rd.state in ('downloading', 'paused')",
 
360
                joins={'remote_downloader rd': 'item.downloader_id=rd.id'})
 
361
 
 
362
    @classmethod
 
363
    def manual_downloads_view(cls):
 
364
        return cls.make_view("NOT item.autoDownloaded AND "
 
365
                "NOT item.pendingManualDL AND "
 
366
                "rd.state in ('downloading', 'paused')",
 
367
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
368
 
 
369
    @classmethod
 
370
    def download_tab_view(cls):
 
371
        return cls.make_view("(item.pendingManualDL OR "
 
372
                "(rd.state in ('downloading', 'paused', 'uploading', "
 
373
                "'uploading-paused', 'offline') OR "
 
374
                "(rd.state == 'failed' AND "
 
375
                "feed.origURL == 'dtv:manualFeed')) AND "
 
376
                "rd.main_item_id=item.id)",
 
377
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id',
 
378
                    'feed': 'item.feed_id=feed.id'})
 
379
 
 
380
    @classmethod
 
381
    def downloading_view(cls):
 
382
        return cls.make_view("rd.state in ('downloading', 'uploading') AND "
 
383
                "rd.main_item_id=item.id",
 
384
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
385
 
 
386
    @classmethod
 
387
    def only_downloading_view(cls):
 
388
        return cls.make_view("rd.state='downloading' AND "
 
389
                "rd.main_item_id=item.id",
 
390
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
391
 
 
392
    @classmethod
 
393
    def paused_view(cls):
 
394
        return cls.make_view("rd.state in ('paused', 'uploading-paused') AND "
 
395
                "rd.main_item_id=item.id",
 
396
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
397
 
 
398
    @classmethod
 
399
    def unwatched_downloaded_items(cls):
 
400
        return cls.make_view("NOT item.seen AND "
 
401
                "item.parent_id IS NULL AND "
 
402
                "rd.state in ('finished', 'uploading', 'uploading-paused')",
 
403
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
404
 
 
405
    @classmethod
 
406
    def newly_downloaded_view(cls):
 
407
        return cls.make_view("NOT item.seen AND "
 
408
                "(item.file_type != 'other') AND "
 
409
                "(is_file_item OR "
 
410
                "rd.state in ('finished', 'uploading', 'uploading-paused'))",
 
411
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
412
 
 
413
    @classmethod
 
414
    def downloaded_view(cls):
 
415
        return cls.make_view("rd.state in ('finished', 'uploading', "
 
416
                "'uploading-paused')",
 
417
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
418
 
 
419
    @classmethod
 
420
    def unique_new_video_view(cls):
 
421
        return cls.make_view("NOT item.seen AND "
 
422
                "item.file_type='video' AND "
 
423
                "((is_file_item AND NOT deleted) OR "
 
424
                "(rd.main_item_id=item.id AND "
 
425
                "rd.state in ('finished', 'uploading', 'uploading-paused')))",
 
426
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
427
 
 
428
    @classmethod
 
429
    def unique_new_audio_view(cls):
 
430
        return cls.make_view("NOT item.seen AND "
 
431
                "item.file_type='audio' AND "
 
432
                "((is_file_item AND NOT deleted) OR "
 
433
                "(rd.main_item_id=item.id AND "
 
434
                "rd.state in ('finished', 'uploading', 'uploading-paused')))",
 
435
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
436
 
 
437
    @classmethod
 
438
    def toplevel_view(cls):
 
439
        return cls.make_view('feed_id IS NOT NULL')
 
440
 
 
441
    @classmethod
 
442
    def feed_view(cls, feed_id):
 
443
        return cls.make_view('feed_id=?', (feed_id,))
 
444
 
 
445
    @classmethod
 
446
    def visible_feed_view(cls, feed_id):
 
447
        return cls.make_view('feed_id=? AND (deleted IS NULL or not deleted)',
 
448
                (feed_id,))
 
449
 
 
450
    @classmethod
 
451
    def visible_folder_view(cls, folder_id):
 
452
        return cls.make_view('folder_id=? AND (deleted IS NULL or not deleted)',
 
453
                (folder_id,),
 
454
                joins={'feed': 'item.feed_id=feed.id'})
 
455
 
 
456
    @classmethod
 
457
    def folder_contents_view(cls, folder_id):
 
458
        return cls.make_view('parent_id=?', (folder_id,))
 
459
 
 
460
    @classmethod
 
461
    def feed_downloaded_view(cls, feed_id):
 
462
        return cls.make_view("feed_id=? AND "
 
463
                "rd.state in ('finished', 'uploading', 'uploading-paused')",
 
464
                (feed_id,),
 
465
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
466
 
 
467
    @classmethod
 
468
    def feed_downloading_view(cls, feed_id):
 
469
        return cls.make_view("feed_id=? AND "
 
470
                "rd.state in ('downloading', 'uploading') AND "
 
471
                "rd.main_item_id=item.id",
 
472
                (feed_id,),
 
473
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
474
 
 
475
    @classmethod
 
476
    def feed_available_view(cls, feed_id):
 
477
        return cls.make_view("feed_id=? AND NOT autoDownloaded "
 
478
                "AND downloadedTime IS NULL AND "
 
479
                "feed.last_viewed <= item.creationTime",
 
480
                (feed_id,),
 
481
                joins={'feed': 'item.feed_id=feed.id'})
 
482
 
 
483
    @classmethod
 
484
    def feed_auto_pending_view(cls, feed_id):
 
485
        return cls.make_view('feed_id=? AND feed.autoDownloadable AND '
 
486
                'NOT item.was_downloaded AND '
 
487
                '(item.eligibleForAutoDownload OR feed.getEverything)',
 
488
                (feed_id,),
 
489
                joins={'feed': 'item.feed_id=feed.id'})
 
490
 
 
491
    @classmethod
 
492
    def feed_unwatched_view(cls, feed_id):
 
493
        return cls.make_view("feed_id=? AND not seen AND "
 
494
                "(is_file_item OR rd.state in ('finished', 'uploading', "
 
495
                "'uploading-paused'))",
 
496
                (feed_id,),
 
497
                joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
 
498
 
 
499
    @classmethod
 
500
    def children_view(cls, parent_id):
 
501
        return cls.make_view('parent_id=?', (parent_id,))
 
502
 
 
503
    @classmethod
 
504
    def playlist_view(cls, playlist_id):
 
505
        return cls.make_view("pim.playlist_id=?", (playlist_id,),
 
506
                joins={'playlist_item_map AS pim': 'item.id=pim.item_id'},
 
507
                order_by='pim.position')
 
508
 
 
509
    @classmethod
 
510
    def playlist_folder_view(cls, playlist_folder_id):
 
511
        return cls.make_view(
 
512
            "pim.playlist_id=?", (playlist_folder_id,),
 
513
            joins={'playlist_folder_item_map AS pim': 'item.id=pim.item_id'},
 
514
            order_by='pim.position')
 
515
 
 
516
    @classmethod
 
517
    def search_item_view(cls):
 
518
        return cls.make_view("feed.origURL == 'dtv:search'",
 
519
                joins={'feed': 'item.feed_id=feed.id'})
 
520
 
 
521
    @classmethod
 
522
    def watchable_video_view(cls):
 
523
        return cls.make_view(
 
524
            "not isContainerItem AND "
 
525
            "(deleted IS NULL or not deleted) AND "
 
526
            "(is_file_item OR rd.main_item_id=item.id) AND "
 
527
            "(feed.origURL IS NULL OR feed.origURL!= 'dtv:singleFeed') AND "
 
528
            "item.file_type='video'",
 
529
            joins={'feed': 'item.feed_id=feed.id',
 
530
                   'remote_downloader as rd': 'item.downloader_id=rd.id'})
 
531
 
 
532
    @classmethod
 
533
    def watchable_audio_view(cls):
 
534
        return cls.make_view("not isContainerItem AND "
 
535
                "(deleted IS NULL or not deleted) AND "
 
536
                "(is_file_item OR rd.main_item_id=item.id) AND "
 
537
                "(feed.origURL IS NULL OR feed.origURL!= 'dtv:singleFeed') AND "
 
538
                "item.file_type='audio'",
 
539
                joins={'feed': 'item.feed_id=feed.id',
 
540
                    'remote_downloader as rd': 'item.downloader_id=rd.id'})
 
541
 
 
542
    @classmethod
 
543
    def watchable_other_view(cls):
 
544
        return cls.make_view(
 
545
            "(deleted IS NULL OR not deleted) AND "
 
546
            "(is_file_item OR rd.id IS NOT NULL) AND "
 
547
            "(parent_id IS NOT NULL or feed.origURL != 'dtv:singleFeed') AND "
 
548
            "item.file_type='other'",
 
549
            joins={'feed': 'item.feed_id=feed.id',
 
550
                   'remote_downloader as rd': 'rd.main_item_id=item.id'})
 
551
 
 
552
    @classmethod
 
553
    def feed_expiring_view(cls, feed_id, watched_before):
 
554
        return cls.make_view("watchedTime is not NULL AND "
 
555
                "watchedTime < ? AND feed_id = ? AND keep = 0",
 
556
                (watched_before, feed_id),
 
557
                joins={'feed': 'item.feed_id=feed.id'})
 
558
 
 
559
    @classmethod
 
560
    def latest_in_feed_view(cls, feed_id):
 
561
        return cls.make_view("feed_id=?", (feed_id,),
 
562
                order_by='releaseDateObj DESC', limit=1)
 
563
 
 
564
    @classmethod
 
565
    def media_children_view(cls, parent_id):
 
566
        return cls.make_view("parent_id=? AND "
 
567
                "file_type IN ('video', 'audio')", (parent_id,))
 
568
 
 
569
    @classmethod
 
570
    def containers_view(cls):
 
571
        return cls.make_view("isContainerItem")
 
572
 
 
573
    @classmethod
 
574
    def file_items_view(cls):
 
575
        return cls.make_view("is_file_item")
 
576
 
 
577
    @classmethod
 
578
    def orphaned_from_feed_view(cls):
 
579
        return cls.make_view('feed_id IS NOT NULL AND '
 
580
                'feed_id NOT IN (SELECT id from feed)')
 
581
 
 
582
    @classmethod
 
583
    def orphaned_from_parent_view(cls):
 
584
        return cls.make_view('parent_id IS NOT NULL AND '
 
585
                'parent_id NOT IN (SELECT id from item)')
 
586
 
 
587
    @classmethod
 
588
    def update_folder_trackers(cls):
 
589
        """Update each view tracker that care's about the item's
 
590
        folder (both playlist and channel folders).
 
591
        """
 
592
 
 
593
        for tracker in app.view_tracker_manager.trackers_for_ddb_class(cls):
 
594
            # bit of a hack here.  We only need to update ViewTrackers
 
595
            # that care about the item's folder.  This seems like a
 
596
            # safe way to check if that's true.
 
597
            if 'folder_id' in tracker.where:
 
598
                tracker.check_all_objects()
 
599
 
 
600
    @classmethod
 
601
    def downloader_view(cls, dler_id):
 
602
        return cls.make_view("downloader_id=?", (dler_id,))
 
603
 
 
604
    def _look_for_downloader(self):
 
605
        self.set_downloader(downloader.lookup_downloader(self.get_url()))
 
606
        if self.has_downloader() and self.downloader.is_finished():
 
607
            self.set_filename(self.downloader.get_filename())
 
608
 
 
609
    getSelected, setSelected = make_simple_get_set(
 
610
        u'selected', change_needs_save=False)
 
611
    getActive, setActive = make_simple_get_set(
 
612
        u'active', change_needs_save=False)
 
613
 
 
614
    def _find_child_paths(self):
 
615
        """If this item points to a directory, return the set all files
 
616
        under that directory.
 
617
        """
 
618
        filename_root = self.get_filename()
 
619
        if fileutil.isdir(filename_root):
 
620
            return set(fileutil.miro_allfiles(filename_root))
 
621
        else:
 
622
            return set()
 
623
 
 
624
    def _make_new_children(self, paths):
 
625
        filename_root = self.get_filename()
 
626
        if filename_root is None:
 
627
            logging.error("Item._make_new_children: get_filename here is None")
 
628
            return
 
629
        for path in paths:
 
630
            assert path.startswith(filename_root)
 
631
            offsetPath = path[len(filename_root):]
 
632
            while offsetPath[0] in ('/', '\\'):
 
633
                offsetPath = offsetPath[1:]
 
634
            FileItem(path, parent_id=self.id, offsetPath=offsetPath)
 
635
 
 
636
    def find_new_children(self):
 
637
        """If this feed is a container item, walk through its
 
638
        directory and find any new children.  Returns True if it found
 
639
        children and ran signal_change().
 
640
        """
 
641
        if not self.isContainerItem:
 
642
            return False
 
643
        if self.get_state() == 'downloading':
 
644
            # don't try to find videos that we're in the middle of
 
645
            # re-downloading
 
646
            return False
 
647
        child_paths = self._find_child_paths()
 
648
        for child in self.get_children():
 
649
            child_paths.discard(child.get_filename())
 
650
        self._make_new_children(child_paths)
 
651
        if child_paths:
 
652
            self.signal_change()
 
653
            return True
 
654
        return False
 
655
 
 
656
    def split_item(self):
 
657
        """returns True if it ran signal_change()"""
 
658
        if self.isContainerItem is not None:
 
659
            return self.find_new_children()
 
660
        if ((not isinstance(self, FileItem)
 
661
             and (self.downloader is None
 
662
                  or not self.downloader.is_finished()))):
 
663
            return False
 
664
        filename_root = self.get_filename()
 
665
        if filename_root is None:
 
666
            return False
 
667
        if fileutil.isdir(filename_root):
 
668
            child_paths = self._find_child_paths()
 
669
            if len(child_paths) > 0:
 
670
                self.isContainerItem = True
 
671
                self._make_new_children(child_paths)
 
672
            else:
 
673
                if not self.get_feed_url().startswith ("dtv:directoryfeed"):
 
674
                    target_dir = config.get(prefs.NON_VIDEO_DIRECTORY)
 
675
                    if not filename_root.startswith(target_dir):
 
676
                        if isinstance(self, FileItem):
 
677
                            self.migrate (target_dir)
 
678
                        else:
 
679
                            self.downloader.migrate (target_dir)
 
680
                self.isContainerItem = False
 
681
        else:
 
682
            self.isContainerItem = False
 
683
        self.signal_change()
 
684
        return True
 
685
 
 
686
    def set_subtitle_encoding(self, encoding):
 
687
        if encoding is not None:
 
688
            self.subtitle_encoding = unicode(encoding)
 
689
            config_value = encoding
 
690
        else:
 
691
            self.subtitle_encoding = None
 
692
            config_value = ''
 
693
        config.set(prefs.SUBTITLE_ENCODING, config_value)
 
694
        self.signal_change()
 
695
 
 
696
    def set_filename(self, filename):
 
697
        self.filename = filename
 
698
        if not self.media_type_checked:
 
699
            self.file_type = self._file_type_for_filename(filename)
 
700
 
 
701
    def set_file_type(self, file_type):
 
702
        self.file_type = file_type
 
703
        self.signal_change()
 
704
 
 
705
    def _file_type_for_filename(self, filename):
 
706
        filename = filename.lower()
 
707
        for ext in filetypes.VIDEO_EXTENSIONS:
 
708
            if filename.endswith(ext):
 
709
                return u'video'
 
710
        for ext in filetypes.AUDIO_EXTENSIONS:
 
711
            if filename.endswith(ext):
 
712
                return u'audio'
 
713
        return u'other'
 
714
 
 
715
    def matches_search(self, search_string):
 
716
        if search_string is None:
 
717
            return True
 
718
        search_string = search_string.lower()
 
719
        title = self.get_title() or u''
 
720
        desc = self.get_description() or u''
 
721
        if self.get_filename():
 
722
            filename = filename_to_unicode(self.get_filename())
 
723
        else:
 
724
            filename = u''
 
725
        if search.match(search_string, [title.lower(), desc.lower(),
 
726
                                       filename.lower()]):
 
727
            return True
 
728
        else:
 
729
            return False
 
730
 
 
731
    def _remove_from_playlists(self):
 
732
        models.PlaylistItemMap.remove_item_from_playlists(self)
 
733
        models.PlaylistFolderItemMap.remove_item_from_playlists(self)
 
734
 
 
735
    def check_constraints(self):
 
736
        if self.feed_id is not None:
 
737
            try:
 
738
                obj = models.Feed.get_by_id(self.feed_id)
 
739
            except ObjectNotFoundError:
 
740
                raise DatabaseConstraintError(
 
741
                    "my feed (%s) is not in database" % self.feed_id)
 
742
            else:
 
743
                if not isinstance(obj, models.Feed):
 
744
                    msg = "feed_id points to a %s instance" % obj.__class__
 
745
                    raise DatabaseConstraintError(msg)
 
746
        if self.has_parent():
 
747
            try:
 
748
                obj = Item.get_by_id(self.parent_id)
 
749
            except ObjectNotFoundError:
 
750
                raise DatabaseConstraintError(
 
751
                    "my parent (%s) is not in database" % self.parent_id)
 
752
            else:
 
753
                if not isinstance(obj, Item):
 
754
                    msg = "parent_id points to a %s instance" % obj.__class__
 
755
                    raise DatabaseConstraintError(msg)
 
756
                # If isContainerItem is None, we may be in the middle
 
757
                # of building the children list.
 
758
                if obj.isContainerItem is not None and not obj.isContainerItem:
 
759
                    msg = "parent_id is not a containerItem"
 
760
                    raise DatabaseConstraintError(msg)
 
761
        if self.parent_id is None and self.feed_id is None:
 
762
            raise DatabaseConstraintError("feed_id and parent_id both None")
 
763
        if self.parent_id is not None and self.feed_id is not None:
 
764
            raise DatabaseConstraintError(
 
765
                "feed_id and parent_id both not None")
 
766
 
 
767
    def on_signal_change(self):
 
768
        self.expiring = None
 
769
        self._sync_title()
 
770
        if hasattr(self, "_state"):
 
771
            del self._state
 
772
        if hasattr(self, "_size"):
 
773
            del self._size
 
774
 
 
775
    def _sync_title(self):
 
776
        # for torrents that aren't from a feed, we use the filename
 
777
        # as the title.
 
778
        if ((self.is_external()
 
779
             and self.has_downloader()
 
780
             and self.downloader.get_type() == "bittorrent"
 
781
             and self.downloader.get_state() == "downloading")):
 
782
            filename = os.path.basename(self.downloader.get_filename())
 
783
            if self.title != filename:
 
784
                self.set_title(filename_to_unicode(filename))
 
785
 
 
786
    def recalc_feed_counts(self):
 
787
        self.get_feed().recalc_counts()
 
788
 
 
789
    def get_viewed(self):
 
790
        """Returns True iff this item has never been viewed in the
 
791
        interface.
 
792
 
 
793
        Note the difference between "viewed" and seen.
 
794
        """
 
795
        try:
 
796
            # optimizing by trying the cached feed
 
797
            return self._feed.last_viewed >= self.creationTime
 
798
        except AttributeError:
 
799
            return self.get_feed().last_viewed >= self.creationTime
 
800
 
 
801
    @returns_unicode
 
802
    def get_url(self):
 
803
        """Returns the URL associated with the first enclosure in the
 
804
        item.
 
805
        """
 
806
        return self.url
 
807
 
 
808
    def has_shareable_url(self):
 
809
        """Does this item have a URL that the user can share with
 
810
        others?
 
811
 
 
812
        This returns True when the item has a non-file URL.
 
813
        """
 
814
        url = self.get_url()
 
815
        return url != u'' and not url.startswith(u"file:")
 
816
 
 
817
    def get_feed(self):
 
818
        """Returns the feed this item came from.
 
819
        """
 
820
        try:
 
821
            return self._feed
 
822
        except AttributeError:
 
823
            pass
 
824
 
 
825
        if self.feed_id is not None:
 
826
            self._feed = models.Feed.get_by_id(self.feed_id)
 
827
        elif self.has_parent():
 
828
            self._feed = self.get_parent().get_feed()
 
829
        else:
 
830
            self._feed = None
 
831
        return self._feed
 
832
 
 
833
    def get_parent(self):
 
834
        if hasattr(self, "_parent"):
 
835
            return self._parent
 
836
 
 
837
        if self.has_parent():
 
838
            self._parent = Item.get_by_id(self.parent_id)
 
839
        else:
 
840
            self._parent = self
 
841
        return self._parent
 
842
 
 
843
    @returns_unicode
 
844
    def get_feed_url(self):
 
845
        return self.get_feed().origURL
 
846
 
 
847
    @returns_unicode
 
848
    def get_source(self):
 
849
        if self.feed_id is not None:
 
850
            feed_ = self.get_feed()
 
851
            if feed_.origURL != 'dtv:manualFeed':
 
852
                return feed_.get_title()
 
853
        if self.has_parent():
 
854
            try:
 
855
                return self.get_parent().get_title()
 
856
            except ObjectNotFoundError:
 
857
                return None
 
858
        return None
 
859
 
 
860
    def get_children(self):
 
861
        if self.isContainerItem:
 
862
            return Item.children_view(self.id)
 
863
        else:
 
864
            raise ValueError("%s is not a container item" % self)
 
865
 
 
866
    def children_signal_change(self):
 
867
        for child in self.get_children():
 
868
            child.signal_change(needs_save=False)
 
869
 
 
870
    def is_playable(self):
 
871
        """Is this a playable item?"""
 
872
 
 
873
        if self.isContainerItem:
 
874
            return Item.media_children_view(self.id).count() > 0
 
875
        else:
 
876
            return self.file_type in ('audio', 'video')
 
877
 
 
878
    def set_feed(self, feed_id):
 
879
        """Moves this item to another feed.
 
880
        """
 
881
        self.feed_id = feed_id
 
882
        # _feed is created by get_feed which caches the result
 
883
        if hasattr(self, "_feed"):
 
884
            del self._feed
 
885
        if self.isContainerItem:
 
886
            for item in self.get_children():
 
887
                if hasattr(item, "_feed"):
 
888
                    del item._feed
 
889
                item.signal_change()
 
890
        self.signal_change()
 
891
 
 
892
    def expire(self):
 
893
        self.confirm_db_thread()
 
894
        self._remove_from_playlists()
 
895
        if not self.is_external():
 
896
            self.delete_files()
 
897
        if self.screenshot:
 
898
            try:
 
899
                fileutil.remove(self.screenshot)
 
900
            except (SystemExit, KeyboardInterrupt):
 
901
                raise
 
902
            except:
 
903
                pass
 
904
        # This should be done even if screenshot = ""
 
905
        self.screenshot = None
 
906
        if self.is_external():
 
907
            if self.is_downloaded():
 
908
                if self.isContainerItem:
 
909
                    for item in self.get_children():
 
910
                        item.make_deleted()
 
911
                elif self.get_filename():
 
912
                    FileItem(self.get_filename(), feed_id=self.feed_id,
 
913
                             parent_id=self.parent_id, deleted=True)
 
914
                if self.has_downloader():
 
915
                    self.downloader.set_delete_files(False)
 
916
            self.remove()
 
917
        else:
 
918
            self.expired = True
 
919
            self.seen = self.keep = self.pendingManualDL = False
 
920
            self.filename = None
 
921
            self.file_type = self.watchedTime = self.duration = None
 
922
            self.isContainerItem = None
 
923
            self.signal_change()
 
924
        self.recalc_feed_counts()
 
925
 
 
926
    def has_downloader(self):
 
927
        return self.downloader_id is not None and self.downloader is not None
 
928
 
 
929
    def has_parent(self):
 
930
        return self.parent_id is not None
 
931
 
 
932
    def is_main_item(self):
 
933
        return (self.has_downloader() and
 
934
                self.downloader.main_item_id == self.id)
 
935
 
 
936
    def downloader_state(self):
 
937
        if not self.has_downloader():
 
938
            return None
 
939
        else:
 
940
            return self.downloader.state
 
941
 
 
942
    def stop_upload(self):
 
943
        if self.downloader:
 
944
            self.downloader.stop_upload()
 
945
            if self.isContainerItem:
 
946
                self.children_signal_change()
 
947
 
 
948
    def pause_upload(self):
 
949
        if self.downloader:
 
950
            self.downloader.pause_upload()
 
951
            if self.isContainerItem:
 
952
                self.children_signal_change()
 
953
 
 
954
    def start_upload(self):
 
955
        if self.downloader:
 
956
            self.downloader.start_upload()
 
957
            if self.isContainerItem:
 
958
                self.children_signal_change()
 
959
 
 
960
    def get_expiration_time(self):
 
961
        """Returns the time when this item should expire.
 
962
 
 
963
        Returns a datetime.datetime object or None if it doesn't expire.
 
964
        """
 
965
        self.confirm_db_thread()
 
966
        if self.get_watched_time() is None or not self.is_downloaded():
 
967
            return None
 
968
        if self.keep:
 
969
            return None
 
970
        ufeed = self.get_feed()
 
971
        if ufeed.expire == u'never' or (ufeed.expire == u'system'
 
972
                and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0):
 
973
            return None
 
974
        else:
 
975
            if ufeed.expire == u"feed":
 
976
                expire_time = ufeed.expireTime
 
977
            elif ufeed.expire == u"system":
 
978
                expire_time = timedelta(
 
979
                    days=config.get(prefs.EXPIRE_AFTER_X_DAYS))
 
980
            return self.get_watched_time() + expire_time
 
981
 
 
982
    def get_watched_time(self):
 
983
        """Returns the most recent watched time of this item or any
 
984
        of its child items.
 
985
 
 
986
        Returns a datetime.datetime instance or None if the item and none
 
987
        of its children have been watched.
 
988
        """
 
989
        if not self.get_seen():
 
990
            return None
 
991
        if self.isContainerItem and self.watchedTime == None:
 
992
            self.watchedTime = datetime.min
 
993
            for item in self.get_children():
 
994
                child_time = item.get_watched_time()
 
995
                if child_time is None:
 
996
                    self.watchedTime = None
 
997
                    return None
 
998
                if child_time > self.watchedTime:
 
999
                    self.watchedTime = child_time
 
1000
            self.signal_change()
 
1001
        return self.watchedTime
 
1002
 
 
1003
    def get_expiring(self):
 
1004
        if self.expiring is None:
 
1005
            if not self.get_seen():
 
1006
                self.expiring = False
 
1007
            elif self.keep:
 
1008
                self.expiring = False
 
1009
            else:
 
1010
                ufeed = self.get_feed()
 
1011
                if ufeed.expire == u'never':
 
1012
                    self.expiring = False
 
1013
                elif (ufeed.expire == u'system'
 
1014
                      and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0):
 
1015
                    self.expiring = False
 
1016
                else:
 
1017
                    self.expiring = True
 
1018
        return self.expiring
 
1019
 
 
1020
    def get_seen(self):
 
1021
        """Returns true iff video has been seen.
 
1022
 
 
1023
        Note the difference between "viewed" and "seen".
 
1024
        """
 
1025
        self.confirm_db_thread()
 
1026
        return self.seen
 
1027
 
 
1028
    def mark_item_seen(self, mark_other_items=True):
 
1029
        """Marks the item as seen.
 
1030
        """
 
1031
        self.confirm_db_thread()
 
1032
        if self.isContainerItem:
 
1033
            for child in self.get_children():
 
1034
                child.seen = True
 
1035
                child.signal_change()
 
1036
        if self.seen == False:
 
1037
            self.seen = True
 
1038
            if self.subtitle_encoding is None:
 
1039
                config_value = config.get(prefs.SUBTITLE_ENCODING)
 
1040
                if config_value:
 
1041
                    self.subtitle_encoding = unicode(config_value)
 
1042
            if self.watchedTime is None:
 
1043
                self.watchedTime = datetime.now()
 
1044
            self.signal_change()
 
1045
            self.update_parent_seen()
 
1046
            if mark_other_items and self.downloader:
 
1047
                for item in self.downloader.item_list:
 
1048
                    if item != self:
 
1049
                        item.mark_item_seen(False)
 
1050
            self.recalc_feed_counts()
 
1051
 
 
1052
    def update_parent_seen(self):
 
1053
        if self.parent_id:
 
1054
            unseen_children = self.make_view('parent_id=? AND NOT seen AND '
 
1055
                    "file_type in ('audio', 'video')", (self.parent_id,))
 
1056
            new_seen = (unseen_children.count() == 0)
 
1057
            parent = self.get_parent()
 
1058
            if parent.seen != new_seen:
 
1059
                parent.seen = new_seen
 
1060
                parent.signal_change()
 
1061
 
 
1062
    def mark_item_unseen(self, mark_other_items=True):
 
1063
        self.confirm_db_thread()
 
1064
        if self.isContainerItem:
 
1065
            for item in self.get_children():
 
1066
                item.seen = False
 
1067
                item.signal_change()
 
1068
        if self.seen:
 
1069
            self.seen = False
 
1070
            self.watchedTime = None
 
1071
            self.resumeTime = 0
 
1072
            self.signal_change()
 
1073
            self.update_parent_seen()
 
1074
            if mark_other_items and self.downloader:
 
1075
                for item in self.downloader.item_list:
 
1076
                    if item != self:
 
1077
                        item.mark_item_unseen(False)
 
1078
            self.recalc_feed_counts()
 
1079
 
 
1080
    @returns_unicode
 
1081
    def get_rss_id(self):
 
1082
        self.confirm_db_thread()
 
1083
        return self.rss_id
 
1084
 
 
1085
    def remove_rss_id(self):
 
1086
        self.confirm_db_thread()
 
1087
        self.rss_id = None
 
1088
        self.signal_change()
 
1089
 
 
1090
    def set_auto_downloaded(self, autodl=True):
 
1091
        self.confirm_db_thread()
 
1092
        if autodl != self.autoDownloaded:
 
1093
            self.autoDownloaded = autodl
 
1094
            self.signal_change()
 
1095
 
 
1096
    @eventloop.as_idle
 
1097
    def set_resume_time(self, position):
 
1098
        if not self.id_exists():
 
1099
            return
 
1100
        try:
 
1101
            position = int(position)
 
1102
        except TypeError:
 
1103
            logging.exception("set_resume_time: not-saving!  given non-int %s",
 
1104
                              position)
 
1105
            return
 
1106
        if self.resumeTime != position:
 
1107
            self.resumeTime = position
 
1108
            self.signal_change()
 
1109
 
 
1110
    def get_auto_downloaded(self):
 
1111
        """Returns true iff item was auto downloaded.
 
1112
        """
 
1113
        self.confirm_db_thread()
 
1114
        return self.autoDownloaded
 
1115
 
 
1116
    def download(self, autodl=False):
 
1117
        """Starts downloading the item.
 
1118
        """
 
1119
        self.confirm_db_thread()
 
1120
        manual_dl_count = Item.manual_downloads_view().count()
 
1121
        self.expired = self.keep = self.seen = False
 
1122
        self.was_downloaded = True
 
1123
 
 
1124
        if ((not autodl) and
 
1125
                manual_dl_count >= config.get(prefs.MAX_MANUAL_DOWNLOADS)):
 
1126
            self.pendingManualDL = True
 
1127
            self.pendingReason = _("queued for download")
 
1128
            self.signal_change()
 
1129
            return
 
1130
        else:
 
1131
            self.set_auto_downloaded(autodl)
 
1132
            self.pendingManualDL = False
 
1133
 
 
1134
        dler = downloader.get_downloader_for_item(self)
 
1135
        if dler is not None:
 
1136
            self.set_downloader(dler)
 
1137
            self.downloader.set_channel_name(
 
1138
                unicode_to_filename(self.get_channel_title(True)))
 
1139
            if self.downloader.is_finished():
 
1140
                self.on_download_finished()
 
1141
            else:
 
1142
                self.downloader.start()
 
1143
        self.signal_change()
 
1144
        self.recalc_feed_counts()
 
1145
 
 
1146
    def pause(self):
 
1147
        if self.downloader:
 
1148
            self.downloader.pause()
 
1149
 
 
1150
    def resume(self):
 
1151
        self.download(self.get_auto_downloaded())
 
1152
 
 
1153
    def is_pending_manual_download(self):
 
1154
        self.confirm_db_thread()
 
1155
        return self.pendingManualDL
 
1156
 
 
1157
    def cancel_auto_download(self):
 
1158
        # FIXME - this is cheating and abusing the was_downloaded flag
 
1159
        self.was_downloaded = True
 
1160
        self.signal_change()
 
1161
        self.recalc_feed_counts()
 
1162
 
 
1163
    def is_eligible_for_auto_download(self):
 
1164
        self.confirm_db_thread()
 
1165
        if self.was_downloaded:
 
1166
            return False
 
1167
        ufeed = self.get_feed()
 
1168
        if ufeed.getEverything:
 
1169
            return True
 
1170
        return self.eligibleForAutoDownload
 
1171
 
 
1172
    def is_pending_auto_download(self):
 
1173
        return (self.get_feed().is_autodownloadable() and
 
1174
                self.is_eligible_for_auto_download())
 
1175
 
 
1176
    @returns_unicode
 
1177
    def get_thumbnail_url(self):
 
1178
        return self.thumbnail_url
 
1179
 
 
1180
    @returns_filename
 
1181
    def get_thumbnail(self):
 
1182
        """NOTE: When changing this function, change feed.icon_changed
 
1183
        to signal the right set of items.
 
1184
        """
 
1185
        self.confirm_db_thread()
 
1186
        if self.icon_cache is not None and self.icon_cache.isValid():
 
1187
            path = self.icon_cache.get_filename()
 
1188
            return resources.path(fileutil.expand_filename(path))
 
1189
        elif self.screenshot:
 
1190
            path = self.screenshot
 
1191
            return resources.path(fileutil.expand_filename(path))
 
1192
        elif self.isContainerItem:
 
1193
            return resources.path("images/thumb-default-folder.png")
 
1194
        else:
 
1195
            feed = self.get_feed()
 
1196
            if feed.thumbnail_valid():
 
1197
                return feed.get_thumbnail_path()
 
1198
            elif (self.get_filename()
 
1199
                  and filetypes.is_audio_filename(self.get_filename())):
 
1200
                return resources.path("images/thumb-default-audio.png")
 
1201
            else:
 
1202
                return resources.path("images/thumb-default-video.png")
 
1203
 
 
1204
    def is_downloaded_torrent(self):
 
1205
        return (self.isContainerItem and self.has_downloader() and
 
1206
                self.downloader.is_finished())
 
1207
 
 
1208
    @returns_unicode
 
1209
    def get_title(self):
 
1210
        """Returns the title of the item.
 
1211
        """
 
1212
        if self.title:
 
1213
            return self.title
 
1214
        if self.is_external() and self.is_downloaded_torrent():
 
1215
            if self.get_filename() is not None:
 
1216
                basename = os.path.basename(self.get_filename())
 
1217
                return filename_to_unicode(basename + os.path.sep)
 
1218
        if self.entry_title is not None:
 
1219
            return self.entry_title
 
1220
        return _('no title')
 
1221
 
 
1222
    def set_title(self, title):
 
1223
        self.confirm_db_thread()
 
1224
        self.title = title
 
1225
        self.signal_change()
 
1226
 
 
1227
    def set_description(self, desc):
 
1228
        self.confirm_db_thread()
 
1229
        self.description = desc
 
1230
        self.signal_change()
 
1231
 
 
1232
    def set_channel_title(self, title):
 
1233
        check_u(title)
 
1234
        self.channelTitle = title
 
1235
        self.signal_change()
 
1236
 
 
1237
    @returns_unicode
 
1238
    def get_channel_title(self, allowSearchFeedTitle=False):
 
1239
        implClass = self.get_feed().actualFeed.__class__
 
1240
        if implClass in (models.RSSFeedImpl, models.ScraperFeedImpl):
 
1241
            return self.get_feed().get_title()
 
1242
        elif implClass == models.SearchFeedImpl and allowSearchFeedTitle:
 
1243
            e = searchengines.get_last_engine()
 
1244
            if e:
 
1245
                return e.title
 
1246
            else:
 
1247
                return u''
 
1248
        elif self.channelTitle:
 
1249
            return self.channelTitle
 
1250
        else:
 
1251
            return u''
 
1252
 
 
1253
    @returns_unicode
 
1254
    def get_description(self):
 
1255
        """Returns the description of the video (unicode).
 
1256
 
 
1257
        If the item is a torrent, then it adds some additional text.
 
1258
        """
 
1259
        if self.description:
 
1260
            if self.is_downloaded_torrent():
 
1261
                return (unicode(self.description) + u'<BR>' +
 
1262
                        _('Contents appear in the library'))
 
1263
            else:
 
1264
                return unicode(self.description)
 
1265
 
 
1266
        if self.entry_description:
 
1267
            if self.is_downloaded_torrent():
 
1268
                return (unicode(self.entry_description) + u'<BR>' +
 
1269
                        _('Contents appear in the library'))
 
1270
            else:
 
1271
                return unicode(self.entry_description)
 
1272
        if self.is_external() and self.is_downloaded_torrent():
 
1273
            lines = [_('Contents:')]
 
1274
            lines.extend(filename_to_unicode(child.offsetPath)
 
1275
                         for child in self.get_children())
 
1276
            return u'<BR>\n'.join(lines)
 
1277
 
 
1278
        return u''
 
1279
 
 
1280
    def looks_like_torrent(self):
 
1281
        """Returns true if we think this item is a torrent.  (For items that
 
1282
        haven't been downloaded this uses the file extension which isn't
 
1283
        totally reliable).
 
1284
        """
 
1285
 
 
1286
        if self.has_downloader():
 
1287
            return self.downloader.get_type() == u'bittorrent'
 
1288
        else:
 
1289
            return filetypes.is_torrent_filename(self.get_url())
 
1290
 
 
1291
    def torrent_seeding_status(self):
 
1292
        """Get the torrent seeding status for this torrent.
 
1293
 
 
1294
        Possible values:
 
1295
 
 
1296
           None - Not part of a downloaded torrent
 
1297
           'seeding' - Part of a torrent that we're seeding
 
1298
           'stopped' - Part of a torrent that we've stopped seeding
 
1299
        """
 
1300
 
 
1301
        downloader_ = self.downloader
 
1302
        if downloader_ is None and self.has_parent():
 
1303
            downloader_ = self.get_parent().downloader
 
1304
        if downloader_ is None or downloader_.get_type() != u'bittorrent':
 
1305
            return None
 
1306
        if downloader_.get_state() == 'uploading':
 
1307
            return 'seeding'
 
1308
        else:
 
1309
            return 'stopped'
 
1310
 
 
1311
    def is_transferring(self):
 
1312
        return (self.downloader
 
1313
                and self.downloader.get_state() in (u'uploading',
 
1314
                                                    u'downloading'))
 
1315
 
 
1316
    def delete_files(self):
 
1317
        """Stops downloading the item.
 
1318
        """
 
1319
        self.confirm_db_thread()
 
1320
        if self.has_downloader():
 
1321
            self.set_downloader(None)
 
1322
        if self.isContainerItem:
 
1323
            for item in self.get_children():
 
1324
                item.delete_files()
 
1325
                item.remove()
 
1326
        self.delete_subtitle_files()
 
1327
 
 
1328
    def delete_subtitle_files(self):
 
1329
        """Deletes subtitle files associated with this item.
 
1330
        """
 
1331
        files = util.gather_subtitle_files(self.get_filename())
 
1332
        for mem in files:
 
1333
            fileutil.delete(mem)
 
1334
 
 
1335
    def get_state(self):
 
1336
        """Get the state of this item.  The state will be on of the
 
1337
        following:
 
1338
 
 
1339
        * new -- User has never seen this item
 
1340
        * not-downloaded -- User has seen the item, but not downloaded it
 
1341
        * downloading -- Item is currently downloading
 
1342
        * newly-downloaded -- Item has been downoladed, but not played
 
1343
        * expiring -- Item has been played and is set to expire
 
1344
        * saved -- Item has been played and has been saved
 
1345
        * expired -- Item has expired.
 
1346
 
 
1347
        Uses caching to prevent recalculating state over and over
 
1348
        """
 
1349
        try:
 
1350
            return self._state
 
1351
        except AttributeError:
 
1352
            self._calc_state()
 
1353
            return self._state
 
1354
 
 
1355
    @returns_unicode
 
1356
    def _calc_state(self):
 
1357
        """Recalculate the state of an item after a change
 
1358
        """
 
1359
        self.confirm_db_thread()
 
1360
        # FIXME, 'failed', and 'paused' should get download icons.
 
1361
        # The user should be able to restart or cancel them (put them
 
1362
        # into the stopped state).
 
1363
        if (self.downloader is None  or
 
1364
                self.downloader.get_state() in (u'failed', u'stopped')):
 
1365
            if self.pendingManualDL:
 
1366
                self._state = u'downloading'
 
1367
            elif self.expired:
 
1368
                self._state = u'expired'
 
1369
            elif (self.get_viewed() or
 
1370
                    (self.downloader and
 
1371
                        self.downloader.get_state() in (u'failed', u'stopped'))):
 
1372
                self._state = u'not-downloaded'
 
1373
            else:
 
1374
                self._state = u'new'
 
1375
        elif self.downloader.get_state() in (u'offline', u'paused'):
 
1376
            if self.pendingManualDL:
 
1377
                self._state = u'downloading'
 
1378
            else:
 
1379
                self._state = u'paused'
 
1380
        elif not self.downloader.is_finished():
 
1381
            self._state = u'downloading'
 
1382
        elif not self.get_seen():
 
1383
            self._state = u'newly-downloaded'
 
1384
        elif self.get_expiring():
 
1385
            self._state = u'expiring'
 
1386
        else:
 
1387
            self._state = u'saved'
 
1388
 
 
1389
    @returns_unicode
 
1390
    def get_channel_category(self):
 
1391
        """Get the category to use for the channel template.
 
1392
 
 
1393
        This method is similar to get_state(), but has some subtle
 
1394
        differences.  get_state() is used by the download-item
 
1395
        template and is usually more useful to determine what's
 
1396
        actually happening with an item.  get_channel_category() is
 
1397
        used by by the channel template to figure out which heading to
 
1398
        put an item under.
 
1399
 
 
1400
        * downloading and not-downloaded are grouped together as
 
1401
          not-downloaded
 
1402
        * Newly downloaded and downloading items are always new if
 
1403
          their feed hasn't been marked as viewed after the item's pub
 
1404
          date.  This is so that when a user gets a list of items and
 
1405
          starts downloading them, the list doesn't reorder itself.
 
1406
          Once they start watching them, then it reorders itself.
 
1407
        """
 
1408
 
 
1409
        self.confirm_db_thread()
 
1410
        if self.downloader is None or not self.downloader.is_finished():
 
1411
            if not self.get_viewed():
 
1412
                return u'new'
 
1413
            if self.expired:
 
1414
                return u'expired'
 
1415
            else:
 
1416
                return u'not-downloaded'
 
1417
        elif not self.get_seen():
 
1418
            if not self.get_viewed():
 
1419
                return u'new'
 
1420
            return u'newly-downloaded'
 
1421
        elif self.get_expiring():
 
1422
            return u'expiring'
 
1423
        else:
 
1424
            return u'saved'
 
1425
 
 
1426
    def is_uploading(self):
 
1427
        """Returns true if this item is currently uploading.  This
 
1428
        only happens for torrents.
 
1429
        """
 
1430
        return self.downloader and self.downloader.get_state() == u'uploading'
 
1431
 
 
1432
    def is_uploading_paused(self):
 
1433
        """Returns true if this item is uploading but paused.  This
 
1434
        only happens for torrents.
 
1435
        """
 
1436
        return (self.downloader
 
1437
                and self.downloader.get_state() == u'uploading-paused')
 
1438
 
 
1439
    def is_downloadable(self):
 
1440
        return self.get_state() in (u'new', u'not-downloaded', u'expired')
 
1441
 
 
1442
    def is_downloaded(self):
 
1443
        return self.get_state() in (u"newly-downloaded", u"expiring", u"saved")
 
1444
 
 
1445
    def show_save_button(self):
 
1446
        return (self.get_state() in (u'newly-downloaded', u'expiring')
 
1447
                and not self.keep)
 
1448
 
 
1449
    def get_size_for_display(self):
 
1450
        """Returns the size of the item to be displayed.
 
1451
        """
 
1452
        return util.format_size_for_user(self.get_size())
 
1453
 
 
1454
    def get_size(self):
 
1455
        if not hasattr(self, "_size"):
 
1456
            self._size = self._get_size()
 
1457
        return self._size
 
1458
 
 
1459
    def _get_size(self):
 
1460
        """Returns the size of the item. We use the following methods
 
1461
        to get the size:
 
1462
 
 
1463
        1. Physical size of a downloaded file
 
1464
        2. HTTP content-length
 
1465
        3. RSS enclosure tag value
 
1466
        """
 
1467
        if self.is_downloaded():
 
1468
            if self.get_filename() is None:
 
1469
                return 0
 
1470
            try:
 
1471
                fname = self.get_filename()
 
1472
                return os.path.getsize(fname)
 
1473
            except OSError:
 
1474
                return 0
 
1475
        elif self.has_downloader():
 
1476
            return self.downloader.get_total_size()
 
1477
        else:
 
1478
            if self.enclosure_size is not None:
 
1479
                return self.enclosure_size
 
1480
        return 0
 
1481
 
 
1482
    def download_progress(self):
 
1483
        """Returns the download progress in absolute percentage [0.0 -
 
1484
        100.0].
 
1485
        """
 
1486
        self.confirm_db_thread()
 
1487
        if self.downloader is None:
 
1488
            return 0
 
1489
        else:
 
1490
            size = self.get_size()
 
1491
            dled = self.downloader.get_current_size()
 
1492
            if size == 0:
 
1493
                return 0
 
1494
            else:
 
1495
                return (100.0*dled) / size
 
1496
 
 
1497
    @returns_unicode
 
1498
    def get_startup_activity(self):
 
1499
        if self.pendingManualDL:
 
1500
            return self.pendingReason
 
1501
        elif self.downloader:
 
1502
            return self.downloader.get_startup_activity()
 
1503
        else:
 
1504
            return _("starting up...")
 
1505
 
 
1506
    def get_pub_date_parsed(self):
 
1507
        """Returns the published date of the item as a datetime object.
 
1508
        """
 
1509
        return self.get_release_date_obj()
 
1510
 
 
1511
    def get_release_date_obj(self):
 
1512
        """Returns the date this video was released or when it was
 
1513
        published.
 
1514
        """
 
1515
        return self.releaseDateObj
 
1516
 
 
1517
    def get_duration_value(self):
 
1518
        """Returns the length of the video in seconds.
 
1519
        """
 
1520
        secs = 0
 
1521
        if self.duration not in (-1, None):
 
1522
            secs = self.duration / 1000
 
1523
        return secs
 
1524
 
 
1525
    @returns_unicode
 
1526
    def get_format(self, empty_for_unknown=True):
 
1527
        """Returns string with the format of the video.
 
1528
        """
 
1529
        if self.looks_like_torrent():
 
1530
            return u'.torrent'
 
1531
 
 
1532
        if self.downloader:
 
1533
            if ((self.downloader.contentType
 
1534
                 and "/" in self.downloader.contentType)):
 
1535
                mtype, subtype = self.downloader.contentType.split('/', 1)
 
1536
                mtype = mtype.lower()
 
1537
                if mtype in KNOWN_MIME_TYPES:
 
1538
                    format_ = subtype.split(';')[0].upper()
 
1539
                    if mtype == u'audio':
 
1540
                        format_ += u' AUDIO'
 
1541
                    if format_.startswith(u'X-'):
 
1542
                        format_ = format_[2:]
 
1543
                    return (u'.%s' %
 
1544
                            MIME_SUBSITUTIONS.get(format_, format_).lower())
 
1545
 
 
1546
        if self.enclosure_format is not None:
 
1547
            return self.enclosure_format
 
1548
 
 
1549
        if empty_for_unknown:
 
1550
            return u""
 
1551
        return u"unknown"
 
1552
 
 
1553
    @returns_unicode
 
1554
    def get_license(self):
 
1555
        """Return the license associated with the video.
 
1556
        """
 
1557
        self.confirm_db_thread()
 
1558
        if self.license:
 
1559
            return self.license
 
1560
        return self.get_feed().get_license()
 
1561
 
 
1562
    @returns_unicode
 
1563
    def get_comments_link(self):
 
1564
        """Returns the comments link if it exists in the feed item.
 
1565
        """
 
1566
        return self.comments_link
 
1567
 
 
1568
    def get_link(self):
 
1569
        """Returns the URL of the webpage associated with the item.
 
1570
        """
 
1571
        return self.link
 
1572
 
 
1573
    def get_payment_link(self):
 
1574
        """Returns the URL of the payment page associated with the
 
1575
        item.
 
1576
        """
 
1577
        return self.payment_link
 
1578
 
 
1579
    def update(self, entry):
 
1580
        """Updates an item with new data
 
1581
 
 
1582
        entry - dict containing the new data
 
1583
        """
 
1584
        self.update_from_feed_parser_values(FeedParserValues(entry))
 
1585
 
 
1586
    def update_from_feed_parser_values(self, fp_values):
 
1587
        fp_values.update_item(self)
 
1588
        self.icon_cache.request_update()
 
1589
        self.signal_change()
 
1590
 
 
1591
    def on_download_finished(self):
 
1592
        """Called when the download for this item finishes."""
 
1593
 
 
1594
        self.confirm_db_thread()
 
1595
        self.downloadedTime = datetime.now()
 
1596
        self.set_filename(self.downloader.get_filename())
 
1597
        self.split_item()
 
1598
        self.signal_change()
 
1599
        self._replace_file_items()
 
1600
        self.check_media_file(signal_change=False)
 
1601
 
 
1602
        for other in Item.make_view('downloader_id IS NULL AND url=?',
 
1603
                (self.url,)):
 
1604
            other.set_downloader(self.downloader)
 
1605
        self.recalc_feed_counts()
 
1606
 
 
1607
    def check_media_file(self, signal_change=True):
 
1608
        if filetypes.is_other_filename(self.filename):
 
1609
            self.file_type = u'other'
 
1610
            self.media_type_checked = True
 
1611
            if signal_change:
 
1612
                self.signal_change()
 
1613
        else:
 
1614
            moviedata.movie_data_updater.request_update(self)
 
1615
 
 
1616
    def on_downloader_migrated(self, old_filename, new_filename):
 
1617
        self.set_filename(new_filename)
 
1618
        self.signal_change()
 
1619
        if self.isContainerItem:
 
1620
            self.migrate_children(self.get_filename())
 
1621
        self._replace_file_items()
 
1622
 
 
1623
    def _replace_file_items(self):
 
1624
        """Remove any FileItems that share our filename from the DB.
 
1625
 
 
1626
        This fixes a race condition during migrate, where we can create
 
1627
        FileItems that duplicate existing Items.  See #12253 for details.
 
1628
        """
 
1629
        view = Item.make_view('is_file_item AND filename=? AND id !=?',
 
1630
                (filename_to_unicode(self.filename), self.id))
 
1631
        for dup in view:
 
1632
            dup.remove()
 
1633
 
 
1634
    def set_downloader(self, new_downloader):
 
1635
        if self.has_downloader():
 
1636
            if new_downloader is self.downloader:
 
1637
                return
 
1638
            self.downloader.remove_item(self)
 
1639
        # Note: this is the attribute--not the property!
 
1640
        self._downloader = new_downloader
 
1641
        if new_downloader is not None:
 
1642
            self.downloader_id = new_downloader.id
 
1643
            self.was_downloaded = True
 
1644
            new_downloader.add_item(self)
 
1645
        else:
 
1646
            self.downloader_id = None
 
1647
        self.signal_change()
 
1648
 
 
1649
    def save(self):
 
1650
        self.confirm_db_thread()
 
1651
        if self.keep != True:
 
1652
            self.keep = True
 
1653
            self.signal_change()
 
1654
 
 
1655
    @returns_filename
 
1656
    def get_filename(self):
 
1657
        return self.filename
 
1658
 
 
1659
    def is_video_file(self):
 
1660
        return (self.isContainerItem != True
 
1661
                and filetypes.is_video_filename(self.get_filename()))
 
1662
 
 
1663
    def is_audio_file(self):
 
1664
        return (self.isContainerItem != True
 
1665
                and filetypes.is_audio_filename(self.get_filename()))
 
1666
 
 
1667
    def is_external(self):
 
1668
        """Returns True iff this item was not downloaded from a Democracy
 
1669
        channel.
 
1670
        """
 
1671
        return (self.feed_id is not None
 
1672
                and self.get_feed_url() == 'dtv:manualFeed')
 
1673
 
 
1674
    def migrate_children(self, newdir):
 
1675
        if self.isContainerItem:
 
1676
            for item in self.get_children():
 
1677
                item.migrate(newdir)
 
1678
 
 
1679
    def remove(self):
 
1680
        if self.has_downloader():
 
1681
            self.set_downloader(None)
 
1682
        self.remove_icon_cache()
 
1683
        if self.isContainerItem:
 
1684
            for item in self.get_children():
 
1685
                item.remove()
 
1686
        self._remove_from_playlists()
 
1687
        DDBObject.remove(self)
 
1688
 
 
1689
    def setup_links(self):
 
1690
        self.split_item()
 
1691
        if not self.id_exists():
 
1692
            # In split_item() we found out that all our children were
 
1693
            # deleted, so we were removed as well.  (#11979)
 
1694
            return
 
1695
        eventloop.add_idle(self.check_deleted, 'checking item deleted')
 
1696
        if self.screenshot and not fileutil.exists(self.screenshot):
 
1697
            self.screenshot = None
 
1698
            self.signal_change()
 
1699
 
 
1700
    def check_deleted(self):
 
1701
        if not self.id_exists():
 
1702
            return
 
1703
        if (self.isContainerItem is not None and
 
1704
                not fileutil.exists(self.get_filename()) and
 
1705
                not hasattr(app, 'in_unit_tests')):
 
1706
            self.expire()
 
1707
 
 
1708
    def _get_downloader(self):
 
1709
        try:
 
1710
            return self._downloader
 
1711
        except AttributeError:
 
1712
            dler = downloader.get_existing_downloader(self)
 
1713
            if dler is not None:
 
1714
                dler.add_item(self)
 
1715
            self._downloader = dler
 
1716
            return dler
 
1717
    downloader = property(_get_downloader)
 
1718
 
 
1719
    def fix_incorrect_torrent_subdir(self):
 
1720
        """Up to revision 6257, torrent downloads were incorrectly
 
1721
        being created in an extra subdirectory.  This method migrates
 
1722
        those torrent downloads to the correct layout.
 
1723
 
 
1724
        from: /path/to/movies/foobar.mp4/foobar.mp4
 
1725
        to:   /path/to/movies/foobar.mp4
 
1726
        """
 
1727
        filename_path = self.get_filename()
 
1728
        if filename_path is None:
 
1729
            return
 
1730
        if fileutil.isdir(filename_path):
 
1731
            enclosed_file = os.path.join(filename_path,
 
1732
                                        os.path.basename(filename_path))
 
1733
            if fileutil.exists(enclosed_file):
 
1734
                logging.info("Migrating incorrect torrent download: %s",
 
1735
                             enclosed_file)
 
1736
                try:
 
1737
                    temp = filename_path + ".tmp"
 
1738
                    fileutil.move(enclosed_file, temp)
 
1739
                    for turd in os.listdir(fileutil.expand_filename(filename_path)):
 
1740
                        os.remove(turd)
 
1741
                    fileutil.rmdir(filename_path)
 
1742
                    fileutil.rename(temp, filename_path)
 
1743
                except (SystemExit, KeyboardInterrupt):
 
1744
                    raise
 
1745
                except:
 
1746
                    logging.warn("fix_incorrect_torrent_subdir error:\n%s",
 
1747
                                 traceback.format_exc())
 
1748
                self.set_filename(filename_path)
 
1749
 
 
1750
    def __str__(self):
 
1751
        return "Item - %s" % stringify(self.get_title())
 
1752
 
 
1753
class FileItem(Item):
 
1754
    """An Item that exists as a local file
 
1755
    """
 
1756
    def setup_new(self, filename, feed_id=None, parent_id=None,
 
1757
            offsetPath=None, deleted=False, fp_values=None,
 
1758
            channel_title=None, mark_seen=False):
 
1759
        if fp_values is None:
 
1760
            fp_values = fp_values_for_file(filename)
 
1761
        Item.setup_new(self, fp_values, feed_id=feed_id, parent_id=parent_id,
 
1762
                eligibleForAutoDownload=False, channel_title=channel_title)
 
1763
        self.is_file_item = True
 
1764
        check_f(filename)
 
1765
        filename = fileutil.abspath(filename)
 
1766
        self.set_filename(filename)
 
1767
        self.set_release_date()
 
1768
        self.deleted = deleted
 
1769
        self.offsetPath = offsetPath
 
1770
        self.shortFilename = clean_filename(os.path.basename(self.filename))
 
1771
        self.was_downloaded = False
 
1772
        if mark_seen:
 
1773
            self.mark_seen = True
 
1774
            self.watchedTime = datetime.now()
 
1775
        if not fileutil.isdir(self.filename):
 
1776
            # If our file isn't a directory, then we know we are definitely
 
1777
            # not a container item.  Note that the opposite isn't true in the
 
1778
            # case where we are a directory with only 1 file inside.
 
1779
            self.isContainerItem = False
 
1780
        self.check_media_file(signal_change=False)
 
1781
        self.split_item()
 
1782
 
 
1783
    # FileItem downloaders are always None
 
1784
    downloader = property(lambda self: None)
 
1785
 
 
1786
    @returns_unicode
 
1787
    def get_state(self):
 
1788
        if self.deleted:
 
1789
            return u"expired"
 
1790
        elif self.get_seen():
 
1791
            return u"saved"
 
1792
        else:
 
1793
            return u"newly-downloaded"
 
1794
 
 
1795
    def is_eligible_for_auto_download(self):
 
1796
        return False
 
1797
 
 
1798
    def get_channel_category(self):
 
1799
        """Get the category to use for the channel template.
 
1800
 
 
1801
        This method is similar to get_state(), but has some subtle
 
1802
        differences.  get_state() is used by the download-item
 
1803
        template and is usually more useful to determine what's
 
1804
        actually happening with an item.  get_channel_category() is
 
1805
        used by by the channel template to figure out which heading to
 
1806
        put an item under.
 
1807
 
 
1808
        * downloading and not-downloaded are grouped together as
 
1809
          not-downloaded
 
1810
        * Items are always new if their feed hasn't been marked as
 
1811
          viewed after the item's pub date.  This is so that when a
 
1812
          user gets a list of items and starts downloading them, the
 
1813
          list doesn't reorder itself.
 
1814
        * Child items match their parents for expiring, where in
 
1815
          get_state, they always act as not expiring.
 
1816
        """
 
1817
 
 
1818
        self.confirm_db_thread()
 
1819
        if self.deleted:
 
1820
            return u'expired'
 
1821
        elif not self.get_seen():
 
1822
            return u'newly-downloaded'
 
1823
 
 
1824
        if self.parent_id and self.get_parent().get_expiring():
 
1825
            return u'expiring'
 
1826
        else:
 
1827
            return u'saved'
 
1828
 
 
1829
    def get_expiring(self):
 
1830
        return False
 
1831
 
 
1832
    def show_save_button(self):
 
1833
        return False
 
1834
 
 
1835
    def get_viewed(self):
 
1836
        return True
 
1837
 
 
1838
    def is_external(self):
 
1839
        return self.parent_id is None
 
1840
 
 
1841
    def expire(self):
 
1842
        self.confirm_db_thread()
 
1843
        if self.has_parent():
 
1844
            # if we can't find the parent, it's possible that it was
 
1845
            # already deleted.
 
1846
            try:
 
1847
                old_parent = self.get_parent()
 
1848
            except ObjectNotFoundError:
 
1849
                old_parent = None
 
1850
        else:
 
1851
            old_parent = None
 
1852
        if not fileutil.exists(self.filename):
 
1853
            # item whose file has been deleted outside of Miro
 
1854
            self.remove()
 
1855
        elif self.has_parent():
 
1856
            self.make_deleted()
 
1857
        else:
 
1858
            # external item that the user deleted in Miro
 
1859
            url = self.get_feed_url()
 
1860
            if ((url.startswith("dtv:manualFeed")
 
1861
                 or url.startswith("dtv:singleFeed"))):
 
1862
                self.remove()
 
1863
            else:
 
1864
                self.make_deleted()
 
1865
        if old_parent is not None and old_parent.get_children().count() == 0:
 
1866
            old_parent.expire()
 
1867
 
 
1868
    def make_deleted(self):
 
1869
        self._remove_from_playlists()
 
1870
        self.downloadedTime = None
 
1871
        self.parent_id = None
 
1872
        self.feed_id = models.Feed.get_manual_feed().id
 
1873
        self.deleted = True
 
1874
        self.signal_change()
 
1875
 
 
1876
    def delete_files(self):
 
1877
        if self.has_parent():
 
1878
            dler = self.get_parent().downloader
 
1879
            if dler is not None and not dler.child_deleted:
 
1880
                dler.stop_upload()
 
1881
                dler.child_deleted = True
 
1882
                dler.signal_change()
 
1883
                for sibling in self.get_parent().get_children():
 
1884
                    sibling.signal_change(needs_save=False)
 
1885
        try:
 
1886
            if fileutil.isfile(self.filename):
 
1887
                fileutil.remove(self.filename)
 
1888
            elif fileutil.isdir(self.filename):
 
1889
                fileutil.rmtree(self.filename)
 
1890
        except (SystemExit, KeyboardInterrupt):
 
1891
            raise
 
1892
        except:
 
1893
            logging.warn("delete_files error:\n%s", traceback.format_exc())
 
1894
 
 
1895
    def download(self, autodl=False):
 
1896
        self.deleted = False
 
1897
        self.signal_change()
 
1898
 
 
1899
    def set_filename(self, filename):
 
1900
        Item.set_filename(self, filename)
 
1901
 
 
1902
    def set_release_date(self):
 
1903
        try:
 
1904
            self.releaseDateObj = datetime.fromtimestamp(
 
1905
                fileutil.getmtime(self.filename))
 
1906
        except OSError:
 
1907
            logging.warn("Error setting release date:\n%s",
 
1908
                    traceback.format_exc())
 
1909
            self.releaseDateObj = datetime.now()
 
1910
 
 
1911
    def get_release_date_obj(self):
 
1912
        if self.parent_id:
 
1913
            return self.get_parent().releaseDateObj
 
1914
        else:
 
1915
            return self.releaseDateObj
 
1916
 
 
1917
    def migrate(self, newdir):
 
1918
        self.confirm_db_thread()
 
1919
        if self.parent_id:
 
1920
            parent = self.get_parent()
 
1921
            self.filename = os.path.join(parent.get_filename(),
 
1922
                                         self.offsetPath)
 
1923
            self.signal_change()
 
1924
            return
 
1925
        if self.shortFilename is None:
 
1926
            logging.warn("""\
 
1927
can't migrate download because we don't have a shortFilename!
 
1928
filename was %s""", stringify(self.filename))
 
1929
            return
 
1930
        new_filename = os.path.join(newdir, self.shortFilename)
 
1931
        if self.filename == new_filename:
 
1932
            return
 
1933
        if fileutil.exists(self.filename):
 
1934
            new_filename = next_free_filename(new_filename)
 
1935
            def callback():
 
1936
                self.filename = new_filename
 
1937
                self.signal_change()
 
1938
            fileutil.migrate_file(self.filename, new_filename, callback)
 
1939
        elif fileutil.exists(new_filename):
 
1940
            self.filename = new_filename
 
1941
            self.signal_change()
 
1942
        self.migrate_children(newdir)
 
1943
 
 
1944
    def setup_links(self):
 
1945
        if self.shortFilename is None:
 
1946
            if self.parent_id is None:
 
1947
                self.shortFilename = clean_filename(
 
1948
                    os.path.basename(self.filename))
 
1949
            else:
 
1950
                parent_file = self.get_parent().get_filename()
 
1951
                if self.filename.startswith(parent_file):
 
1952
                    self.shortFilename = clean_filename(
 
1953
                        self.filename[len(parent_file):])
 
1954
                else:
 
1955
                    logging.warn("%s is not a subdirectory of %s",
 
1956
                            self.filename, parent_file)
 
1957
        Item.setup_links(self)
 
1958
 
 
1959
def fp_values_for_file(filename, title=None, description=None):
 
1960
    data = {
 
1961
            'enclosures': [{'url': resources.url(filename)}]
 
1962
    }
 
1963
    if title is None:
 
1964
        data['title'] = filename_to_unicode(os.path.basename(filename))
 
1965
    else:
 
1966
        data['title'] = title
 
1967
    if description is not None:
 
1968
        data['description'] = description
 
1969
    return FeedParserValues(FeedParserDict(data))
 
1970
 
 
1971
def update_incomplete_movie_data():
 
1972
    for item in chain(Item.downloaded_view(), Item.file_items_view()):
 
1973
        if ((item.duration is None or item.duration == -1 or
 
1974
             item.screenshot is None or not item.media_type_checked)):
 
1975
            item.check_media_file()
 
1976
 
 
1977
def fix_non_container_parents():
 
1978
    """Make sure all items referenced by parent_id have isContainerItem set
 
1979
 
 
1980
    Bug #12906 has a database where this was not so.
 
1981
    """
 
1982
    where_sql = ("(isContainerItem = 0 OR isContainerItem IS NULL) AND "
 
1983
            "id IN (SELECT parent_id FROM item)")
 
1984
    for item in Item.make_view(where_sql):
 
1985
        logging.warn("parent_id points to %s but isContainerItem == %r. "
 
1986
                "Setting isContainerItem to True", item.id,
 
1987
                item.isContainerItem)
 
1988
        item.isContainerItem = True
 
1989
        item.signal_change()
 
1990
 
 
1991
def move_orphaned_items():
 
1992
    manual_feed = models.Feed.get_manual_feed()
 
1993
    feedless_items = []
 
1994
    parentless_items = []
 
1995
 
 
1996
    for item in Item.orphaned_from_feed_view():
 
1997
        logging.warn("No feed for Item: %s.  Moving to manual", item.id)
 
1998
        item.set_feed(manual_feed.id)
 
1999
        feedless_items.append('%s: %s' % (item.id, item.url))
 
2000
 
 
2001
    for item in Item.orphaned_from_parent_view():
 
2002
        logging.warn("No parent for Item: %s.  Moving to manual", item.id)
 
2003
        item.parent_id = None
 
2004
        item.set_feed(manual_feed.id)
 
2005
        parentless_items.append('%s: %s' % (item.id, item.url))
 
2006
 
 
2007
    if feedless_items:
 
2008
        databaselog.info("Moved items to manual feed because their feed was "
 
2009
                "gone: %s", ', '.join(feedless_items))
 
2010
    if parentless_items:
 
2011
        databaselog.info("Moved items to manual feed because their parent was "
 
2012
                "gone: %s", ', '.join(parentless_items))
 
2013