Multimedia support - Commiting from a not yet finished state - Details below
* DONE Initially testing with arista ** DONE Video display templates *** TODO Multi-browser support ** TODO Video thumbnails ** TODO Link to original video ** TODO Video cropping Also contains a lot of "debug" print's
This commit is contained in:
parent
9122a9d047
commit
93bdab9daa
@ -107,3 +107,11 @@ def user_add_forgot_password_token_and_expires(database):
|
|||||||
{'fp_token_expire': {'$exists': False}},
|
{'fp_token_expire': {'$exists': False}},
|
||||||
{'$set': {'fp_token_expire': None}},
|
{'$set': {'fp_token_expire': None}},
|
||||||
multi=True)
|
multi=True)
|
||||||
|
|
||||||
|
|
||||||
|
@RegisterMigration(7)
|
||||||
|
def media_type_image_to_multimedia_type_image(database):
|
||||||
|
database['media_entries'].update(
|
||||||
|
{'media_type': 'image'},
|
||||||
|
{'$set': {'media_type': 'mediagoblin.media_types.image'}},
|
||||||
|
multi=True)
|
||||||
|
@ -17,8 +17,13 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from mediagoblin.media_types import get_media_types
|
||||||
|
|
||||||
|
|
||||||
MANDATORY_CELERY_IMPORTS = ['mediagoblin.process_media']
|
MANDATORY_CELERY_IMPORTS = ['mediagoblin.process_media']
|
||||||
|
MANDATORY_CELERY_IMPORTS = [i for i in get_media_types()]
|
||||||
|
|
||||||
|
print(MANDATORY_CELERY_IMPORTS)
|
||||||
|
|
||||||
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
|
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
|
||||||
|
|
||||||
|
70
mediagoblin/media_types/__init__.py
Normal file
70
mediagoblin/media_types/__init__.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class FileTypeNotSupported(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidFileType(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
MEDIA_TYPES = [
|
||||||
|
'mediagoblin.media_types.image',
|
||||||
|
'mediagoblin.media_types.video']
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_types():
|
||||||
|
for media_type in MEDIA_TYPES:
|
||||||
|
yield media_type
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_managers():
|
||||||
|
for media_type in get_media_types():
|
||||||
|
'''
|
||||||
|
FIXME
|
||||||
|
__import__ returns the lowest-level module. If the plugin is located
|
||||||
|
outside the conventional plugin module tree, it will not be loaded
|
||||||
|
properly because of the [...]ugin.media_types.
|
||||||
|
|
||||||
|
We need this if we want to support a separate site-specific plugin
|
||||||
|
folder.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
__import__(media_type)
|
||||||
|
except ImportError as e:
|
||||||
|
raise Exception('ERROR: Could not import {0}: {1}'.format(media_type, e))
|
||||||
|
|
||||||
|
yield media_type, sys.modules[media_type].MEDIA_MANAGER
|
||||||
|
|
||||||
|
def get_media_manager(_media_type = None):
|
||||||
|
for media_type, manager in get_media_managers():
|
||||||
|
if media_type in _media_type:
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_type_and_manager(filename):
|
||||||
|
for media_type, manager in get_media_managers():
|
||||||
|
if filename.find('.') > 0:
|
||||||
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
else:
|
||||||
|
raise InvalidFileType(
|
||||||
|
'Could not find any file extension in "{0}"'.format(
|
||||||
|
filename))
|
||||||
|
|
||||||
|
if ext[1:] in manager['accepted_extensions']:
|
||||||
|
return media_type, manager
|
28
mediagoblin/media_types/image/__init__.py
Normal file
28
mediagoblin/media_types/image/__init__.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from mediagoblin.media_types.image.processing import process_media
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_MANAGER = {
|
||||||
|
"human_readable": "Image",
|
||||||
|
"processor": process_media, # alternately a string,
|
||||||
|
# 'mediagoblin.media_types.image.processing'?
|
||||||
|
"display_template": "mediagoblin/media_displays/image.html",
|
||||||
|
"default_thumb": "images/media_thumbs/image.jpg",
|
||||||
|
"accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"],
|
||||||
|
"accepted_mimetypes": [
|
||||||
|
"image/jpeg", "image/png", "image/gif", "image/tiff"]}
|
207
mediagoblin/media_types/image/processing.py
Normal file
207
mediagoblin/media_types/image/processing.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import Image
|
||||||
|
|
||||||
|
from celery.task import Task
|
||||||
|
from celery import registry
|
||||||
|
|
||||||
|
from mediagoblin.db.util import ObjectId
|
||||||
|
from mediagoblin import mg_globals as mgg
|
||||||
|
|
||||||
|
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||||
|
|
||||||
|
THUMB_SIZE = 180, 180
|
||||||
|
MEDIUM_SIZE = 640, 640
|
||||||
|
|
||||||
|
|
||||||
|
def create_pub_filepath(entry, filename):
|
||||||
|
return mgg.public_store.get_unique_filepath(
|
||||||
|
['media_entries',
|
||||||
|
unicode(entry['_id']),
|
||||||
|
filename])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProcessingFail(Exception):
|
||||||
|
"""
|
||||||
|
Base exception that all other processing failure messages should
|
||||||
|
subclass from.
|
||||||
|
|
||||||
|
You shouldn't call this itself; instead you should subclass it
|
||||||
|
and provid the exception_path and general_message applicable to
|
||||||
|
this error.
|
||||||
|
"""
|
||||||
|
general_message = u''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exception_path(self):
|
||||||
|
return u"%s:%s" % (
|
||||||
|
self.__class__.__module__, self.__class__.__name__)
|
||||||
|
|
||||||
|
def __init__(self, **metadata):
|
||||||
|
self.metadata = metadata or {}
|
||||||
|
|
||||||
|
|
||||||
|
class BadMediaFail(BaseProcessingFail):
|
||||||
|
"""
|
||||||
|
Error that should be raised when an inappropriate file was given
|
||||||
|
for the media type specified.
|
||||||
|
"""
|
||||||
|
general_message = _(u'Invalid file given for media type.')
|
||||||
|
|
||||||
|
|
||||||
|
################################
|
||||||
|
# Media processing initial steps
|
||||||
|
################################
|
||||||
|
|
||||||
|
class ProcessMedia(Task):
|
||||||
|
"""
|
||||||
|
Pass this entry off for processing.
|
||||||
|
"""
|
||||||
|
def run(self, media_id):
|
||||||
|
"""
|
||||||
|
Pass the media entry off to the appropriate processing function
|
||||||
|
(for now just process_image...)
|
||||||
|
"""
|
||||||
|
entry = mgg.database.MediaEntry.one(
|
||||||
|
{'_id': ObjectId(media_id)})
|
||||||
|
|
||||||
|
# Try to process, and handle expected errors.
|
||||||
|
try:
|
||||||
|
process_image(entry)
|
||||||
|
except BaseProcessingFail, exc:
|
||||||
|
mark_entry_failed(entry[u'_id'], exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
entry['state'] = u'processed'
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||||
|
"""
|
||||||
|
If the processing failed we should mark that in the database.
|
||||||
|
|
||||||
|
Assuming that the exception raised is a subclass of BaseProcessingFail,
|
||||||
|
we can use that to get more information about the failure and store that
|
||||||
|
for conveying information to users about the failure, etc.
|
||||||
|
"""
|
||||||
|
entry_id = args[0]
|
||||||
|
mark_entry_failed(entry_id, exc)
|
||||||
|
|
||||||
|
|
||||||
|
process_media = registry.tasks[ProcessMedia.name]
|
||||||
|
|
||||||
|
|
||||||
|
def mark_entry_failed(entry_id, exc):
|
||||||
|
"""
|
||||||
|
Mark a media entry as having failed in its conversion.
|
||||||
|
|
||||||
|
Uses the exception that was raised to mark more information. If the
|
||||||
|
exception is a derivative of BaseProcessingFail then we can store extra
|
||||||
|
information that can be useful for users telling them why their media failed
|
||||||
|
to process.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- entry_id: The id of the media entry
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Was this a BaseProcessingFail? In other words, was this a
|
||||||
|
# type of error that we know how to handle?
|
||||||
|
if isinstance(exc, BaseProcessingFail):
|
||||||
|
# Looks like yes, so record information about that failure and any
|
||||||
|
# metadata the user might have supplied.
|
||||||
|
mgg.database['media_entries'].update(
|
||||||
|
{'_id': entry_id},
|
||||||
|
{'$set': {u'state': u'failed',
|
||||||
|
u'fail_error': exc.exception_path,
|
||||||
|
u'fail_metadata': exc.metadata}})
|
||||||
|
else:
|
||||||
|
# Looks like no, so just mark it as failed and don't record a
|
||||||
|
# failure_error (we'll assume it wasn't handled) and don't record
|
||||||
|
# metadata (in fact overwrite it if somehow it had previous info
|
||||||
|
# here)
|
||||||
|
mgg.database['media_entries'].update(
|
||||||
|
{'_id': entry_id},
|
||||||
|
{'$set': {u'state': u'failed',
|
||||||
|
u'fail_error': None,
|
||||||
|
u'fail_metadata': {}}})
|
||||||
|
|
||||||
|
|
||||||
|
def process_image(entry):
|
||||||
|
"""
|
||||||
|
Code to process an image
|
||||||
|
"""
|
||||||
|
workbench = mgg.workbench_manager.create_workbench()
|
||||||
|
|
||||||
|
queued_filepath = entry['queued_media_file']
|
||||||
|
queued_filename = workbench.localized_file(
|
||||||
|
mgg.queue_store, queued_filepath,
|
||||||
|
'source')
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumb = Image.open(queued_filename)
|
||||||
|
except IOError:
|
||||||
|
raise BadMediaFail()
|
||||||
|
|
||||||
|
thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
|
||||||
|
# ensure color mode is compatible with jpg
|
||||||
|
if thumb.mode != "RGB":
|
||||||
|
thumb = thumb.convert("RGB")
|
||||||
|
|
||||||
|
thumb_filepath = create_pub_filepath(entry, 'thumbnail.jpg')
|
||||||
|
thumb_file = mgg.public_store.get_file(thumb_filepath, 'w')
|
||||||
|
|
||||||
|
with thumb_file:
|
||||||
|
thumb.save(thumb_file, "JPEG", quality=90)
|
||||||
|
|
||||||
|
# If the size of the original file exceeds the specified size of a `medium`
|
||||||
|
# file, a `medium.jpg` files is created and later associated with the media
|
||||||
|
# entry.
|
||||||
|
medium = Image.open(queued_filename)
|
||||||
|
medium_processed = False
|
||||||
|
|
||||||
|
if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]:
|
||||||
|
medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS)
|
||||||
|
|
||||||
|
if medium.mode != "RGB":
|
||||||
|
medium = medium.convert("RGB")
|
||||||
|
|
||||||
|
medium_filepath = create_pub_filepath(entry, 'medium.jpg')
|
||||||
|
medium_file = mgg.public_store.get_file(medium_filepath, 'w')
|
||||||
|
|
||||||
|
with medium_file:
|
||||||
|
medium.save(medium_file, "JPEG", quality=90)
|
||||||
|
medium_processed = True
|
||||||
|
|
||||||
|
# we have to re-read because unlike PIL, not everything reads
|
||||||
|
# things in string representation :)
|
||||||
|
queued_file = file(queued_filename, 'rb')
|
||||||
|
|
||||||
|
with queued_file:
|
||||||
|
original_filepath = create_pub_filepath(entry, queued_filepath[-1])
|
||||||
|
|
||||||
|
with mgg.public_store.get_file(original_filepath, 'wb') as original_file:
|
||||||
|
original_file.write(queued_file.read())
|
||||||
|
|
||||||
|
mgg.queue_store.delete_file(queued_filepath)
|
||||||
|
entry['queued_media_file'] = []
|
||||||
|
media_files_dict = entry.setdefault('media_files', {})
|
||||||
|
media_files_dict['thumb'] = thumb_filepath
|
||||||
|
media_files_dict['original'] = original_filepath
|
||||||
|
if medium_processed:
|
||||||
|
media_files_dict['medium'] = medium_filepath
|
||||||
|
|
||||||
|
# clean up workbench
|
||||||
|
workbench.destroy_self()
|
26
mediagoblin/media_types/video/__init__.py
Normal file
26
mediagoblin/media_types/video/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from mediagoblin.media_types.video.processing import process_media
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_MANAGER = {
|
||||||
|
"human_readable": "Video",
|
||||||
|
"processor": process_media, # alternately a string,
|
||||||
|
# 'mediagoblin.media_types.image.processing'?
|
||||||
|
"display_template": "mediagoblin/media_displays/video.html",
|
||||||
|
"default_thumb": "images/media_thumbs/video.jpg",
|
||||||
|
"accepted_extensions": ["mp4", "mov", "webm", "avi", "3gp", "3gpp"]}
|
260
mediagoblin/media_types/video/processing.py
Normal file
260
mediagoblin/media_types/video/processing.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||||
|
# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import Image
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from celery.task import Task
|
||||||
|
from celery import registry
|
||||||
|
|
||||||
|
from mediagoblin.db.util import ObjectId
|
||||||
|
from mediagoblin import mg_globals as mgg
|
||||||
|
|
||||||
|
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||||
|
|
||||||
|
import gobject
|
||||||
|
|
||||||
|
import gst
|
||||||
|
import arista
|
||||||
|
|
||||||
|
from arista.transcoder import TranscoderOptions
|
||||||
|
|
||||||
|
THUMB_SIZE = 180, 180
|
||||||
|
MEDIUM_SIZE = 640, 640
|
||||||
|
ARISTA_DEVICE_KEY = 'web'
|
||||||
|
|
||||||
|
|
||||||
|
loop = None
|
||||||
|
|
||||||
|
|
||||||
|
def process_video(entry):
|
||||||
|
"""
|
||||||
|
Code to process a video
|
||||||
|
"""
|
||||||
|
info = {}
|
||||||
|
workbench = mgg.workbench_manager.create_workbench()
|
||||||
|
|
||||||
|
queued_filepath = entry['queued_media_file']
|
||||||
|
queued_filename = workbench.localized_file(
|
||||||
|
mgg.queue_store, queued_filepath,
|
||||||
|
'source')
|
||||||
|
|
||||||
|
arista.init()
|
||||||
|
|
||||||
|
devices = arista.presets.get()
|
||||||
|
device = devices[ARISTA_DEVICE_KEY]
|
||||||
|
|
||||||
|
queue = arista.queue.TranscodeQueue()
|
||||||
|
|
||||||
|
info['tmp_file'] = tmp_file = tempfile.NamedTemporaryFile()
|
||||||
|
|
||||||
|
info['medium_filepath'] = medium_filepath = create_pub_filepath(entry, 'video.webm')
|
||||||
|
|
||||||
|
output = tmp_file.name
|
||||||
|
|
||||||
|
uri = 'file://' + queued_filename
|
||||||
|
|
||||||
|
preset = device.presets[device.default]
|
||||||
|
|
||||||
|
opts = TranscoderOptions(uri, preset, output)
|
||||||
|
|
||||||
|
queue.append(opts)
|
||||||
|
|
||||||
|
info['entry'] = entry
|
||||||
|
|
||||||
|
queue.connect("entry-start", entry_start, info)
|
||||||
|
# queue.connect("entry-pass-setup", entry_pass_setup, options)
|
||||||
|
queue.connect("entry-error", entry_error, info)
|
||||||
|
queue.connect("entry-complete", entry_complete, info)
|
||||||
|
|
||||||
|
info['loop'] = loop = gobject.MainLoop()
|
||||||
|
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
# we have to re-read because unlike PIL, not everything reads
|
||||||
|
# things in string representation :)
|
||||||
|
queued_file = file(queued_filename, 'rb')
|
||||||
|
|
||||||
|
with queued_file:
|
||||||
|
original_filepath = create_pub_filepath(entry, queued_filepath[-1])
|
||||||
|
|
||||||
|
with mgg.public_store.get_file(original_filepath, 'wb') as original_file:
|
||||||
|
original_file.write(queued_file.read())
|
||||||
|
|
||||||
|
mgg.queue_store.delete_file(queued_filepath)
|
||||||
|
entry['queued_media_file'] = []
|
||||||
|
media_files_dict = entry.setdefault('media_files', {})
|
||||||
|
media_files_dict['original'] = original_filepath
|
||||||
|
|
||||||
|
# clean up workbench
|
||||||
|
workbench.destroy_self()
|
||||||
|
|
||||||
|
|
||||||
|
def create_pub_filepath(entry, filename):
|
||||||
|
return mgg.public_store.get_unique_filepath(
|
||||||
|
['media_entries',
|
||||||
|
unicode(entry['_id']),
|
||||||
|
filename])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProcessingFail(Exception):
|
||||||
|
"""
|
||||||
|
Base exception that all other processing failure messages should
|
||||||
|
subclass from.
|
||||||
|
|
||||||
|
You shouldn't call this itself; instead you should subclass it
|
||||||
|
and provid the exception_path and general_message applicable to
|
||||||
|
this error.
|
||||||
|
"""
|
||||||
|
general_message = u''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exception_path(self):
|
||||||
|
return u"%s:%s" % (
|
||||||
|
self.__class__.__module__, self.__class__.__name__)
|
||||||
|
|
||||||
|
def __init__(self, **metadata):
|
||||||
|
self.metadata = metadata or {}
|
||||||
|
|
||||||
|
|
||||||
|
class BadMediaFail(BaseProcessingFail):
|
||||||
|
"""
|
||||||
|
Error that should be raised when an inappropriate file was given
|
||||||
|
for the media type specified.
|
||||||
|
"""
|
||||||
|
general_message = _(u'Invalid file given for media type.')
|
||||||
|
|
||||||
|
|
||||||
|
################################
|
||||||
|
# Media processing initial steps
|
||||||
|
################################
|
||||||
|
|
||||||
|
class ProcessMedia(Task):
|
||||||
|
"""
|
||||||
|
Pass this entry off for processing.
|
||||||
|
"""
|
||||||
|
def run(self, media_id):
|
||||||
|
"""
|
||||||
|
Pass the media entry off to the appropriate processing function
|
||||||
|
(for now just process_image...)
|
||||||
|
"""
|
||||||
|
entry = mgg.database.MediaEntry.one(
|
||||||
|
{'_id': ObjectId(media_id)})
|
||||||
|
|
||||||
|
# Try to process, and handle expected errors.
|
||||||
|
try:
|
||||||
|
process_video(entry)
|
||||||
|
except BaseProcessingFail, exc:
|
||||||
|
mark_entry_failed(entry[u'_id'], exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
entry['state'] = u'processed'
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||||
|
"""
|
||||||
|
If the processing failed we should mark that in the database.
|
||||||
|
|
||||||
|
Assuming that the exception raised is a subclass of BaseProcessingFail,
|
||||||
|
we can use that to get more information about the failure and store that
|
||||||
|
for conveying information to users about the failure, etc.
|
||||||
|
"""
|
||||||
|
entry_id = args[0]
|
||||||
|
mark_entry_failed(entry_id, exc)
|
||||||
|
|
||||||
|
|
||||||
|
process_media = registry.tasks[ProcessMedia.name]
|
||||||
|
|
||||||
|
|
||||||
|
def mark_entry_failed(entry_id, exc):
|
||||||
|
"""
|
||||||
|
Mark a media entry as having failed in its conversion.
|
||||||
|
|
||||||
|
Uses the exception that was raised to mark more information. If the
|
||||||
|
exception is a derivative of BaseProcessingFail then we can store extra
|
||||||
|
information that can be useful for users telling them why their media failed
|
||||||
|
to process.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- entry_id: The id of the media entry
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Was this a BaseProcessingFail? In other words, was this a
|
||||||
|
# type of error that we know how to handle?
|
||||||
|
if isinstance(exc, BaseProcessingFail):
|
||||||
|
# Looks like yes, so record information about that failure and any
|
||||||
|
# metadata the user might have supplied.
|
||||||
|
mgg.database['media_entries'].update(
|
||||||
|
{'_id': entry_id},
|
||||||
|
{'$set': {u'state': u'failed',
|
||||||
|
u'fail_error': exc.exception_path,
|
||||||
|
u'fail_metadata': exc.metadata}})
|
||||||
|
else:
|
||||||
|
# Looks like no, so just mark it as failed and don't record a
|
||||||
|
# failure_error (we'll assume it wasn't handled) and don't record
|
||||||
|
# metadata (in fact overwrite it if somehow it had previous info
|
||||||
|
# here)
|
||||||
|
mgg.database['media_entries'].update(
|
||||||
|
{'_id': entry_id},
|
||||||
|
{'$set': {u'state': u'failed',
|
||||||
|
u'fail_error': None,
|
||||||
|
u'fail_metadata': {}}})
|
||||||
|
|
||||||
|
|
||||||
|
def entry_start(queue, entry, options):
|
||||||
|
print(queue, entry, options)
|
||||||
|
|
||||||
|
def entry_complete(queue, entry, info):
|
||||||
|
entry.transcoder.stop()
|
||||||
|
gobject.idle_add(info['loop'].quit)
|
||||||
|
|
||||||
|
with info['tmp_file'] as tmp_file:
|
||||||
|
mgg.public_store.get_file(info['medium_filepath'], 'wb').write(
|
||||||
|
tmp_file.read())
|
||||||
|
info['entry']['media_files']['medium'] = info['medium_filepath']
|
||||||
|
|
||||||
|
print('\n=== DONE! ===\n')
|
||||||
|
|
||||||
|
print(queue, entry, info)
|
||||||
|
|
||||||
|
def entry_error(queue, entry, options):
|
||||||
|
print(queue, entry, options)
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""
|
||||||
|
Handle Ctr-C gracefully and shut down the transcoder.
|
||||||
|
"""
|
||||||
|
global interrupted
|
||||||
|
print
|
||||||
|
print _("Interrupt caught. Cleaning up... (Ctrl-C to force exit)")
|
||||||
|
interrupted = True
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
|
||||||
|
def check_interrupted():
|
||||||
|
"""
|
||||||
|
Check whether we have been interrupted by Ctrl-C and stop the
|
||||||
|
transcoder.
|
||||||
|
"""
|
||||||
|
if interrupted:
|
||||||
|
try:
|
||||||
|
source = transcoder.pipe.get_by_name("source")
|
||||||
|
source.send_event(gst.event_new_eos())
|
||||||
|
except:
|
||||||
|
# Something pretty bad happened... just exit!
|
||||||
|
gobject.idle_add(loop.quit)
|
||||||
|
|
||||||
|
return False
|
||||||
|
return True
|
@ -97,8 +97,14 @@ class CloudFilesStorage(StorageInterface):
|
|||||||
def delete_file(self, filepath):
|
def delete_file(self, filepath):
|
||||||
# TODO: Also delete unused directories if empty (safely, with
|
# TODO: Also delete unused directories if empty (safely, with
|
||||||
# checks to avoid race conditions).
|
# checks to avoid race conditions).
|
||||||
self.container.delete_object(
|
try:
|
||||||
self._resolve_filepath(filepath))
|
self.container.delete_object(
|
||||||
|
self._resolve_filepath(filepath))
|
||||||
|
except cloudfiles.container.ResponseError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def file_url(self, filepath):
|
def file_url(self, filepath):
|
||||||
return '/'.join([
|
return '/'.join([
|
||||||
|
@ -28,8 +28,9 @@ from mediagoblin.util import (
|
|||||||
from mediagoblin.util import pass_to_ugettext as _
|
from mediagoblin.util import pass_to_ugettext as _
|
||||||
from mediagoblin.decorators import require_active_login
|
from mediagoblin.decorators import require_active_login
|
||||||
from mediagoblin.submit import forms as submit_forms, security
|
from mediagoblin.submit import forms as submit_forms, security
|
||||||
from mediagoblin.process_media import process_media, mark_entry_failed
|
from mediagoblin.process_media import mark_entry_failed
|
||||||
from mediagoblin.messages import add_message, SUCCESS
|
from mediagoblin.messages import add_message, SUCCESS
|
||||||
|
from mediagoblin.media_types import get_media_type_and_manager
|
||||||
|
|
||||||
|
|
||||||
@require_active_login
|
@require_active_login
|
||||||
@ -45,15 +46,15 @@ def submit_start(request):
|
|||||||
and request.POST['file'].file):
|
and request.POST['file'].file):
|
||||||
submit_form.file.errors.append(
|
submit_form.file.errors.append(
|
||||||
_(u'You must provide a file.'))
|
_(u'You must provide a file.'))
|
||||||
elif not security.check_filetype(request.POST['file']):
|
|
||||||
submit_form.file.errors.append(
|
|
||||||
_(u"The file doesn't seem to be an image!"))
|
|
||||||
else:
|
else:
|
||||||
filename = request.POST['file'].filename
|
filename = request.POST['file'].filename
|
||||||
|
|
||||||
|
media_type, media_manager = get_media_type_and_manager(filename)
|
||||||
|
|
||||||
# create entry and save in database
|
# create entry and save in database
|
||||||
entry = request.db.MediaEntry()
|
entry = request.db.MediaEntry()
|
||||||
entry['_id'] = ObjectId()
|
entry['_id'] = ObjectId()
|
||||||
|
entry['media_type'] = unicode(media_type)
|
||||||
entry['title'] = (
|
entry['title'] = (
|
||||||
unicode(request.POST['title'])
|
unicode(request.POST['title'])
|
||||||
or unicode(splitext(filename)[0]))
|
or unicode(splitext(filename)[0]))
|
||||||
@ -62,7 +63,6 @@ def submit_start(request):
|
|||||||
entry['description_html'] = cleaned_markdown_conversion(
|
entry['description_html'] = cleaned_markdown_conversion(
|
||||||
entry['description'])
|
entry['description'])
|
||||||
|
|
||||||
entry['media_type'] = u'image' # heh
|
|
||||||
entry['uploader'] = request.user['_id']
|
entry['uploader'] = request.user['_id']
|
||||||
|
|
||||||
# Process the user's folksonomy "tags"
|
# Process the user's folksonomy "tags"
|
||||||
@ -72,6 +72,7 @@ def submit_start(request):
|
|||||||
# Generate a slug from the title
|
# Generate a slug from the title
|
||||||
entry.generate_slug()
|
entry.generate_slug()
|
||||||
|
|
||||||
|
|
||||||
# Now store generate the queueing related filename
|
# Now store generate the queueing related filename
|
||||||
queue_filepath = request.app.queue_store.get_unique_filepath(
|
queue_filepath = request.app.queue_store.get_unique_filepath(
|
||||||
['media_entries',
|
['media_entries',
|
||||||
@ -103,7 +104,7 @@ def submit_start(request):
|
|||||||
# (... don't change entry after this point to avoid race
|
# (... don't change entry after this point to avoid race
|
||||||
# conditions with changes to the document via processing code)
|
# conditions with changes to the document via processing code)
|
||||||
try:
|
try:
|
||||||
process_media.apply_async(
|
media_manager['processor'].apply_async(
|
||||||
[unicode(entry['_id'])], {},
|
[unicode(entry['_id'])], {},
|
||||||
task_id=task_id)
|
task_id=task_id)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{% extends 'mediagoblin/user_pages/media.html' %}
|
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'mediagoblin/user_pages/media.html' %}
|
||||||
|
{% block mediagoblin_media %}
|
||||||
|
<video width="640" height="" controls>
|
||||||
|
<source src="{{ request.app.public_store.file_url(
|
||||||
|
media['media_files']['medium']) }}"
|
||||||
|
type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
</video>
|
||||||
|
{% endblock %}
|
@ -24,24 +24,26 @@
|
|||||||
{% if media %}
|
{% if media %}
|
||||||
<div class="grid_11 alpha">
|
<div class="grid_11 alpha">
|
||||||
<div class="media_image_container">
|
<div class="media_image_container">
|
||||||
{% set display_media = request.app.public_store.file_url(
|
{% block mediagoblin_media %}
|
||||||
media.get_display_media(media.media_files)) %}
|
{% set display_media = request.app.public_store.file_url(
|
||||||
|
media.get_display_media(media.media_files)) %}
|
||||||
|
|
||||||
{# if there's a medium file size, that means the medium size
|
{# if there's a medium file size, that means the medium size
|
||||||
# isn't the original... so link to the original!
|
# isn't the original... so link to the original!
|
||||||
#}
|
#}
|
||||||
{% if media['media_files'].has_key('medium') %}
|
{% if media['media_files'].has_key('medium') %}
|
||||||
<a href="{{ request.app.public_store.file_url(
|
<a href="{{ request.app.public_store.file_url(
|
||||||
media['media_files']['original']) }}">
|
media['media_files']['original']) }}">
|
||||||
|
<img class="media_image"
|
||||||
|
src="{{ display_media }}"
|
||||||
|
alt="Image for {{ media.title }}" />
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
<img class="media_image"
|
<img class="media_image"
|
||||||
src="{{ display_media }}"
|
src="{{ display_media }}"
|
||||||
alt="Image for {{ media.title }}" />
|
alt="Image for {{ media.title }}" />
|
||||||
</a>
|
{% endif %}
|
||||||
{% else %}
|
{% endblock %}
|
||||||
<img class="media_image"
|
|
||||||
src="{{ display_media }}"
|
|
||||||
alt="Image for {{ media.title }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="media_title">
|
<h2 class="media_title">
|
||||||
|
@ -29,6 +29,8 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
|
|||||||
|
|
||||||
from werkzeug.contrib.atom import AtomFeed
|
from werkzeug.contrib.atom import AtomFeed
|
||||||
|
|
||||||
|
from mediagoblin.media_types import get_media_manager
|
||||||
|
|
||||||
|
|
||||||
@uses_pagination
|
@uses_pagination
|
||||||
def user_home(request, page):
|
def user_home(request, page):
|
||||||
@ -113,9 +115,11 @@ def media_home(request, media, page, **kwargs):
|
|||||||
|
|
||||||
comment_form = user_forms.MediaCommentForm(request.POST)
|
comment_form = user_forms.MediaCommentForm(request.POST)
|
||||||
|
|
||||||
|
media_template_name = get_media_manager(media['media_type'])['display_template']
|
||||||
|
|
||||||
return render_to_response(
|
return render_to_response(
|
||||||
request,
|
request,
|
||||||
'mediagoblin/user_pages/media.html',
|
media_template_name,
|
||||||
{'media': media,
|
{'media': media,
|
||||||
'comments': comments,
|
'comments': comments,
|
||||||
'pagination': pagination,
|
'pagination': pagination,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user