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
from miro.eventloop import as_idle
40
from miro import config
41
from miro import prefs
42
from miro import signals
44
from miro import fileutil
45
from miro.plat.utils import FilenameType, kill_process, movie_data_program_info
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
51
# Time to sleep while we're polling the external movie command
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")
59
def thumbnail_directory():
60
dir_ = os.path.join(config.get(prefs.ICON_CACHE_DIRECTORY), "extracted")
62
fileutil.makedirs(dir_)
63
except (KeyboardInterrupt, SystemExit):
69
class MovieDataInfo(object):
70
"""Little utility class to keep track of data associated with each
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.
78
def __init__(self, item):
80
self.video_path = item.get_filename()
81
if self.video_path is None:
82
self._program_info = None
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(),
91
if hasattr(app, 'in_unit_tests'):
92
self._program_info = None
94
def _get_program_info(self):
96
return self._program_info
97
except AttributeError:
98
self._calc_program_info()
99
return self._program_info
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)
107
program_info = property(_get_program_info)
109
class MovieDataUpdater(signals.SignalEmitter):
111
signals.SignalEmitter.__init__(self, 'begin-loop', 'end-loop')
112
self.in_shutdown = False
113
self.queue = Queue.Queue()
116
def start_thread(self):
117
self.thread = threading.Thread(name='Movie Data Thread',
118
target=self.thread_loop)
119
self.thread.setDaemon(True)
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
129
self.emit('end-loop')
133
screenshot_worked = False
135
command_line, env = mdi.program_info
136
stdout = self.run_movie_data_program(command_line, env)
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):
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
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,
158
self.update_finished(mdi.item, duration, screenshot, mediatype)
159
except StandardError:
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')
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)
180
if pipe.poll() is None:
181
logging.info("Movie data process running after shutdown, "
183
self.kill_process(pipe.pid)
185
return pipe.stdout.read()
187
def kill_process(self, pid):
190
except (KeyboardInterrupt, SystemExit):
193
logging.warn("Error trying to kill the movie data process:\n%s",
194
traceback.format_exc())
196
logging.info("Movie data process killed")
198
def parse_duration(self, stdout):
199
duration_match = DURATION_RE.search(stdout)
201
return int(duration_match.group(1))
205
def parse_type(self, stdout):
206
type_match = TYPE_RE.search(stdout)
208
return type_match.group(1)
213
def update_finished(self, item, duration, screenshot, mediatype):
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
223
def request_update(self, item):
226
filename = item.get_filename()
227
if not filename or not fileutil.isfile(filename):
229
if item.downloader and not item.downloader.is_finished():
231
if item.updating_movie_info:
234
item.updating_movie_info = True
235
self.queue.put(MovieDataInfo(item))
238
self.in_shutdown = True
241
if self.thread is not None:
244
movie_data_updater = MovieDataUpdater()