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.
31
In general, frontends should do the following to handle startup.
33
- (optional) call startup.install_movies_gone_handler()
34
- Call startup.initialize()
35
- Wait for either the 'startup-success', or 'startup-failure' signal
38
from miro.gtcache import gettext as _
45
from miro.clock import clock
47
from miro import autodler
48
from miro import autoupdate
49
from miro import config
50
from miro import commandline
51
from miro import crashreport
52
from miro import controller
53
from miro import database
54
from miro import databaselog
55
from miro import databaseupgrade
56
from miro import dialogs
57
from miro import downloader
58
from miro import eventloop
59
from miro import fileutil
60
from miro import guide
61
from miro import httpauth
62
from miro import httpclient
63
from miro import iconcache
66
from miro import folder
67
from miro import messages
68
from miro import messagehandler
69
from miro import models
70
from miro import moviedata
71
from miro import playlist
72
from miro import prefs
73
from miro.plat.utils import setup_logging
74
from miro.plat import config as platformcfg
76
from miro import theme
78
from miro import searchengines
79
from miro import storedatabase
80
from miro import videoconversion
82
DEBUG_DB_MEM_USAGE = False
83
mem_usage_test_event = threading.Event()
85
class StartupError(Exception):
86
def __init__(self, summary, description):
87
self.summary = summary
88
self.description = description
90
def startup_function(func):
91
"""Decorator for startup functions. This decorator catches exceptions and
92
turns them into StartupFailure messages.
94
def wrapped(*args, **kwargs):
97
except StartupError, e:
98
if e.summary is not None:
99
m = messages.StartupFailure(e.summary, e.description)
101
m = messages.FrontendQuit()
103
except (SystemExit, KeyboardInterrupt):
105
except Exception, exc:
106
# we do this so that we only kick up the database error
107
# if it's a database-related exception AND the app has a
109
if ((isinstance(exc, (database.DatabaseException,
110
database.DatabaseStandardError,
111
storedatabase.sqlite3.OperationalError))
112
and app.db is not None)):
114
# somewhere in one of the startup functions, Miro
115
# kicked up a database-related problem. we don't know
116
# where it happend, so we can't just start fresh and
117
# keep going. instead we have to start fresh, shut
118
# miro down, and on the next run, maybe miro will
123
logging.exception("Database error on startup:")
124
m = messages.StartupDatabaseFailure(
126
_("We're sorry, %(appname)s was unable to start up due "
127
"to a problem with the database:\n\n"
128
"Error: %(error)s\n\n"
129
"It's possible that your database file is corrupted and "
130
"cannot be used.\n\n"
131
"You can start fresh and your damaged database will be "
132
"removed, but you will have to re-add your feeds and "
133
"media files. If you want to do this, press the "
134
"Start Fresh button and restart %(appname)s.\n\n"
135
"To help us fix problems like this in the future, "
136
"please file a bug report at %(url)s.",
137
{"appname": config.get(prefs.SHORT_APP_NAME),
138
"url": config.get(prefs.BUG_REPORT_URL),
145
"Unknown startup error: %s", traceback.format_exc())
146
m = messages.StartupFailure(
148
_("An unknown error prevented %(appname)s from startup. "
149
"Please file a bug report at %(url)s.",
150
{"appname": config.get(prefs.SHORT_APP_NAME),
151
"url": config.get(prefs.BUG_REPORT_URL)}
156
def _movies_directory_gone_handler(callback):
157
"""Default _movies_directory_gone_handler. The frontend should
158
override this using the ``install_movies_directory_gone_handler``
161
logging.error("Movies directory is gone -- no handler installed!")
162
eventloop.add_urgent_call(callback, "continuing startup")
164
def install_movies_directory_gone_handler(callback):
165
global _movies_directory_gone_handler
166
_movies_directory_gone_handler = callback
168
def _first_time_handler(callback):
169
"""Default _first_time_handler. The frontend should override this
170
using the ``install_first_time_handler`` function.
172
logging.error("First time -- no handler installed.")
173
eventloop.add_urgent_call(callback, "continuing startup")
175
def install_first_time_handler(callback):
176
global _first_time_handler
177
_first_time_handler = callback
179
def setup_global_feed(url, *args, **kwargs):
180
view = feed.Feed.make_view('origURL=?', (url,))
181
view_count = view.count()
183
logging.info("Spawning global feed %s", url)
184
feed.Feed(url, *args, **kwargs)
186
allFeeds = [f for f in view]
187
for extra in allFeeds[1:]:
189
raise StartupError("Database inconsistent",
190
"Too many db objects for %s" % url)
192
def initialize(themeName):
193
"""Initialize Miro. This sets up things like logging and the config
194
system and should be called as early as possible.
196
# this is platform specific
198
# this is portable general
200
app.controller = controller.Controller()
201
config.load(themeName)
206
This method starts up the eventloop and schedules the rest of the startup
207
to run in the event loop.
209
Frontends should call this method, then wait for 1 of 2 messages
211
StartupSuccess is sent once the startup is done and the backend is ready
214
StartupFailure is sent if something bad happened.
216
initialize() must be called before startup().
218
logging.info("Starting up %s", config.get(prefs.LONG_APP_NAME))
219
logging.info("Version: %s", config.get(prefs.APP_VERSION))
220
logging.info("Revision: %s", config.get(prefs.APP_REVISION))
221
logging.info("Builder: %s", config.get(prefs.BUILD_MACHINE))
222
logging.info("Build Time: %s", config.get(prefs.BUILD_TIME))
223
eventloop.connect('thread-started', finish_startup)
224
logging.info("Reading HTTP Password list")
226
httpauth.restore_from_file()
227
logging.info("Starting libCURL thread")
228
httpclient.init_libcurl()
229
httpclient.start_thread()
230
logging.info("Starting event loop thread")
232
if DEBUG_DB_MEM_USAGE:
233
mem_usage_test_event.wait()
236
def finish_startup(obj, thread):
237
database.set_thread(thread)
238
logging.info("Restoring database...")
240
app.db = storedatabase.LiveStorage()
242
app.db.upgrade_database()
243
except databaseupgrade.DatabaseTooNewError:
244
summary = _("Database too new")
246
"You have a database that was saved with a newer version of "
247
"%(appname)s. You must download the latest version of "
248
"%(appname)s and run that.",
249
{"appname": config.get(prefs.SHORT_APP_NAME)},
251
raise StartupError(summary, description)
252
except storedatabase.UpgradeErrorSendCrashReport, e:
253
send_startup_crash_report(e.report)
255
except storedatabase.UpgradeError:
256
raise StartupError(None, None)
257
database.initialize()
259
logging.timing("Database upgrade time: %.3f", end - start)
260
if app.db.startup_version != app.db.current_version:
261
databaselog.info("Upgraded database from version %s to %s",
262
app.db.startup_version, app.db.current_version)
263
databaselog.print_old_log_entries()
265
if DEBUG_DB_MEM_USAGE:
266
util.db_mem_usage_test()
267
mem_usage_test_event.set()
269
logging.info("Loading video converters...")
270
videoconversion.conversion_manager.startup()
271
searchengines.create_engines()
273
# call fix_database_inconsistencies() ASAP after the manual feed is set up
274
fix_database_inconsistencies()
275
logging.info("setup tabs...")
277
logging.info("setup theme...")
279
install_message_handler()
280
downloader.init_controller()
282
eventloop.add_urgent_call(check_firsttime, "check first time")
284
def fix_database_inconsistencies():
285
item.fix_non_container_parents()
286
item.move_orphaned_items()
287
playlist.fix_missing_item_ids()
288
folder.fix_playlist_missing_item_ids()
291
def check_firsttime():
292
"""Run the first time wizard if need be.
294
callback = lambda: eventloop.add_urgent_call(check_movies_gone, "check movies gone")
296
logging.info("First time -- calling handler.")
297
_first_time_handler(callback)
300
eventloop.add_urgent_call(check_movies_gone, "check movies gone")
303
def check_movies_gone():
304
"""Checks to see if the movies directory is gone.
306
callback = lambda: eventloop.add_urgent_call(fix_movies_gone,
309
if is_movies_directory_gone():
310
logging.info("Movies directory is gone -- calling handler.")
311
_movies_directory_gone_handler(callback)
314
movies_dir = fileutil.expand_filename(config.get(prefs.MOVIES_DIRECTORY))
315
# if the directory doesn't exist, create it.
316
if not os.path.exists(movies_dir):
318
os.makedirs(movies_dir)
320
logging.info("Movies directory can't be created -- calling handler")
321
# FIXME - this isn't technically correct, but it's probably
322
# close enough that a user can fix the issue and Miro can
324
_movies_directory_gone_handler(callback)
327
# make sure the directory is writeable
329
fn = os.path.join(movies_dir, "testfile")
335
_movies_directory_gone_handler(callback)
337
eventloop.add_urgent_call(finish_backend_startup, "reconnect downloaders")
340
def fix_movies_gone():
341
config.set(prefs.MOVIES_DIRECTORY, platformcfg.get(prefs.MOVIES_DIRECTORY))
342
eventloop.add_urgent_call(finish_backend_startup, "reconnect downloaders")
345
def finish_backend_startup():
346
"""Last bit of startup required before we load the frontend. """
347
# Uncomment the next line to test startup error handling
348
# raise StartupError("Test Error", "Startup Failed")
349
reconnect_downloaders()
350
guide.download_guides()
351
feed.remove_orphaned_feed_impls()
352
messages.StartupSuccess().send_to_frontend()
354
@eventloop.idle_iterator
355
def on_frontend_started():
356
"""Perform startup actions that should happen after the frontend is
357
already up and running.
359
logging.info("Starting auto downloader...")
360
autodler.start_downloader()
364
logging.info("Starting movie data updates")
365
item.update_incomplete_movie_data()
367
moviedata.movie_data_updater.start_thread()
369
commandline.startup()
371
autoupdate.check_for_updates()
373
# Wait a bit before starting the downloader daemon. It can cause a bunch
374
# of disk/CPU load, so try to avoid it slowing other stuff down.
375
eventloop.add_timeout(5, downloader.startup_downloader,
376
"start downloader daemon")
377
# ditto for feed updates
378
eventloop.add_timeout(30, feed.start_updates, "start feed updates")
379
# ditto for clearing stale icon cache files, except it's the very lowest
381
eventloop.add_timeout(10, clear_icon_cache_orphans, "clear orphans")
383
def setup_global_feeds():
384
setup_global_feed(u'dtv:manualFeed', initiallyAutoDownloadable=False)
385
setup_global_feed(u'dtv:singleFeed', initiallyAutoDownloadable=False)
386
setup_global_feed(u'dtv:search', initiallyAutoDownloadable=False)
387
setup_global_feed(u'dtv:searchDownloads')
388
setup_global_feed(u'dtv:directoryfeed')
391
def setup_tab_order(type):
392
current_tab_orders = list(tabs.TabOrder.view_for_type(type))
393
if len(current_tab_orders) == 0:
394
logging.info("Creating %s tab order" % type)
395
tab_order = tabs.TabOrder(type)
397
current_tab_orders[0].restore_tab_list()
398
setup_tab_order(u'site')
399
setup_tab_order(u'channel')
400
setup_tab_order(u'audio-channel')
401
setup_tab_order(u'playlist')
404
"""Checks to see if this is the first time that Miro has been run.
405
This is to do any first-time setup, show the user the first-time
408
Returns True if yes, False if no.
410
return not config.get(prefs.STARTUP_TASKS_DONE)
412
def mark_first_time():
413
config.set(prefs.STARTUP_TASKS_DONE, True)
415
def is_movies_directory_gone():
416
"""Checks to see if the MOVIES_DIRECTORY exists.
418
Returns True if yes, False if no.
420
movies_dir = fileutil.expand_filename(config.get(prefs.MOVIES_DIRECTORY))
421
if not movies_dir.endswith(os.path.sep):
422
movies_dir += os.path.sep
423
logging.info("Checking movies directory %r...", movies_dir)
425
if os.path.exists(movies_dir):
426
contents = os.listdir(movies_dir)
428
# there's something inside the directory consider it
429
# present (even if all our items are missing).
433
# we can't access the directory--treat it as if it's gone.
434
logging.info("Can't access directory.")
437
# at this point either there's no movies_dir or there is an empty
438
# movies_dir. we check to see if we think something is downloaded.
439
for downloader_ in downloader.RemoteDownloader.make_view():
440
if ((downloader_.is_finished()
441
and downloader_.get_filename().startswith(movies_dir))):
442
# we think something is downloaded, so it seems like the
443
# movies directory is gone.
444
logging.info("Directory there, but missing files.")
447
# we have no content, so everything's fine.
451
themeHistory = _get_theme_history()
452
themeHistory.check_new_theme()
454
def install_message_handler():
455
handler = messagehandler.BackendMessageHandler(on_frontend_started)
456
messages.BackendMessage.install_handler(handler)
458
def _get_theme_history():
459
current_themes = list(theme.ThemeHistory.make_view())
460
if len(current_themes) > 0:
461
return current_themes[0]
463
return theme.ThemeHistory()
465
@eventloop.idle_iterator
466
def clear_icon_cache_orphans():
467
# delete icon_cache rows from the database with no associated
470
for ic in iconcache.IconCache.orphaned_view():
471
logging.warn("No object for IconCache: %s. Discarding", ic)
473
removed_objs.append(str(ic.url))
475
databaselog.info("Removed IconCache objects without an associated "
476
"db object: %s", ','.join(removed_objs))
479
# delete files in the icon cache directory that don't belong to IconCache
482
cachedir = fileutil.expand_filename(config.get(prefs.ICON_CACHE_DIRECTORY))
483
if not os.path.isdir(cachedir):
486
existingFiles = [os.path.normcase(os.path.join(cachedir, f))
487
for f in os.listdir(cachedir)]
490
knownIcons = iconcache.IconCache.all_filenames()
493
knownIcons = [ os.path.normcase(fileutil.expand_filename(path)) for path in
497
for filename in existingFiles:
498
if (os.path.exists(filename)
499
and os.path.basename(filename)[0] != '.'
500
and os.path.basename(filename) != 'extracted'
501
and not filename in knownIcons):
508
def send_startup_crash_report(report):
509
logging.info("Startup failed, waiting to send crash report")
510
title = _("Submitting Crash Report")
512
"%(appname)s will now submit a crash report to our crash "
514
"Do you want to include entire program database "
515
"including all video and feed metadata with crash report? "
516
"This will help us diagnose the issue.",
517
{"appname": config.get(prefs.SHORT_APP_NAME)})
518
d = dialogs.ChoiceDialog(title, description,
519
dialogs.BUTTON_INCLUDE_DATABASE,
520
dialogs.BUTTON_DONT_INCLUDE_DATABASE)
521
choice = d.run_blocking()
522
send_database = (choice == dialogs.BUTTON_INCLUDE_DATABASE)
523
app.controller.send_bug_report(report, '', send_database, quit_after=True)
525
def reconnect_downloaders():
526
for downloader_ in downloader.RemoteDownloader.orphaned_view():
527
logging.warn("removing orphaned downloader: %s", downloader_.url)
529
manualItems = item.Item.feed_view(feed.Feed.get_manual_feed().get_id())
530
for item_ in manualItems:
531
if item_.downloader is None and item_.__class__ == item.Item:
532
logging.warn("removing cancelled external torrent: %s", item_)