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

« back to all changes in this revision

Viewing changes to portable/moviedata.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
 
from miro.eventloop import as_idle
30
 
import os.path
31
 
import re
32
 
import subprocess
33
 
import time
34
 
import traceback
35
 
import threading
36
 
import Queue
37
 
import logging
38
 
 
39
 
from miro import app
40
 
from miro import config
41
 
from miro import prefs
42
 
from miro import signals
43
 
from miro import util
44
 
from miro import fileutil
45
 
from miro.plat.utils import FilenameType, kill_process, movie_data_program_info
46
 
 
47
 
# Time in seconds that we wait for the utility to execute.  If it goes
48
 
# longer than this, we assume it's hung and kill it.
49
 
MOVIE_DATA_UTIL_TIMEOUT = 120
50
 
 
51
 
# Time to sleep while we're polling the external movie command
52
 
SLEEP_DELAY = 0.1
53
 
 
54
 
DURATION_RE = re.compile("Miro-Movie-Data-Length: (\d+)")
55
 
TYPE_RE = re.compile("Miro-Movie-Data-Type: (audio|video|other)")
56
 
THUMBNAIL_SUCCESS_RE = re.compile("Miro-Movie-Data-Thumbnail: Success")
57
 
TRY_AGAIN_RE = re.compile("Miro-Try-Again: True")
58
 
 
59
 
def thumbnail_directory():
60
 
    dir_ = os.path.join(config.get(prefs.ICON_CACHE_DIRECTORY), "extracted")
61
 
    try:
62
 
        fileutil.makedirs(dir_)
63
 
    except (KeyboardInterrupt, SystemExit):
64
 
        raise
65
 
    except:
66
 
        pass
67
 
    return dir_
68
 
 
69
 
class MovieDataInfo(object):
70
 
    """Little utility class to keep track of data associated with each
71
 
    movie.  This is:
72
 
 
73
 
    * The item.
74
 
    * The path to the video.
75
 
    * Path to the thumbnail we're trying to make.
76
 
    * List of commands that we're trying to run, and their environments.
77
 
    """
78
 
    def __init__(self, item):
79
 
        self.item = item
80
 
        self.video_path = item.get_filename()
81
 
        if self.video_path is None:
82
 
            self._program_info = None
83
 
            return
84
 
        # add a random string to the filename to ensure it's unique.
85
 
        # Two videos can have the same basename if they're in
86
 
        # different directories.
87
 
        thumbnail_filename = '%s.%s.png' % (os.path.basename(self.video_path),
88
 
                                            util.random_string(5))
89
 
        self.thumbnail_path = os.path.join(thumbnail_directory(),
90
 
                                           thumbnail_filename)
91
 
        if hasattr(app, 'in_unit_tests'):
92
 
            self._program_info = None
93
 
 
94
 
    def _get_program_info(self):
95
 
        try:
96
 
            return self._program_info
97
 
        except AttributeError:
98
 
            self._calc_program_info()
99
 
            return self._program_info
100
 
 
101
 
    def _calc_program_info(self):
102
 
        videopath = fileutil.expand_filename(self.video_path)
103
 
        thumbnailpath = fileutil.expand_filename(self.thumbnail_path)
104
 
        command_line, env = movie_data_program_info(videopath, thumbnailpath)
105
 
        self._program_info = (command_line, env)
106
 
 
107
 
    program_info = property(_get_program_info)
108
 
 
109
 
class MovieDataUpdater(signals.SignalEmitter):
110
 
    def __init__ (self):
111
 
        signals.SignalEmitter.__init__(self, 'begin-loop', 'end-loop')
112
 
        self.in_shutdown = False
113
 
        self.queue = Queue.Queue()
114
 
        self.thread = None
115
 
 
116
 
    def start_thread(self):
117
 
        self.thread = threading.Thread(name='Movie Data Thread',
118
 
                                       target=self.thread_loop)
119
 
        self.thread.setDaemon(True)
120
 
        self.thread.start()
121
 
 
122
 
    def thread_loop(self):
123
 
        while not self.in_shutdown:
124
 
            self.emit('begin-loop')
125
 
            mdi = self.queue.get(block=True)
126
 
            if mdi is None or mdi.program_info is None:
127
 
                # shutdown() was called or there's no moviedata
128
 
                # implemented.
129
 
                self.emit('end-loop')
130
 
                break
131
 
            try:
132
 
                duration = -1
133
 
                screenshot_worked = False
134
 
                screenshot = None
135
 
                command_line, env = mdi.program_info
