From: Joar Wandborg Date: Fri, 23 Sep 2011 00:35:57 +0000 (+0200) Subject: Multimedia support - Commiting from a not yet finished state - Details below X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=93bdab9daad3ae431afd41a2efaefae05a555d88;p=mediagoblin.git 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 --- diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 755f49c5..01df7208 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -107,3 +107,11 @@ def user_add_forgot_password_token_and_expires(database): {'fp_token_expire': {'$exists': False}}, {'$set': {'fp_token_expire': None}}, 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) diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py index c58b1305..05c54b05 100644 --- a/mediagoblin/init/celery/__init__.py +++ b/mediagoblin/init/celery/__init__.py @@ -17,8 +17,13 @@ import os import sys +from mediagoblin.media_types import get_media_types + 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' diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py new file mode 100644 index 00000000..67dab418 --- /dev/null +++ b/mediagoblin/media_types/__init__.py @@ -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 . + +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 diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py new file mode 100644 index 00000000..0cd0383f --- /dev/null +++ b/mediagoblin/media_types/image/__init__.py @@ -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 . + +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"]} diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py new file mode 100644 index 00000000..2c4ad2b1 --- /dev/null +++ b/mediagoblin/media_types/image/processing.py @@ -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 . + +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() diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py new file mode 100644 index 00000000..2a36623e --- /dev/null +++ b/mediagoblin/media_types/video/__init__.py @@ -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 . + +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"]} diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py new file mode 100644 index 00000000..94784836 --- /dev/null +++ b/mediagoblin/media_types/video/processing.py @@ -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 . + +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 diff --git a/mediagoblin/storage/cloudfiles.py b/mediagoblin/storage/cloudfiles.py index b1dd9450..85d52242 100644 --- a/mediagoblin/storage/cloudfiles.py +++ b/mediagoblin/storage/cloudfiles.py @@ -97,8 +97,14 @@ class CloudFilesStorage(StorageInterface): def delete_file(self, filepath): # TODO: Also delete unused directories if empty (safely, with # checks to avoid race conditions). - self.container.delete_object( - self._resolve_filepath(filepath)) + try: + self.container.delete_object( + self._resolve_filepath(filepath)) + except cloudfiles.container.ResponseError: + pass + finally: + pass + def file_url(self, filepath): return '/'.join([ diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index e24d78f3..78f52160 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -28,8 +28,9 @@ from mediagoblin.util import ( from mediagoblin.util import pass_to_ugettext as _ from mediagoblin.decorators import require_active_login 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.media_types import get_media_type_and_manager @require_active_login @@ -45,15 +46,15 @@ def submit_start(request): and request.POST['file'].file): submit_form.file.errors.append( _(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: filename = request.POST['file'].filename + media_type, media_manager = get_media_type_and_manager(filename) + # create entry and save in database entry = request.db.MediaEntry() entry['_id'] = ObjectId() + entry['media_type'] = unicode(media_type) entry['title'] = ( unicode(request.POST['title']) or unicode(splitext(filename)[0])) @@ -62,7 +63,6 @@ def submit_start(request): entry['description_html'] = cleaned_markdown_conversion( entry['description']) - entry['media_type'] = u'image' # heh entry['uploader'] = request.user['_id'] # Process the user's folksonomy "tags" @@ -72,6 +72,7 @@ def submit_start(request): # Generate a slug from the title entry.generate_slug() + # Now store generate the queueing related filename queue_filepath = request.app.queue_store.get_unique_filepath( ['media_entries', @@ -103,7 +104,7 @@ def submit_start(request): # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) try: - process_media.apply_async( + media_manager['processor'].apply_async( [unicode(entry['_id'])], {}, task_id=task_id) except BaseException as exc: diff --git a/mediagoblin/templates/mediagoblin/media_displays/image.html b/mediagoblin/templates/mediagoblin/media_displays/image.html new file mode 100644 index 00000000..ad60fa94 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/media_displays/image.html @@ -0,0 +1 @@ +{% extends 'mediagoblin/user_pages/media.html' %} diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html new file mode 100644 index 00000000..37586924 --- /dev/null +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -0,0 +1,8 @@ +{% extends 'mediagoblin/user_pages/media.html' %} +{% block mediagoblin_media %} + +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 442bef6d..82a48e7c 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -24,24 +24,26 @@ {% if media %}
- {% 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 - # isn't the original... so link to the original! - #} - {% if media['media_files'].has_key('medium') %} - + {% block mediagoblin_media %} + {% 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 + # isn't the original... so link to the original! + #} + {% if media['media_files'].has_key('medium') %} + + Image for {{ media.title }} + + {% else %} Image for {{ media.title }} - - {% else %} - Image for {{ media.title }} - {% endif %} + {% endif %} + {% endblock %}

diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 6a82d718..5458c694 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -29,6 +29,8 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry, from werkzeug.contrib.atom import AtomFeed +from mediagoblin.media_types import get_media_manager + @uses_pagination def user_home(request, page): @@ -113,9 +115,11 @@ def media_home(request, media, page, **kwargs): comment_form = user_forms.MediaCommentForm(request.POST) + media_template_name = get_media_manager(media['media_type'])['display_template'] + return render_to_response( request, - 'mediagoblin/user_pages/media.html', + media_template_name, {'media': media, 'comments': comments, 'pagination': pagination,