1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
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.
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.
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
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
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.
29
"""``miro.item`` -- Holds ``Item`` class and related things.
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,
38
from miro.plat.utils import filename_to_unicode, unicode_to_filename
43
from miro.download_utils import clean_filename, next_free_filename
44
from miro.feedparser import FeedParserDict
46
from miro.database import (DDBObject, ObjectNotFoundError,
47
DatabaseConstraintError)
48
from miro.databasehelper import make_simple_get_set
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
58
from miro import moviedata
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
66
_charset = locale.getpreferredencoding()
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'
78
class FeedParserValues(object):
79
"""Helper class to get values from feedparser entries
81
FeedParserValues objects inspect the FeedParserDict for the entry
82
attribute for various attributes using in Item (entry_title,
85
def __init__(self, entry):
87
self.first_video_enclosure = get_first_video_enclosure(entry)
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(),
105
def update_item(self, item):
106
for key, value in self.data.items():
107
setattr(item, key, value)
109
def compare_to_item(self, item):
110
for key, value in self.data.items():
111
if getattr(item, key) != value:
115
def compare_to_item_enclosures(self, item):
117
'url', 'enclosure_size', 'enclosure_type',
120
for key in compare_keys:
121
if getattr(item, key) != self.data[key]:
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)
131
if ((self.first_video_enclosure
132
and 'url' in self.first_video_enclosure)):
133
return self.first_video_enclosure['url'].decode("ascii",
137
def _calc_thumbnail_url(self):
138
"""Returns a link to the thumbnail of the video. """
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)
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)
153
# Try to get the thumbnail for our entry
154
return self._get_element_thumbnail(self.entry)
156
def _get_element_thumbnail(self, element):
158
thumb = element["thumbnail"]
161
if isinstance(thumb, str):
163
elif isinstance(thumb, unicode):
164
return thumb.decode('ascii', 'replace')
166
return thumb["url"].decode('ascii', 'replace')
167
except (KeyError, AttributeError):
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.
174
Both first_video_enclosure and entry are FeedParserDicts,
175
which does some fancy footwork with normalizing feed entry
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)
187
def _calc_link(self):
188
if hasattr(self.entry, "link"):
189
link = self.entry.link
190
if isinstance(link, dict):
197
if isinstance(link, unicode):
200
return link.decode('ascii', 'replace')
201
except UnicodeDecodeError:
202
return link.decode('ascii', 'ignore')
205
def _calc_payment_link(self):
207
return self.first_video_enclosure.payment_url.decode('ascii',
211
return self.entry.payment_url.decode('ascii','replace')
215
def _calc_comments_link(self):
216
return self.entry.get('comments', u"")
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)
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", ""):
230
return int(enc['length'])
231
except (KeyError, ValueError):
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']
241
def _calc_enclosure_format(self):
242
enclosure = self.first_video_enclosure
245
extension = enclosure['url'].split('.')[-1]
246
extension = extension.lower().encode('ascii', 'replace')
247
except (SystemExit, KeyboardInterrupt):
251
# Hack for mp3s, "mpeg audio" isn't clear enough
252
if extension.lower() == u'mp3':
254
if enclosure.get('type'):
255
enc = enclosure['type'].decode('ascii', 'replace')
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':
263
if format.startswith(u'X-'):
266
MIME_SUBSITUTIONS.get(format, format).lower())
268
if extension in KNOWN_MIME_SUBTYPES:
269
return u'.%s' % extension
272
def _calc_release_date(self):
274
return datetime(*self.first_video_enclosure.updated_parsed[0:7])
275
except (SystemExit, KeyboardInterrupt):
279
return datetime(*self.entry.updated_parsed[0:7])
280
except (SystemExit, KeyboardInterrupt):
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.
291
ICON_CACHE_VITAL = False
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
301
self.autoDownloaded = False
302
self.pendingManualDL = False
303
self.downloadedTime = None
304
self.watchedTime = None
305
self.pendingReason = u""
307
self.description = u""
308
fp_values.update_item(self)
311
self.filename = self.file_type = None
312
self.eligibleForAutoDownload = eligibleForAutoDownload
314
self.screenshot = None
315
self.media_type_checked = False
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
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()
334
def setup_restored(self):
338
def setup_common(self):
339
self.selected = False
342
self.showMoreInfo = False
343
self.updating_movie_info = False
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'})
353
def manual_pending_view(cls):
354
return cls.make_view('pendingManualDL')
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'})
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'})
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'})
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'})
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'})
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'})
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'})
406
def newly_downloaded_view(cls):
407
return cls.make_view("NOT item.seen AND "
408
"(item.file_type != 'other') AND "
410
"rd.state in ('finished', 'uploading', 'uploading-paused'))",
411
joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
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'})
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'})
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'})
438
def toplevel_view(cls):
439
return cls.make_view('feed_id IS NOT NULL')
442
def feed_view(cls, feed_id):
443
return cls.make_view('feed_id=?', (feed_id,))
446
def visible_feed_view(cls, feed_id):
447
return cls.make_view('feed_id=? AND (deleted IS NULL or not deleted)',
451
def visible_folder_view(cls, folder_id):
452
return cls.make_view('folder_id=? AND (deleted IS NULL or not deleted)',
454
joins={'feed': 'item.feed_id=feed.id'})
457
def folder_contents_view(cls, folder_id):
458
return cls.make_view('parent_id=?', (folder_id,))
461
def feed_downloaded_view(cls, feed_id):
462
return cls.make_view("feed_id=? AND "
463
"rd.state in ('finished', 'uploading', 'uploading-paused')",
465
joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
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",
473
joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
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",
481
joins={'feed': 'item.feed_id=feed.id'})
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)',
489
joins={'feed': 'item.feed_id=feed.id'})
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'))",
497
joins={'remote_downloader AS rd': 'item.downloader_id=rd.id'})
500
def children_view(cls, parent_id):
501
return cls.make_view('parent_id=?', (parent_id,))
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')
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')
517
def search_item_view(cls):
518
return cls.make_view("feed.origURL == 'dtv:search'",
519
joins={'feed': 'item.feed_id=feed.id'})
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'})
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'})
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'})
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'})
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)
565
def media_children_view(cls, parent_id):
566
return cls.make_view("parent_id=? AND "
567
"file_type IN ('video', 'audio')", (parent_id,))
570
def containers_view(cls):
571
return cls.make_view("isContainerItem")
574
def file_items_view(cls):
575
return cls.make_view("is_file_item")
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)')
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)')
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).
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()
601
def downloader_view(cls, dler_id):
602
return cls.make_view("downloader_id=?", (dler_id,))
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())
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)
614
def _find_child_paths(self):
615
"""If this item points to a directory, return the set all files
616
under that directory.
618
filename_root = self.get_filename()
619
if fileutil.isdir(filename_root):
620
return set(fileutil.miro_allfiles(filename_root))
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")
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)
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().
641
if not self.isContainerItem:
643
if self.get_state() == 'downloading':
644
# don't try to find videos that we're in the middle of
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)
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()))):
664
filename_root = self.get_filename()
665
if filename_root is None:
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)
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)
679
self.downloader.migrate (target_dir)
680
self.isContainerItem = False
682
self.isContainerItem = False
686
def set_subtitle_encoding(self, encoding):
687
if encoding is not None:
688
self.subtitle_encoding = unicode(encoding)
689
config_value = encoding
691
self.subtitle_encoding = None
693
config.set(prefs.SUBTITLE_ENCODING, config_value)
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)
701
def set_file_type(self, file_type):
702
self.file_type = file_type
705
def _file_type_for_filename(self, filename):
706
filename = filename.lower()
707
for ext in filetypes.VIDEO_EXTENSIONS:
708
if filename.endswith(ext):
710
for ext in filetypes.AUDIO_EXTENSIONS:
711
if filename.endswith(ext):
715
def matches_search(self, search_string):
716
if search_string is None:
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())
725
if search.match(search_string, [title.lower(), desc.lower(),
731
def _remove_from_playlists(self):
732
models.PlaylistItemMap.remove_item_from_playlists(self)
733
models.PlaylistFolderItemMap.remove_item_from_playlists(self)
735
def check_constraints(self):
736
if self.feed_id is not None:
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)
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():
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)
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")
767
def on_signal_change(self):
770
if hasattr(self, "_state"):
772
if hasattr(self, "_size"):
775
def _sync_title(self):
776
# for torrents that aren't from a feed, we use the filename
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))
786
def recalc_feed_counts(self):
787
self.get_feed().recalc_counts()
789
def get_viewed(self):
790
"""Returns True iff this item has never been viewed in the
793
Note the difference between "viewed" and seen.
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
803
"""Returns the URL associated with the first enclosure in the
808
def has_shareable_url(self):
809
"""Does this item have a URL that the user can share with
812
This returns True when the item has a non-file URL.
815
return url != u'' and not url.startswith(u"file:")
818
"""Returns the feed this item came from.
822
except AttributeError:
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()
833
def get_parent(self):
834
if hasattr(self, "_parent"):
837
if self.has_parent():
838
self._parent = Item.get_by_id(self.parent_id)
844
def get_feed_url(self):
845
return self.get_feed().origURL
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():
855
return self.get_parent().get_title()
856
except ObjectNotFoundError:
860
def get_children(self):
861
if self.isContainerItem:
862
return Item.children_view(self.id)
864
raise ValueError("%s is not a container item" % self)
866
def children_signal_change(self):
867
for child in self.get_children():
868
child.signal_change(needs_save=False)
870
def is_playable(self):
871
"""Is this a playable item?"""
873
if self.isContainerItem:
874
return Item.media_children_view(self.id).count() > 0
876
return self.file_type in ('audio', 'video')
878
def set_feed(self, feed_id):
879
"""Moves this item to another feed.
881
self.feed_id = feed_id
882
# _feed is created by get_feed which caches the result
883
if hasattr(self, "_feed"):
885
if self.isContainerItem:
886
for item in self.get_children():
887
if hasattr(item, "_feed"):
893
self.confirm_db_thread()
894
self._remove_from_playlists()
895
if not self.is_external():
899
fileutil.remove(self.screenshot)
900
except (SystemExit, KeyboardInterrupt):
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():
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)
919
self.seen = self.keep = self.pendingManualDL = False
921
self.file_type = self.watchedTime = self.duration = None
922
self.isContainerItem = None
924
self.recalc_feed_counts()
926
def has_downloader(self):
927
return self.downloader_id is not None and self.downloader is not None
929
def has_parent(self):
930
return self.parent_id is not None
932
def is_main_item(self):
933
return (self.has_downloader() and
934
self.downloader.main_item_id == self.id)
936
def downloader_state(self):
937
if not self.has_downloader():
940
return self.downloader.state
942
def stop_upload(self):
944
self.downloader.stop_upload()
945
if self.isContainerItem:
946
self.children_signal_change()
948
def pause_upload(self):
950
self.downloader.pause_upload()
951
if self.isContainerItem:
952
self.children_signal_change()
954
def start_upload(self):
956
self.downloader.start_upload()
957
if self.isContainerItem:
958
self.children_signal_change()
960
def get_expiration_time(self):
961
"""Returns the time when this item should expire.
963
Returns a datetime.datetime object or None if it doesn't expire.
965
self.confirm_db_thread()
966
if self.get_watched_time() is None or not self.is_downloaded():
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):
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
982
def get_watched_time(self):
983
"""Returns the most recent watched time of this item or any
986
Returns a datetime.datetime instance or None if the item and none
987
of its children have been watched.
989
if not self.get_seen():
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
998
if child_time > self.watchedTime:
999
self.watchedTime = child_time
1000
self.signal_change()
1001
return self.watchedTime
1003
def get_expiring(self):
1004
if self.expiring is None:
1005
if not self.get_seen():
1006
self.expiring = False
1008
self.expiring = False
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
1017
self.expiring = True
1018
return self.expiring
1021
"""Returns true iff video has been seen.
1023
Note the difference between "viewed" and "seen".
1025
self.confirm_db_thread()
1028
def mark_item_seen(self, mark_other_items=True):
1029
"""Marks the item as seen.
1031
self.confirm_db_thread()
1032
if self.isContainerItem:
1033
for child in self.get_children():
1035
child.signal_change()
1036
if self.seen == False:
1038
if self.subtitle_encoding is None:
1039
config_value = config.get(prefs.SUBTITLE_ENCODING)
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:
1049
item.mark_item_seen(False)
1050
self.recalc_feed_counts()
1052
def update_parent_seen(self):
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()
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():
1067
item.signal_change()
1070
self.watchedTime = None
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:
1077
item.mark_item_unseen(False)
1078
self.recalc_feed_counts()
1081
def get_rss_id(self):
1082
self.confirm_db_thread()
1085
def remove_rss_id(self):
1086
self.confirm_db_thread()
1088
self.signal_change()
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()
1097
def set_resume_time(self, position):
1098
if not self.id_exists():
1101
position = int(position)
1103
logging.exception("set_resume_time: not-saving! given non-int %s",
1106
if self.resumeTime != position:
1107
self.resumeTime = position
1108
self.signal_change()
1110
def get_auto_downloaded(self):
1111
"""Returns true iff item was auto downloaded.
1113
self.confirm_db_thread()
1114
return self.autoDownloaded
1116
def download(self, autodl=False):
1117
"""Starts downloading the item.
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
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()
1131
self.set_auto_downloaded(autodl)
1132
self.pendingManualDL = False
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()
1142
self.downloader.start()
1143
self.signal_change()
1144
self.recalc_feed_counts()
1148
self.downloader.pause()
1151
self.download(self.get_auto_downloaded())
1153
def is_pending_manual_download(self):
1154
self.confirm_db_thread()
1155
return self.pendingManualDL
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()
1163
def is_eligible_for_auto_download(self):
1164
self.confirm_db_thread()
1165
if self.was_downloaded:
1167
ufeed = self.get_feed()
1168
if ufeed.getEverything:
1170
return self.eligibleForAutoDownload
1172
def is_pending_auto_download(self):
1173
return (self.get_feed().is_autodownloadable() and
1174
self.is_eligible_for_auto_download())
1177
def get_thumbnail_url(self):
1178
return self.thumbnail_url
1181
def get_thumbnail(self):
1182
"""NOTE: When changing this function, change feed.icon_changed
1183
to signal the right set of items.
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")
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")
1202
return resources.path("images/thumb-default-video.png")
1204
def is_downloaded_torrent(self):
1205
return (self.isContainerItem and self.has_downloader() and
1206
self.downloader.is_finished())
1209
def get_title(self):
1210
"""Returns the title of the item.
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')
1222
def set_title(self, title):
1223
self.confirm_db_thread()
1225
self.signal_change()
1227
def set_description(self, desc):
1228
self.confirm_db_thread()
1229
self.description = desc
1230
self.signal_change()
1232
def set_channel_title(self, title):
1234
self.channelTitle = title
1235
self.signal_change()
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()
1248
elif self.channelTitle:
1249
return self.channelTitle
1254
def get_description(self):
1255
"""Returns the description of the video (unicode).
1257
If the item is a torrent, then it adds some additional text.
1259
if self.description:
1260
if self.is_downloaded_torrent():
1261
return (unicode(self.description) + u'<BR>' +
1262
_('Contents appear in the library'))
1264
return unicode(self.description)
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'))
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)
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
1286
if self.has_downloader():
1287
return self.downloader.get_type() == u'bittorrent'
1289
return filetypes.is_torrent_filename(self.get_url())
1291
def torrent_seeding_status(self):
1292
"""Get the torrent seeding status for this torrent.
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
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':
1306
if downloader_.get_state() == 'uploading':
1311
def is_transferring(self):
1312
return (self.downloader
1313
and self.downloader.get_state() in (u'uploading',
1316
def delete_files(self):
1317
"""Stops downloading the item.
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():
1326
self.delete_subtitle_files()
1328
def delete_subtitle_files(self):
1329
"""Deletes subtitle files associated with this item.
1331
files = util.gather_subtitle_files(self.get_filename())
1333
fileutil.delete(mem)
1335
def get_state(self):
1336
"""Get the state of this item. The state will be on of the
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.
1347
Uses caching to prevent recalculating state over and over
1351
except AttributeError:
1356
def _calc_state(self):
1357
"""Recalculate the state of an item after a change
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'
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'
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'
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'
1387
self._state = u'saved'
1390
def get_channel_category(self):
1391
"""Get the category to use for the channel template.
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
1400
* downloading and not-downloaded are grouped together as
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.
1409
self.confirm_db_thread()
1410
if self.downloader is None or not self.downloader.is_finished():
1411
if not self.get_viewed():
1416
return u'not-downloaded'
1417
elif not self.get_seen():
1418
if not self.get_viewed():
1420
return u'newly-downloaded'
1421
elif self.get_expiring():
1426
def is_uploading(self):
1427
"""Returns true if this item is currently uploading. This
1428
only happens for torrents.
1430
return self.downloader and self.downloader.get_state() == u'uploading'
1432
def is_uploading_paused(self):
1433
"""Returns true if this item is uploading but paused. This
1434
only happens for torrents.
1436
return (self.downloader
1437
and self.downloader.get_state() == u'uploading-paused')
1439
def is_downloadable(self):
1440
return self.get_state() in (u'new', u'not-downloaded', u'expired')
1442
def is_downloaded(self):
1443
return self.get_state() in (u"newly-downloaded", u"expiring", u"saved")
1445
def show_save_button(self):
1446
return (self.get_state() in (u'newly-downloaded', u'expiring')
1449
def get_size_for_display(self):
1450
"""Returns the size of the item to be displayed.
1452
return util.format_size_for_user(self.get_size())
1455
if not hasattr(self, "_size"):
1456
self._size = self._get_size()
1459
def _get_size(self):
1460
"""Returns the size of the item. We use the following methods
1463
1. Physical size of a downloaded file
1464
2. HTTP content-length
1465
3. RSS enclosure tag value
1467
if self.is_downloaded():
1468
if self.get_filename() is None:
1471
fname = self.get_filename()
1472
return os.path.getsize(fname)
1475
elif self.has_downloader():
1476
return self.downloader.get_total_size()
1478
if self.enclosure_size is not None:
1479
return self.enclosure_size
1482
def download_progress(self):
1483
"""Returns the download progress in absolute percentage [0.0 -
1486
self.confirm_db_thread()
1487
if self.downloader is None:
1490
size = self.get_size()
1491
dled = self.downloader.get_current_size()
1495
return (100.0*dled) / size
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()
1504
return _("starting up...")
1506
def get_pub_date_parsed(self):
1507
"""Returns the published date of the item as a datetime object.
1509
return self.get_release_date_obj()
1511
def get_release_date_obj(self):
1512
"""Returns the date this video was released or when it was
1515
return self.releaseDateObj
1517
def get_duration_value(self):
1518
"""Returns the length of the video in seconds.
1521
if self.duration not in (-1, None):
1522
secs = self.duration / 1000
1526
def get_format(self, empty_for_unknown=True):
1527
"""Returns string with the format of the video.
1529
if self.looks_like_torrent():
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:]
1544
MIME_SUBSITUTIONS.get(format_, format_).lower())
1546
if self.enclosure_format is not None:
1547
return self.enclosure_format
1549
if empty_for_unknown:
1554
def get_license(self):
1555
"""Return the license associated with the video.
1557
self.confirm_db_thread()
1560
return self.get_feed().get_license()
1563
def get_comments_link(self):
1564
"""Returns the comments link if it exists in the feed item.
1566
return self.comments_link
1569
"""Returns the URL of the webpage associated with the item.
1573
def get_payment_link(self):
1574
"""Returns the URL of the payment page associated with the
1577
return self.payment_link
1579
def update(self, entry):
1580
"""Updates an item with new data
1582
entry - dict containing the new data
1584
self.update_from_feed_parser_values(FeedParserValues(entry))
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()
1591
def on_download_finished(self):
1592
"""Called when the download for this item finishes."""
1594
self.confirm_db_thread()
1595
self.downloadedTime = datetime.now()
1596
self.set_filename(self.downloader.get_filename())
1598
self.signal_change()
1599
self._replace_file_items()
1600
self.check_media_file(signal_change=False)
1602
for other in Item.make_view('downloader_id IS NULL AND url=?',
1604
other.set_downloader(self.downloader)
1605
self.recalc_feed_counts()
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
1612
self.signal_change()
1614
moviedata.movie_data_updater.request_update(self)
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()
1623
def _replace_file_items(self):
1624
"""Remove any FileItems that share our filename from the DB.
1626
This fixes a race condition during migrate, where we can create
1627
FileItems that duplicate existing Items. See #12253 for details.
1629
view = Item.make_view('is_file_item AND filename=? AND id !=?',
1630
(filename_to_unicode(self.filename), self.id))
1634
def set_downloader(self, new_downloader):
1635
if self.has_downloader():
1636
if new_downloader is self.downloader:
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)
1646
self.downloader_id = None
1647
self.signal_change()
1650
self.confirm_db_thread()
1651
if self.keep != True:
1653
self.signal_change()
1656
def get_filename(self):
1657
return self.filename
1659
def is_video_file(self):
1660
return (self.isContainerItem != True
1661
and filetypes.is_video_filename(self.get_filename()))
1663
def is_audio_file(self):
1664
return (self.isContainerItem != True
1665
and filetypes.is_audio_filename(self.get_filename()))
1667
def is_external(self):
1668
"""Returns True iff this item was not downloaded from a Democracy
1671
return (self.feed_id is not None
1672
and self.get_feed_url() == 'dtv:manualFeed')
1674
def migrate_children(self, newdir):
1675
if self.isContainerItem:
1676
for item in self.get_children():
1677
item.migrate(newdir)
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():
1686
self._remove_from_playlists()
1687
DDBObject.remove(self)
1689
def setup_links(self):
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)
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()
1700
def check_deleted(self):
1701
if not self.id_exists():
1703
if (self.isContainerItem is not None and
1704
not fileutil.exists(self.get_filename()) and
1705
not hasattr(app, 'in_unit_tests')):
1708
def _get_downloader(self):
1710
return self._downloader
1711
except AttributeError:
1712
dler = downloader.get_existing_downloader(self)
1713
if dler is not None:
1715
self._downloader = dler
1717
downloader = property(_get_downloader)
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.
1724
from: /path/to/movies/foobar.mp4/foobar.mp4
1725
to: /path/to/movies/foobar.mp4
1727
filename_path = self.get_filename()
1728
if filename_path is None:
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",
1737
temp = filename_path + ".tmp"
1738
fileutil.move(enclosed_file, temp)
1739
for turd in os.listdir(fileutil.expand_filename(filename_path)):
1741
fileutil.rmdir(filename_path)
1742
fileutil.rename(temp, filename_path)
1743
except (SystemExit, KeyboardInterrupt):
1746
logging.warn("fix_incorrect_torrent_subdir error:\n%s",
1747
traceback.format_exc())
1748
self.set_filename(filename_path)
1751
return "Item - %s" % stringify(self.get_title())
1753
class FileItem(Item):
1754
"""An Item that exists as a local file
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
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
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)
1783
# FileItem downloaders are always None
1784
downloader = property(lambda self: None)
1787
def get_state(self):
1790
elif self.get_seen():
1793
return u"newly-downloaded"
1795
def is_eligible_for_auto_download(self):
1798
def get_channel_category(self):
1799
"""Get the category to use for the channel template.
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
1808
* downloading and not-downloaded are grouped together as
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.
1818
self.confirm_db_thread()
1821
elif not self.get_seen():
1822
return u'newly-downloaded'
1824
if self.parent_id and self.get_parent().get_expiring():
1829
def get_expiring(self):
1832
def show_save_button(self):
1835
def get_viewed(self):
1838
def is_external(self):
1839
return self.parent_id is None
1842
self.confirm_db_thread()
1843
if self.has_parent():
1844
# if we can't find the parent, it's possible that it was
1847
old_parent = self.get_parent()
1848
except ObjectNotFoundError:
1852
if not fileutil.exists(self.filename):
1853
# item whose file has been deleted outside of Miro
1855
elif self.has_parent():
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"))):
1865
if old_parent is not None and old_parent.get_children().count() == 0:
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
1874
self.signal_change()
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:
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)
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):
1893
logging.warn("delete_files error:\n%s", traceback.format_exc())
1895
def download(self, autodl=False):
1896
self.deleted = False
1897
self.signal_change()
1899
def set_filename(self, filename):
1900
Item.set_filename(self, filename)
1902
def set_release_date(self):
1904
self.releaseDateObj = datetime.fromtimestamp(
1905
fileutil.getmtime(self.filename))
1907
logging.warn("Error setting release date:\n%s",
1908
traceback.format_exc())
1909
self.releaseDateObj = datetime.now()
1911
def get_release_date_obj(self):
1913
return self.get_parent().releaseDateObj
1915
return self.releaseDateObj
1917
def migrate(self, newdir):
1918
self.confirm_db_thread()
1920
parent = self.get_parent()
1921
self.filename = os.path.join(parent.get_filename(),
1923
self.signal_change()
1925
if self.shortFilename is None:
1927
can't migrate download because we don't have a shortFilename!
1928
filename was %s""", stringify(self.filename))
1930
new_filename = os.path.join(newdir, self.shortFilename)
1931
if self.filename == new_filename:
1933
if fileutil.exists(self.filename):
1934
new_filename = next_free_filename(new_filename)
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)
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))
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):])
1955
logging.warn("%s is not a subdirectory of %s",
1956
self.filename, parent_file)
1957
Item.setup_links(self)
1959
def fp_values_for_file(filename, title=None, description=None):
1961
'enclosures': [{'url': resources.url(filename)}]
1964
data['title'] = filename_to_unicode(os.path.basename(filename))
1966
data['title'] = title
1967
if description is not None:
1968
data['description'] = description
1969
return FeedParserValues(FeedParserDict(data))
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()
1977
def fix_non_container_parents():
1978
"""Make sure all items referenced by parent_id have isContainerItem set
1980
Bug #12906 has a database where this was not so.
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()
1991
def move_orphaned_items():
1992
manual_feed = models.Feed.get_manual_feed()
1994
parentless_items = []
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))
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))
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))