136
 
                stdout = self.run_movie_data_program(command_line, env)
137
 
 
138
 
                # if the moviedata program tells us to try again, we move
139
 
                # along without updating the item at all
140
 
                if TRY_AGAIN_RE.search(stdout):
141
 
                    continue
142
 
 
143
 
                if duration == -1:
144
 
                    duration = self.parse_duration(stdout)
145
 
                mediatype = self.parse_type(stdout)
146
 
                if THUMBNAIL_SUCCESS_RE.search(stdout):
147
 
                    screenshot_worked = True
148
 
                if ((screenshot_worked and
149
 
                     fileutil.exists(mdi.thumbnail_path))):
150
 
                    screenshot = mdi.thumbnail_path
151
 
                else:
152
 
                    # All the programs failed, maybe it's an audio
153
 
                    # file?  Setting it to "" instead of None, means
154
 
                    # that we won't try to take the screenshot again.
155
 
                    screenshot = FilenameType("")
156
 
                logging.debug("moviedata: %s %s %s", duration, screenshot, 
157
 
                              mediatype)
158
 
                self.update_finished(mdi.item, duration, screenshot, mediatype)
159
 
            except StandardError:
160
 
                if self.in_shutdown:
161
 
                    break
162
 
                signals.system.failed_exn(
163
 
                    "When running external movie data program")
164
 
                self.update_finished(mdi.item, -1, None, None)
165
 
            self.emit('end-loop')
166
 
 
167
 
    def run_movie_data_program(self, command_line, env):
168
 
        start_time = time.time()
169
 
        pipe = subprocess.Popen(command_line, stdout=subprocess.PIPE,
170
 
                stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env,
171
 
                startupinfo=util.no_console_startupinfo())
172
 
        while pipe.poll() is None and not self.in_shutdown:
173
 
            time.sleep(SLEEP_DELAY)
174
 
            if time.time() - start_time > MOVIE_DATA_UTIL_TIMEOUT:
175
 
                logging.info("Movie data process hung, killing it")
176
 
                self.kill_process(pipe.pid)
177
 
                return ''
178
 
 
179
 
        if self.in_shutdown:
180
 
            if pipe.poll() is None:
181
 
                logging.info("Movie data process running after shutdown, "
182
 
                             "killing it")
183
 
                self.kill_process(pipe.pid)
184
 
            return ''
185
 
        return pipe.stdout.read()
186
 
 
187
 
    def kill_process(self, pid):
188
 
        try:
189
 
            kill_process(pid)
190
 
        except (KeyboardInterrupt, SystemExit):
191
 
            raise
192
 
        except:
193
 
            logging.warn("Error trying to kill the movie data process:\n%s",
194
 
                         traceback.format_exc())
195
 
        else:
196
 
            logging.info("Movie data process killed")
197
 
 
198
 
    def parse_duration(self, stdout):
199
 
        duration_match = DURATION_RE.search(stdout)
200
 
        if duration_match:
201
 
            return int(duration_match.group(1))
202
 
        else:
203
 
            return -1
204
 
 
205
 
    def parse_type(self, stdout):
206
 
        type_match = TYPE_RE.search(stdout)
207
 
        if type_match:
208
 
            return type_match.group(1)
209
 
        else:
210
 
            return None
211
 
 
212
 
    @as_idle
213
 
    def update_finished(self, item, duration, screenshot, mediatype):
214
 
        if item.id_exists():
215
 
            item.duration = duration
216
 
            item.screenshot = screenshot
217
 
            item.updating_movie_info = False
218
 
            if mediatype is not None:
219
 
                item.file_type = unicode(mediatype)
220
 
                item.media_type_checked = True
221
 
            item.signal_change()
222
 
 
223
 
    def request_update(self, item):
224
 
        if self.in_shutdown:
225
 
            return
226
 
        filename = item.get_filename()
227
 
        if not filename or not fileutil.isfile(filename):
228
 
            return
229
 
        if item.downloader and not item.downloader.is_finished():
230
 
            return
231
 
        if item.updating_movie_info:
232
 
            return
233
 
 
234
 
        item.updating_movie_info = True
235
 
        self.queue.put(MovieDataInfo(item))
236
 
 
237
 
    def shutdown(self):
238
 
        self.in_shutdown = True
239
 
        # wake up our thread
240
 
        self.queue.put(None)
241
 
        if self.thread is not None:
242
 
            self.thread.join()
243
 
 
244
 
movie_data_updater = MovieDataUpdater()