aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/config_spec.ini3
-rw-r--r--mediagoblin/db/migrations.py8
-rw-r--r--mediagoblin/gmg_commands/import_export.py10
-rw-r--r--mediagoblin/init/celery/__init__.py2
-rw-r--r--mediagoblin/media_types/__init__.py73
-rw-r--r--mediagoblin/media_types/image/__init__.py26
-rw-r--r--mediagoblin/media_types/image/processing.py (renamed from mediagoblin/process_media/__init__.py)89
-rw-r--r--mediagoblin/media_types/video/__init__.py27
-rw-r--r--mediagoblin/media_types/video/devices/web-advanced.json505
-rw-r--r--mediagoblin/media_types/video/devices/web-flv.pngbin0 -> 2234 bytes
-rw-r--r--mediagoblin/media_types/video/devices/web-webm.svg259
-rw-r--r--mediagoblin/media_types/video/devices/web.svg982
-rw-r--r--mediagoblin/media_types/video/processing.py118
-rw-r--r--mediagoblin/media_types/video/transcoders.py658
-rw-r--r--mediagoblin/process_media/errors.py45
-rw-r--r--mediagoblin/processing.py143
-rw-r--r--mediagoblin/static/images/media_thumbs/video.jpgbin0 -> 7278 bytes
-rw-r--r--mediagoblin/storage/cloudfiles.py10
-rw-r--r--mediagoblin/submit/views.py139
-rw-r--r--mediagoblin/templates/mediagoblin/base.html7
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/image.html1
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/video.html25
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html30
-rw-r--r--mediagoblin/tests/test_celery_setup.py4
-rw-r--r--mediagoblin/tests/test_submission.py6
-rw-r--r--mediagoblin/user_pages/views.py6
-rw-r--r--mediagoblin/views.py8
-rw-r--r--mediagoblin/workbench.py5
-rw-r--r--setup.py1
29 files changed, 2964 insertions, 226 deletions
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index eef6f6e0..e5e059c9 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -50,6 +50,9 @@ allow_attachments = boolean(default=False)
# Cookie stuff
csrf_cookie_name = string(default='mediagoblin_csrftoken')
+# Media types
+enable_video = boolean(default=False)
+
[storage:publicstore]
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
base_dir = string(default="%(here)s/user_dev/media/public")
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index edaf5630..cfc01287 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -100,3 +100,11 @@ def user_add_forgot_password_token_and_expires(database):
"""
add_table_field(database, 'users', 'fp_verification_key', None)
add_table_field(database, 'users', 'fp_token_expire', None)
+
+
+@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/gmg_commands/import_export.py b/mediagoblin/gmg_commands/import_export.py
index 30112969..4ec17d47 100644
--- a/mediagoblin/gmg_commands/import_export.py
+++ b/mediagoblin/gmg_commands/import_export.py
@@ -211,10 +211,12 @@ def _export_media(db, args):
_log.info(u'Exporting {0} - {1}'.format(
entry['title'],
name))
-
- mc_file = media_cache.get_file(path, mode='wb')
- mc_file.write(
- mg_globals.public_store.get_file(path, mode='rb').read())
+ try:
+ mc_file = media_cache.get_file(path, mode='wb')
+ mc_file.write(
+ mg_globals.public_store.get_file(path, mode='rb').read())
+ except e:
+ _log.error('Failed: {0}'.format(e))
_log.info('...Media exported')
diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py
index f7ef9f39..1eb21d7a 100644
--- a/mediagoblin/init/celery/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -18,7 +18,7 @@ import os
import sys
-MANDATORY_CELERY_IMPORTS = ['mediagoblin.process_media']
+MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing']
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..f56fd942
--- /dev/null
+++ b/mediagoblin/media_types/__init__.py
@@ -0,0 +1,73 @@
+# 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
+
+from mediagoblin import mg_globals
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+
+
+class FileTypeNotSupported(Exception):
+ pass
+
+class InvalidFileType(Exception):
+ pass
+
+# This should be more dynamic in the future. Perhaps put it in the .ini?
+# -- Joar
+MEDIA_TYPES = [
+ 'mediagoblin.media_types.image']
+
+if mg_globals.app_config['enable_video']:
+ MEDIA_TYPES.append('mediagoblin.media_types.video')
+
+
+def get_media_types():
+ '''
+ Generator that returns the available media types
+ '''
+ for media_type in MEDIA_TYPES:
+ yield media_type
+
+
+def get_media_managers():
+ '''
+ Generator that returns all available media managers
+ '''
+ for media_type in get_media_types():
+ __import__(media_type)
+
+ 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 "{filename}"').format(
+ filename=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..3b63d8eb
--- /dev/null
+++ b/mediagoblin/media_types/image/__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 <http://www.gnu.org/licenses/>.
+
+from mediagoblin.media_types.image.processing import process_image
+
+
+MEDIA_MANAGER = {
+ "human_readable": "Image",
+ "processor": process_image, # 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"]}
diff --git a/mediagoblin/process_media/__init__.py b/mediagoblin/media_types/image/processing.py
index 54c0c493..2932c455 100644
--- a/mediagoblin/process_media/__init__.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -14,104 +14,23 @@
# 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 os
-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.process_media.errors import BaseProcessingFail, BadMediaFail
-
-
-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])
+from mediagoblin.processing import BaseProcessingFail, \
+ mark_entry_failed, BadMediaFail, create_pub_filepath, THUMB_SIZE, \
+ MEDIUM_SIZE
################################
# 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._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):
"""
diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py
new file mode 100644
index 00000000..a970ab01
--- /dev/null
+++ b/mediagoblin/media_types/video/__init__.py
@@ -0,0 +1,27 @@
+# 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_video
+
+
+MEDIA_MANAGER = {
+ "human_readable": "Video",
+ "processor": process_video, # 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", "mkv", "ogv", "ogg"]}
diff --git a/mediagoblin/media_types/video/devices/web-advanced.json b/mediagoblin/media_types/video/devices/web-advanced.json
new file mode 100644
index 00000000..ce1d22ff
--- /dev/null
+++ b/mediagoblin/media_types/video/devices/web-advanced.json
@@ -0,0 +1,505 @@
+{
+ "make": "Generic",
+ "model": "Web Browser (Advanced)",
+ "description": "Media for World Wide Web",
+ "version": "0.1",
+ "author": {
+ "name": "Dionisio E Alonso",
+ "email": "dealonso@gmail.com"
+ },
+ "icon": "file://web.svg",
+ "default": "WebM 480p",
+ "presets": [
+ {
+ "name": "H.264 720p",
+ "extension": "mp4",
+ "container": "qtmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "qtmux",
+ "width": [
+ 960, 1280
+ ],
+ "height": [
+ 720, 720
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "qtmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+ {
+ "name": "WebM 720p",
+ "extension": "webm",
+ "container": "webmmux",
+ "icon": "file://web-webm.svg",
+ "vcodec": {
+ "name": "vp8enc",
+ "container": "webmmux",
+ "width": [
+ 960, 1280
+ ],
+ "height": [
+ 720, 720
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "quality=5.75 threads=%(threads)s speed=2"
+ ]
+ },
+ "acodec": {
+ "name": "vorbisenc",
+ "container": "webmmux",
+ "width": [
+ 8, 32
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "quality=0.3"
+ ]
+ }
+ },
+ {
+ "name": "Flash Video 720p",
+ "extension": "flv",
+ "icon": "file://web-flv.png",
+ "container": "flvmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "flvmux",
+ "width": [
+ 960, 1280
+ ],
+ "height": [
+ 720, 720
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "flvmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+
+ {
+ "name": "H.264 576p",
+ "extension": "mp4",
+ "container": "qtmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "qtmux",
+ "width": [
+ 768, 1024
+ ],
+ "height": [
+ 576, 576
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "qtmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+ {
+ "name": "WebM 576p",
+ "extension": "webm",
+ "container": "webmmux",
+ "icon": "file://web-webm.svg",
+ "vcodec": {
+ "name": "vp8enc",
+ "container": "webmmux",
+ "width": [
+ 768, 1024
+ ],
+ "height": [
+ 576, 576
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "quality=5.75 threads=%(threads)s speed=2"
+ ]
+ },
+ "acodec": {
+ "name": "vorbisenc",
+ "container": "webmmux",
+ "width": [
+ 8, 32
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "quality=0.3"
+ ]
+ }
+ },
+ {
+ "name": "Flash Video 576p",
+ "extension": "flv",
+ "icon": "file://web-flv.png",
+ "container": "flvmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "flvmux",
+ "width": [
+ 768, 1024
+ ],
+ "height": [
+ 576, 576
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "flvmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+
+ {
+ "name": "H.264 480p",
+ "extension": "mp4",
+ "container": "qtmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "qtmux",
+ "width": [
+ 640, 854
+ ],
+ "height": [
+ 480, 480
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "qtmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+ {
+ "name": "WebM 480p",
+ "extension": "webm",
+ "container": "webmmux",
+ "icon": "file://web-webm.svg",
+ "vcodec": {
+ "name": "vp8enc",
+ "container": "webmmux",
+ "width": [
+ 640, 854
+ ],
+ "height": [
+ 480, 480
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "quality=5.75 threads=%(threads)s speed=2"
+ ]
+ },
+ "acodec": {
+ "name": "vorbisenc",
+ "container": "webmmux",
+ "width": [
+ 8, 32
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "quality=0.3"
+ ]
+ }
+ },
+ {
+ "name": "Flash Video 480p",
+ "extension": "flv",
+ "icon": "file://web-flv.png",
+ "container": "flvmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "flvmux",
+ "width": [
+ 640, 854
+ ],
+ "height": [
+ 480, 480
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "flvmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+
+ {
+ "name": "H.264 360p",
+ "extension": "mp4",
+ "container": "qtmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "qtmux",
+ "width": [
+ 480, 640
+ ],
+ "height": [
+ 360, 360
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "qtmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ },
+ {
+ "name": "WebM 360p",
+ "extension": "webm",
+ "container": "webmmux",
+ "icon": "file://web-webm.svg",
+ "vcodec": {
+ "name": "vp8enc",
+ "container": "webmmux",
+ "width": [
+ 480, 640
+ ],
+ "height": [
+ 360, 360
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "quality=5.75 threads=%(threads)s speed=2"
+ ]
+ },
+ "acodec": {
+ "name": "vorbisenc",
+ "container": "webmmux",
+ "width": [
+ 8, 32
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "quality=0.3"
+ ]
+ }
+ },
+ {
+ "name": "Flash Video 360p",
+ "extension": "flv",
+ "icon": "file://web-flv.png",
+ "container": "flvmux",
+ "vcodec": {
+ "name": "x264enc",
+ "container": "flvmux",
+ "width": [
+ 480, 640
+ ],
+ "height": [
+ 360, 360
+ ],
+ "rate": [
+ 1, 30
+ ],
+ "passes": [
+ "pass=qual quantizer=23 subme=6 cabac=0 threads=0"
+ ]
+ },
+ "acodec": {
+ "name": "faac",
+ "container": "flvmux",
+ "width": [
+ 8, 24
+ ],
+ "depth": [
+ 8, 24
+ ],
+ "rate": [
+ 8000, 96000
+ ],
+ "channels": [
+ 1, 2
+ ],
+ "passes": [
+ "bitrate=131072 profile=LC"
+ ]
+ }
+ }
+ ]
+}
diff --git a/mediagoblin/media_types/video/devices/web-flv.png b/mediagoblin/media_types/video/devices/web-flv.png
new file mode 100644
index 00000000..b75699f4
--- /dev/null
+++ b/mediagoblin/media_types/video/devices/web-flv.png
Binary files differ
diff --git a/mediagoblin/media_types/video/devices/web-webm.svg b/mediagoblin/media_types/video/devices/web-webm.svg
new file mode 100644
index 00000000..4e5b3e97
--- /dev/null
+++ b/mediagoblin/media_types/video/devices/web-webm.svg
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48px"
+ height="48px"
+ id="svg2816"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="web-webm.svg">
+ <defs
+ id="defs2818">
+ <linearGradient
+ id="linearGradient3656">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3658" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop3660" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3632">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0.54901963;"
+ offset="0"
+ id="stop3634" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3636" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3622">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3624" />
+ <stop
+ style="stop-color:#d3d7cf;stop-opacity:1;"
+ offset="1"
+ id="stop3626" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3600">
+ <stop
+ style="stop-color:#8ae234;stop-opacity:1;"
+ offset="0"
+ id="stop3602" />
+ <stop
+ style="stop-color:#4e9a06;stop-opacity:1;"
+ offset="1"
+ id="stop3604" />
+ </linearGradient>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective2824" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3600"
+ id="linearGradient3606"
+ x1="20.256382"
+ y1="2.546674"
+ x2="20.256382"
+ y2="46.881901"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3622"
+ id="linearGradient3628"
+ x1="21.2349"
+ y1="7.948472"
+ x2="21.2349"
+ y2="40.191879"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3632"
+ id="linearGradient3638"
+ x1="6.4826794"
+ y1="4.543263"
+ x2="25.363527"
+ y2="35.227882"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,-0.35355339)" />
+ <inkscape:perspective
+ id="perspective3693"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3600-5"
+ id="linearGradient3606-9"
+ x1="20.256382"
+ y1="2.546674"
+ x2="20.256382"
+ y2="46.881901"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3600-5">
+ <stop
+ style="stop-color:#8ae234;stop-opacity:1;"
+ offset="0"
+ id="stop3602-7" />
+ <stop
+ style="stop-color:#4e9a06;stop-opacity:1;"
+ offset="1"
+ id="stop3604-2" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3731">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.82730657"
+ id="feGaussianBlur3733" />
+ </filter>
+ <inkscape:perspective
+ id="perspective3749"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3782"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="8"
+ inkscape:cx="20.51741"
+ inkscape:cy="22.534228"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1099"
+ inkscape:window-height="834"
+ inkscape:window-x="801"
+ inkscape:window-y="106"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3608" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata2821">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <path
+ sodipodi:type="star"
+ style="fill:#000000;fill-opacity:0.2869955;stroke:none;filter:url(#filter3731)"
+ id="path3598-4"
+ sodipodi:sides="3"
+ sodipodi:cx="13.857143"
+ sodipodi:cy="24.714287"
+ sodipodi:r1="25.596954"
+ sodipodi:r2="12.798477"
+ sodipodi:arg1="0"
+ sodipodi:arg2="1.0471976"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="M 39.454098,24.714287 20.256381,35.798093 1.0586662,46.8819 l 0,-22.167614 0,-22.1676119 19.1977168,11.0838069 19.197715,11.083806 z"
+ transform="matrix(1.0537808,0,0,1.0537808,3.6163385,-1.9600717)" />
+ <path
+ sodipodi:type="star"
+ style="fill:url(#linearGradient3606);fill-opacity:1;stroke:#366a04;stroke-width:1.05497880999999993;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-linejoin:round"
+ id="path3598"
+ sodipodi:sides="3"
+ sodipodi:cx="13.857143"
+ sodipodi:cy="24.714287"
+ sodipodi:r1="25.596954"
+ sodipodi:r2="12.798477"
+ sodipodi:arg1="0"
+ sodipodi:arg2="1.0471976"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="M 39.454098,24.714287 20.256381,35.798093 1.0586662,46.8819 l 0,-22.167614 0,-22.1676119 19.1977168,11.0838069 19.197715,11.083806 z"
+ transform="matrix(0.94788634,0,0,0.94788634,5.0257749,0.56128794)" />
+ <path
+ style="fill:url(#linearGradient3628);stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
+ d="m 6.5304575,9.646791 8.7347075,20.091724 4.674611,-18.160553 4.525987,2.612472 3.885316,12.559503 4.403755,-7.765833 1.744319,1.009296 -2.127799,9.211229 -6.155446,3.554753 -4.028978,-9.439016 -2.255629,13.086534 -5.852703,3.373025 -7.5584205,-9.989634 0.01028,-20.1435 z"
+ id="path3620"
+ sodipodi:nodetypes="cccccccccccccc" />
+ <path
+ style="fill:none;stroke:url(#linearGradient3638);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 6.9826793,42.785087 0,-38.0953773 32.9068657,18.9987873"
+ id="path3630"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.15686275"
+ d="M 6.6184028,8.6135689 15.026019,28.134068 19.45616,10.995613"
+ id="path3739"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.15686275"
+ d="m 25.081121,14.552251 3.345117,11.020499 3.93014,-6.825955"
+ id="path3739-5"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.15686275"
+ d="m 6.6291261,30.85266 7.0710679,9.280777"
+ id="path3772"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.15686275"
+ d="m 34.736621,20.290253 -2.032932,8.794642"
+ id="path3772-6"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.15686275"
+ d="m 20.594485,35.934991 1.811961,-10.650796 3.270369,7.778174"
+ id="path3796"
+ sodipodi:nodetypes="ccc" />
+ </g>
+</svg>
diff --git a/mediagoblin/media_types/video/devices/web.svg b/mediagoblin/media_types/video/devices/web.svg
new file mode 100644
index 00000000..c0c68244
--- /dev/null
+++ b/mediagoblin/media_types/video/devices/web.svg
@@ -0,0 +1,982 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48px"
+ height="48px"
+ id="svg3440"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/apps"
+ sodipodi:docname="internet-web-browser.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <defs
+ id="defs3">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective156" />
+ <linearGradient
+ id="linearGradient4750">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop4752" />
+ <stop
+ style="stop-color:#fefefe;stop-opacity:1.0000000;"
+ offset="0.37931034"
+ id="stop4758" />
+ <stop
+ style="stop-color:#1d1d1d;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop4754" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4350">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop4352" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop4354" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4126">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop4128" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0.16494845;"
+ offset="1.0000000"
+ id="stop4130" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4114">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop4116" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop4118" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3962">
+ <stop
+ style="stop-color:#d3e9ff;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop3964" />
+ <stop
+ style="stop-color:#d3e9ff;stop-opacity:1.0000000;"
+ offset="0.15517241"
+ id="stop4134" />
+ <stop
+ style="stop-color:#4074ae;stop-opacity:1.0000000;"
+ offset="0.75000000"
+ id="stop4346" />
+ <stop
+ style="stop-color:#36486c;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop3966" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3962"
+ id="radialGradient3968"
+ gradientTransform="scale(0.999989,1.000011)"
+ cx="18.247644"
+ cy="15.716079"
+ fx="18.247644"
+ fy="15.716079"
+ r="29.993349"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4114"
+ id="radialGradient4120"
+ gradientTransform="scale(1.643990,0.608276)"
+ cx="15.115514"
+ cy="63.965388"
+ fx="15.115514"
+ fy="63.965388"
+ r="12.289036"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4126"
+ id="radialGradient4132"
+ gradientTransform="scale(0.999989,1.000011)"
+ cx="15.601279"
+ cy="12.142302"
+ fx="15.601279"
+ fy="12.142302"
+ r="43.526714"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4350"
+ id="radialGradient4356"
+ gradientTransform="scale(1.179536,0.847791)"
+ cx="11.826907"
+ cy="10.476453"
+ fx="11.826907"
+ fy="10.476453"
+ r="32.664848"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4750"
+ id="radialGradient4756"
+ gradientTransform="scale(1.036822,0.964486)"
+ cx="18.633780"
+ cy="17.486208"
+ fx="18.934305"
+ fy="17.810213"
+ r="40.692665"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1460"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1462"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1466"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1468"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1470"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1474"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1476"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1478"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1482"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1484"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1486"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1490"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1492"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1494"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1498"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1500"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1502"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1506"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1508"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1510"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1514"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1516"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1518"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1522"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1524"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1526"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1528"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1530"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1532"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1534"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1536"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1538"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1540"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1542"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1544"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1546"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1550"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1552"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1554"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ <radialGradient
+ r="40.692665"
+ fy="17.810213"
+ fx="18.934305"
+ cy="17.486208"
+ cx="18.633780"
+ gradientTransform="scale(1.036822,0.964486)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient1558"
+ xlink:href="#linearGradient4750"
+ inkscape:collect="always" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="0.17254902"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="9.8994949"
+ inkscape:cx="25.799661"
+ inkscape:cy="24.622653"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="1440"
+ inkscape:window-height="823"
+ inkscape:window-x="0"
+ inkscape:window-y="30"
+ inkscape:showpageshadow="false" />
+ <metadata
+ id="metadata4">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Globe</dc:title>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:contributor>
+ <cc:Agent>
+ <dc:title>Tuomas Kuosmanen</dc:title>
+ </cc:Agent>
+ </dc:contributor>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+ <dc:source>http://jimmac.musichall.cz</dc:source>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>globe</rdf:li>
+ <rdf:li>international</rdf:li>
+ <rdf:li>web</rdf:li>
+ <rdf:li>www</rdf:li>
+ <rdf:li>internet</rdf:li>
+ <rdf:li>network</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/publicdomain/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <path
+ sodipodi:type="arc"
+ style="fill:url(#radialGradient4120);fill-opacity:1.0000000;stroke:none;stroke-opacity:1.0000000"
+ id="path4112"
+ sodipodi:cx="24.849752"
+ sodipodi:cy="38.908627"
+ sodipodi:rx="20.203051"
+ sodipodi:ry="7.4751287"
+ d="M 45.052803 38.908627 A 20.203051 7.4751287 0 1 1 4.6467018,38.908627 A 20.203051 7.4751287 0 1 1 45.052803 38.908627 z"
+ transform="matrix(1.000000,0.000000,0.000000,1.243244,0.000000,-10.27241)" />
+ <path
+ style="fill:url(#radialGradient3968);fill-opacity:1.0000000;fill-rule:nonzero;stroke:#39396c;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000"
+ d="M 43.959853,23.485499 C 43.959853,34.195217 35.277750,42.877222 24.569505,42.877222 C 13.860279,42.877222 5.1786663,34.195119 5.1786663,23.485499 C 5.1786663,12.776272 13.860279,4.0951517 24.569505,4.0951517 C 35.277750,4.0951517 43.959853,12.776272 43.959853,23.485499 L 43.959853,23.485499 z "
+ id="path3214" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:0.42159382;fill:url(#radialGradient4356);fill-opacity:1.0000000;stroke:none;stroke-opacity:1.0000000"
+ id="path4348"
+ sodipodi:cx="17.778685"
+ sodipodi:cy="15.271057"
+ sodipodi:rx="12.929953"
+ sodipodi:ry="9.2934036"
+ d="M 30.708637 15.271057 A 12.929953 9.2934036 0 1 1 4.8487320,15.271057 A 12.929953 9.2934036 0 1 1 30.708637 15.271057 z"
+ transform="matrix(0.835938,0.000000,0.000000,1.000000,9.886868,0.000000)" />
+ <g
+ id="g4136"
+ style="fill:#000000;fill-opacity:0.71345031;fill-rule:nonzero;stroke:none;stroke-miterlimit:4.0000000"
+ transform="matrix(0.982371,0.000000,0.000000,0.982371,0.121079,0.232914)">
+ <g
+ id="g4138">
+ <g
+ id="g4142">
+ <path
+ d="M 44.071300,20.714400 C 44.071300,20.977100 44.071300,20.714400 44.071300,20.714400 L 43.526400,21.331600 C 43.192400,20.938000 42.817400,20.607000 42.436600,20.261300 L 41.600700,20.384300 L 40.837000,19.521000 L 40.837000,20.589400 L 41.491300,21.084500 L 41.926800,21.577700 L 42.508800,20.919500 C 42.655300,21.193900 42.799800,21.468300 42.945300,21.742700 L 42.945300,22.565000 L 42.290000,23.305200 L 41.090800,24.128400 L 40.182600,25.034700 L 39.600600,24.374500 L 39.891600,23.634300 L 39.310500,22.976100 L 38.329100,20.878400 L 37.493200,19.933100 L 37.274400,20.179200 L 37.602500,21.372600 L 38.219700,22.071800 C 38.572200,23.089400 38.920900,24.062000 39.383800,25.034700 C 40.101600,25.034700 40.778300,24.958500 41.491200,24.868700 L 41.491200,25.444900 L 40.619100,27.584100 L 39.819300,28.488400 L 39.165000,29.888800 C 39.165000,30.656400 39.165000,31.424000 39.165000,32.191500 L 39.383800,33.097800 L 39.020500,33.508000 L 38.219700,34.002100 L 37.383800,34.701300 L 38.075200,35.482600 L 37.129900,36.306800 L 37.311500,36.840000 L 35.893500,38.445500 L 34.949200,38.445500 L 34.149400,38.939600 L 33.639600,38.939600 L 33.639600,38.281400 L 33.422800,36.963000 C 33.141500,36.136800 32.848600,35.316500 32.550700,34.496200 C 32.550700,33.890700 32.586800,33.291100 32.623000,32.685700 L 32.987300,31.863400 L 32.477500,30.875100 L 32.514600,29.517700 L 31.823200,28.736400 L 32.168900,27.605500 L 31.606400,26.967300 L 30.624000,26.967300 L 30.296900,26.597200 L 29.315500,27.214900 L 28.916100,26.761300 L 28.006900,27.543000 C 27.389700,26.843300 26.771500,26.144100 26.153400,25.444900 L 25.426800,23.716400 L 26.081100,22.730100 L 25.717800,22.319000 L 26.516600,20.425400 C 27.172900,19.609000 27.858400,18.825800 28.551800,18.039700 L 29.788100,17.710600 L 31.169000,17.546500 L 32.114300,17.793600 L 33.459000,19.150000 L 33.931700,18.615800 L 34.585000,18.533800 L 35.821300,18.944900 L 36.766600,18.944900 L 37.420900,18.368700 L 37.711900,17.957600 L 37.056600,17.546500 L 35.965800,17.464500 C 35.663100,17.044600 35.381800,16.603200 35.022400,16.230100 L 34.658100,16.394200 L 34.512600,17.464500 L 33.858300,16.724300 L 33.713800,15.900100 L 32.987200,15.325900 L 32.695200,15.325900 L 33.422700,16.148200 L 33.131700,16.888400 L 32.550600,17.052500 L 32.913900,16.312300 L 32.258600,15.984200 L 31.678500,15.326000 L 30.586700,15.572100 L 30.442200,15.900200 L 29.787900,16.312300 L 29.424600,17.217600 L 28.516400,17.669700 L 28.116000,17.217600 L 27.680500,17.217600 L 27.680500,15.736200 L 28.625800,15.242100 L 29.352400,15.242100 L 29.205900,14.666900 L 28.625800,14.090700 L 29.606300,13.884600 L 30.151200,13.268400 L 30.586700,12.527200 L 31.387500,12.527200 L 31.168700,11.952000 L 31.678500,11.622900 L 31.678500,12.281100 L 32.768300,12.527200 L 33.858100,11.622900 L 33.931300,11.210800 L 34.875600,10.553100 C 34.533800,10.595600 34.192000,10.626800 33.858000,10.717700 L 33.858000,9.9766000 L 34.221300,9.1538000 L 33.858000,9.1538000 L 33.059600,9.8940000 L 32.840800,10.305600 L 33.059600,10.882300 L 32.695300,11.868600 L 32.114200,11.539500 L 31.606400,10.964300 L 30.805600,11.539500 L 30.514600,10.223600 L 31.895500,9.3188000 L 31.895500,8.8247000 L 32.768500,8.2490000 L 34.149400,7.9194000 L 35.094700,8.2490000 L 36.838800,8.5781000 L 36.403300,9.0713000 L 35.458000,9.0713000 L 36.403300,10.058600 L 37.129900,9.2363000 L 37.350600,8.8745000 C 37.350600,8.8745000 40.137700,11.372500 41.730500,14.105000 C 43.323300,16.838400 44.071300,20.060100 44.071300,20.714400 z "
+ id="path4144" />
+ </g>
+ </g>
+ <g
+ id="g4146">
+ <g
+ id="g4150">
+ <path
+ d="M 26.070300,9.2363000 L 25.997100,9.7295000 L 26.506900,10.058600 L 27.378000,9.4829000 L 26.942500,8.9892000 L 26.360500,9.3188000 L 26.070500,9.2363000"
+ id="path4152" />
+ </g>
+ </g>
+ <g
+ id="g4154">
+ <g
+ id="g4158">
+ <path
+ d="M 26.870100,5.8633000 L 24.979500,5.1226000 L 22.799800,5.3692000 L 20.109400,6.1094000 L 19.600600,6.6035000 L 21.272500,7.7549000 L 21.272500,8.4131000 L 20.618200,9.0713000 L 21.491200,10.800300 L 22.071300,10.470200 L 22.799800,9.3188000 C 23.922800,8.9716000 24.929700,8.5781000 25.997100,8.0844000 L 26.870100,5.8632000"
+ id="path4160" />
+ </g>
+ </g>
+ <g
+ id="g4162">
+ <g
+ id="g4166">
+ <path
+ d="M 28.833000,12.774900 L 28.542000,12.033700 L 28.032200,12.198700 L 28.178700,13.103000 L 28.833000,12.774900"
+ id="path4168" />
+ </g>
+ </g>
+ <g
+ id="g4170">
+ <g
+ id="g4174">
+ <path
+ d="M 29.123000,12.608900 L 28.977500,13.597200 L 29.777300,13.432200 L 30.358400,12.857000 L 29.849600,12.362900 C 29.678700,11.907800 29.482400,11.483000 29.268500,11.046500 L 28.833000,11.046500 L 28.833000,11.539700 L 29.123000,11.868800 L 29.123000,12.609000"
+ id="path4176" />
+ </g>
+ </g>
+ <g
+ id="g4178">
+ <g
+ id="g4182">
+ <path
+ d="M 18.365200,28.242200 L 17.783200,27.089900 L 16.692900,26.843300 L 16.111400,25.280800 L 14.657800,25.444900 L 13.422400,24.540600 L 12.113300,25.692000 L 12.113300,25.873600 C 11.717300,25.759300 11.230500,25.743700 10.877900,25.526900 L 10.586900,24.704600 L 10.586900,23.799300 L 9.7148000,23.881300 C 9.7876000,23.305100 9.8598000,22.729900 9.9331000,22.153800 L 9.4238000,22.153800 L 8.9155000,22.812000 L 8.4062000,23.058100 L 7.6791000,22.647900 L 7.6063000,21.742600 L 7.7518000,20.755300 L 8.8426000,19.933000 L 9.7147000,19.933000 L 9.8597000,19.438900 L 10.950000,19.685000 L 11.749800,20.673300 L 11.895300,19.026800 L 13.276600,17.875400 L 13.785400,16.641000 L 14.803000,16.229900 L 15.384500,15.407600 L 16.692600,15.159600 L 17.347400,14.173300 C 16.693100,14.173300 16.038800,14.173300 15.384500,14.173300 L 16.620300,13.597100 L 17.491900,13.597100 L 18.728200,13.185000 L 18.873700,12.692800 L 18.437200,12.280700 L 17.928400,12.115700 L 18.073900,11.622500 L 17.710600,10.882300 L 16.838000,11.210400 L 16.983500,10.552700 L 15.965900,9.9765000 L 15.166600,11.374400 L 15.238900,11.868500 L 14.439600,12.198600 L 13.930300,13.267900 L 13.712500,12.280600 L 12.331200,11.704400 L 12.112900,10.964200 L 13.930300,9.8939000 L 14.730100,9.1537000 L 14.802900,8.2489000 L 14.366900,8.0018000 L 13.785400,7.9193000 L 13.422100,8.8246000 C 13.422100,8.8246000 12.814200,8.9437000 12.657900,8.9823000 C 10.661800,10.821700 6.6286000,14.792400 5.6916000,22.288500 C 5.7287000,22.462300 6.3708000,23.470100 6.3708000,23.470100 L 7.8972000,24.374400 L 9.4236000,24.786500 L 10.078400,25.609700 L 11.095500,26.349900 L 11.677000,26.267900 L 12.113000,26.464200 L 12.113000,26.597000 L 11.531900,28.160000 L 11.095400,28.818200 L 11.240900,29.148300 L 10.877600,30.380700 L 12.186200,32.767400 L 13.494300,33.919700 L 14.076300,34.742000 L 14.003100,36.470500 L 14.439600,37.456800 L 14.003100,39.349400 C 14.003100,39.349400 13.968900,39.337700 14.024600,39.527100 C 14.080800,39.716600 16.353700,40.978300 16.498200,40.870900 C 16.642200,40.761500 16.765300,40.665800 16.765300,40.665800 L 16.620300,40.255600 L 17.201400,39.679400 L 17.419700,39.103200 L 18.365000,38.773100 L 19.091600,36.962600 L 18.873800,36.470400 L 19.381600,35.730200 L 20.472400,35.482200 L 21.054400,34.165800 L 20.908900,32.521300 L 21.781000,31.286900 L 21.926500,30.052500 C 20.733100,29.460700 19.549500,28.851300 18.365000,28.242000"
+ id="path4184" />
+ </g>
+ </g>
+ <g
+ id="g4186">
+ <g
+ id="g4190">
+ <path
+ d="M 16.765600,9.5649000 L 17.492200,10.058600 L 18.074200,10.058600 L 18.074200,9.4829000 L 17.347600,9.1538000 L 16.765600,9.5649000"
+ id="path4192" />
+ </g>
+ </g>
+ <g
+ id="g4194">
+ <g
+ id="g4198">
+ <path
+ d="M 14.876000,8.9072000 L 14.512200,9.8120000 L 15.239300,9.8120000 L 15.603100,8.9892000 C 15.916600,8.7675000 16.228600,8.5444000 16.547900,8.3310000 L 17.275000,8.5781000 C 17.759400,8.9072000 18.243800,9.2363000 18.728600,9.5649000 L 19.456100,8.9072000 L 18.655800,8.5781000 L 18.292000,7.8374000 L 16.911100,7.6728000 L 16.838300,7.2612000 L 16.184000,7.4262000 L 15.893600,8.0020000 L 15.529800,7.2613000 L 15.384800,7.5904000 L 15.457600,8.4132000 L 14.876000,8.9072000"
+ id="path4200" />
+ </g>
+ </g>
+ <g
+ id="g4202">
+ <g
+ style="opacity:0.75000000"
+ id="g4204">
+ <path
+ id="path4206"
+ d="" />
+ </g>
+ <g
+ id="g4208">
+ <path
+ id="path4210"
+ d="" />
+ </g>
+ </g>
+ <g
+ id="g4212">
+ <g
+ style="opacity:0.75000000"
+ id="g4214">
+ <path
+ id="path4216"
+ d="" />
+ </g>
+ <g
+ id="g4218">
+ <path
+ id="path4220"
+ d="" />
+ </g>
+ </g>
+ <g
+ id="g4222">
+ <g
+ id="g4226">
+ <path
+ d="M 17.492200,6.8496000 L 17.856000,6.5210000 L 18.583100,6.3564000 C 19.081100,6.1142000 19.581100,5.9511000 20.109500,5.7802000 L 19.819500,5.2865000 L 18.881000,5.4213000 L 18.437600,5.8632000 L 17.706600,5.9692000 L 17.056700,6.2744000 L 16.740800,6.4272000 L 16.547900,6.6855000 L 17.492200,6.8496000"
+ id="path4228" />
+ </g>
+ </g>
+ <g
+ id="g4230">
+ <g
+ id="g4234">
+ <path
+ d="M 18.728500,14.666500 L 19.165000,14.008300 L 18.510200,13.515100 L 18.728500,14.666500"
+ id="path4236" />
+ </g>
+ </g>
+ </g>
+ <g
+ id="g3216"
+ style="color:#000000;fill:url(#radialGradient1460);fill-opacity:1.0000000;fill-rule:nonzero;stroke:none;stroke-width:1.0179454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ transform="matrix(0.982371,0.000000,0.000000,0.982371,-8.095179e-2,3.088300e-2)">
+ <g
+ id="g3218"
+ style="color:#000000;fill:url(#radialGradient1462);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3222"
+ style="color:#000000;fill:url(#radialGradient1466);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 44.071300,20.714400 C 44.071300,20.977100 44.071300,20.714400 44.071300,20.714400 L 43.526400,21.331600 C 43.192400,20.938000 42.817400,20.607000 42.436600,20.261300 L 41.600700,20.384300 L 40.837000,19.521000 L 40.837000,20.589400 L 41.491300,21.084500 L 41.926800,21.577700 L 42.508800,20.919500 C 42.655300,21.193900 42.799800,21.468300 42.945300,21.742700 L 42.945300,22.565000 L 42.290000,23.305200 L 41.090800,24.128400 L 40.182600,25.034700 L 39.600600,24.374500 L 39.891600,23.634300 L 39.310500,22.976100 L 38.329100,20.878400 L 37.493200,19.933100 L 37.274400,20.179200 L 37.602500,21.372600 L 38.219700,22.071800 C 38.572200,23.089400 38.920900,24.062000 39.383800,25.034700 C 40.101600,25.034700 40.778300,24.958500 41.491200,24.868700 L 41.491200,25.444900 L 40.619100,27.584100 L 39.819300,28.488400 L 39.165000,29.888800 C 39.165000,30.656400 39.165000,31.424000 39.165000,32.191500 L 39.383800,33.097800 L 39.020500,33.508000 L 38.219700,34.002100 L 37.383800,34.701300 L 38.075200,35.482600 L 37.129900,36.306800 L 37.311500,36.840000 L 35.893500,38.445500 L 34.949200,38.445500 L 34.149400,38.939600 L 33.639600,38.939600 L 33.639600,38.281400 L 33.422800,36.963000 C 33.141500,36.136800 32.848600,35.316500 32.550700,34.496200 C 32.550700,33.890700 32.586800,33.291100 32.623000,32.685700 L 32.987300,31.863400 L 32.477500,30.875100 L 32.514600,29.517700 L 31.823200,28.736400 L 32.168900,27.605500 L 31.606400,26.967300 L 30.624000,26.967300 L 30.296900,26.597200 L 29.315500,27.214900 L 28.916100,26.761300 L 28.006900,27.543000 C 27.389700,26.843300 26.771500,26.144100 26.153400,25.444900 L 25.426800,23.716400 L 26.081100,22.730100 L 25.717800,22.319000 L 26.516600,20.425400 C 27.172900,19.609000 27.858400,18.825800 28.551800,18.039700 L 29.788100,17.710600 L 31.169000,17.546500 L 32.114300,17.793600 L 33.459000,19.150000 L 33.931700,18.615800 L 34.585000,18.533800 L 35.821300,18.944900 L 36.766600,18.944900 L 37.420900,18.368700 L 37.711900,17.957600 L 37.056600,17.546500 L 35.965800,17.464500 C 35.663100,17.044600 35.381800,16.603200 35.022400,16.230100 L 34.658100,16.394200 L 34.512600,17.464500 L 33.858300,16.724300 L 33.713800,15.900100 L 32.987200,15.325900 L 32.695200,15.325900 L 33.422700,16.148200 L 33.131700,16.888400 L 32.550600,17.052500 L 32.913900,16.312300 L 32.258600,15.984200 L 31.678500,15.326000 L 30.586700,15.572100 L 30.442200,15.900200 L 29.787900,16.312300 L 29.424600,17.217600 L 28.516400,17.669700 L 28.116000,17.217600 L 27.680500,17.217600 L 27.680500,15.736200 L 28.625800,15.242100 L 29.352400,15.242100 L 29.205900,14.666900 L 28.625800,14.090700 L 29.606300,13.884600 L 30.151200,13.268400 L 30.586700,12.527200 L 31.387500,12.527200 L 31.168700,11.952000 L 31.678500,11.622900 L 31.678500,12.281100 L 32.768300,12.527200 L 33.858100,11.622900 L 33.931300,11.210800 L 34.875600,10.553100 C 34.533800,10.595600 34.192000,10.626800 33.858000,10.717700 L 33.858000,9.9766000 L 34.221300,9.1538000 L 33.858000,9.1538000 L 33.059600,9.8940000 L 32.840800,10.305600 L 33.059600,10.882300 L 32.695300,11.868600 L 32.114200,11.539500 L 31.606400,10.964300 L 30.805600,11.539500 L 30.514600,10.223600 L 31.895500,9.3188000 L 31.895500,8.8247000 L 32.768500,8.2490000 L 34.149400,7.9194000 L 35.094700,8.2490000 L 36.838800,8.5781000 L 36.403300,9.0713000 L 35.458000,9.0713000 L 36.403300,10.058600 L 37.129900,9.2363000 L 37.350600,8.8745000 C 37.350600,8.8745000 40.137700,11.372500 41.730500,14.105000 C 43.323300,16.838400 44.071300,20.060100 44.071300,20.714400 z "
+ id="path3224"
+ style="color:#000000;fill:url(#radialGradient1468);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3226"
+ style="color:#000000;fill:url(#radialGradient1470);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3230"
+ style="color:#000000;fill:url(#radialGradient1474);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 26.070300,9.2363000 L 25.997100,9.7295000 L 26.506900,10.058600 L 27.378000,9.4829000 L 26.942500,8.9892000 L 26.360500,9.3188000 L 26.070500,9.2363000"
+ id="path3232"
+ style="color:#000000;fill:url(#radialGradient1476);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3234"
+ style="color:#000000;fill:url(#radialGradient1478);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3238"
+ style="color:#000000;fill:url(#radialGradient1482);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 26.870100,5.8633000 L 24.979500,5.1226000 L 22.799800,5.3692000 L 20.109400,6.1094000 L 19.600600,6.6035000 L 21.272500,7.7549000 L 21.272500,8.4131000 L 20.618200,9.0713000 L 21.491200,10.800300 L 22.071300,10.470200 L 22.799800,9.3188000 C 23.922800,8.9716000 24.929700,8.5781000 25.997100,8.0844000 L 26.870100,5.8632000"
+ id="path3240"
+ style="color:#000000;fill:url(#radialGradient1484);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3242"
+ style="color:#000000;fill:url(#radialGradient1486);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3246"
+ style="color:#000000;fill:url(#radialGradient1490);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 28.833000,12.774900 L 28.542000,12.033700 L 28.032200,12.198700 L 28.178700,13.103000 L 28.833000,12.774900"
+ id="path3248"
+ style="color:#000000;fill:url(#radialGradient1492);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3250"
+ style="color:#000000;fill:url(#radialGradient1494);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3254"
+ style="color:#000000;fill:url(#radialGradient1498);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 29.123000,12.608900 L 28.977500,13.597200 L 29.777300,13.432200 L 30.358400,12.857000 L 29.849600,12.362900 C 29.678700,11.907800 29.482400,11.483000 29.268500,11.046500 L 28.833000,11.046500 L 28.833000,11.539700 L 29.123000,11.868800 L 29.123000,12.609000"
+ id="path3256"
+ style="color:#000000;fill:url(#radialGradient1500);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3258"
+ style="color:#000000;fill:url(#radialGradient1502);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3262"
+ style="color:#000000;fill:url(#radialGradient1506);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 18.365200,28.242200 L 17.783200,27.089900 L 16.692900,26.843300 L 16.111400,25.280800 L 14.657800,25.444900 L 13.422400,24.540600 L 12.113300,25.692000 L 12.113300,25.873600 C 11.717300,25.759300 11.230500,25.743700 10.877900,25.526900 L 10.586900,24.704600 L 10.586900,23.799300 L 9.7148000,23.881300 C 9.7876000,23.305100 9.8598000,22.729900 9.9331000,22.153800 L 9.4238000,22.153800 L 8.9155000,22.812000 L 8.4062000,23.058100 L 7.6791000,22.647900 L 7.6063000,21.742600 L 7.7518000,20.755300 L 8.8426000,19.933000 L 9.7147000,19.933000 L 9.8597000,19.438900 L 10.950000,19.685000 L 11.749800,20.673300 L 11.895300,19.026800 L 13.276600,17.875400 L 13.785400,16.641000 L 14.803000,16.229900 L 15.384500,15.407600 L 16.692600,15.159600 L 17.347400,14.173300 C 16.693100,14.173300 16.038800,14.173300 15.384500,14.173300 L 16.620300,13.597100 L 17.491900,13.597100 L 18.728200,13.185000 L 18.873700,12.692800 L 18.437200,12.280700 L 17.928400,12.115700 L 18.073900,11.622500 L 17.710600,10.882300 L 16.838000,11.210400 L 16.983500,10.552700 L 15.965900,9.9765000 L 15.166600,11.374400 L 15.238900,11.868500 L 14.439600,12.198600 L 13.930300,13.267900 L 13.712500,12.280600 L 12.331200,11.704400 L 12.112900,10.964200 L 13.930300,9.8939000 L 14.730100,9.1537000 L 14.802900,8.2489000 L 14.366900,8.0018000 L 13.785400,7.9193000 L 13.422100,8.8246000 C 13.422100,8.8246000 12.814200,8.9437000 12.657900,8.9823000 C 10.661800,10.821700 6.6286000,14.792400 5.6916000,22.288500 C 5.7287000,22.462300 6.3708000,23.470100 6.3708000,23.470100 L 7.8972000,24.374400 L 9.4236000,24.786500 L 10.078400,25.609700 L 11.095500,26.349900 L 11.677000,26.267900 L 12.113000,26.464200 L 12.113000,26.597000 L 11.531900,28.160000 L 11.095400,28.818200 L 11.240900,29.148300 L 10.877600,30.380700 L 12.186200,32.767400 L 13.494300,33.919700 L 14.076300,34.742000 L 14.003100,36.470500 L 14.439600,37.456800 L 14.003100,39.349400 C 14.003100,39.349400 13.968900,39.337700 14.024600,39.527100 C 14.080800,39.716600 16.353700,40.978300 16.498200,40.870900 C 16.642200,40.761500 16.765300,40.665800 16.765300,40.665800 L 16.620300,40.255600 L 17.201400,39.679400 L 17.419700,39.103200 L 18.365000,38.773100 L 19.091600,36.962600 L 18.873800,36.470400 L 19.381600,35.730200 L 20.472400,35.482200 L 21.054400,34.165800 L 20.908900,32.521300 L 21.781000,31.286900 L 21.926500,30.052500 C 20.733100,29.460700 19.549500,28.851300 18.365000,28.242000"
+ id="path3264"
+ style="color:#000000;fill:url(#radialGradient1508);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3266"
+ style="color:#000000;fill:url(#radialGradient1510);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3270"
+ style="color:#000000;fill:url(#radialGradient1514);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 16.765600,9.5649000 L 17.492200,10.058600 L 18.074200,10.058600 L 18.074200,9.4829000 L 17.347600,9.1538000 L 16.765600,9.5649000"
+ id="path3272"
+ style="color:#000000;fill:url(#radialGradient1516);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3274"
+ style="color:#000000;fill:url(#radialGradient1518);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3278"
+ style="color:#000000;fill:url(#radialGradient1522);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 14.876000,8.9072000 L 14.512200,9.8120000 L 15.239300,9.8120000 L 15.603100,8.9892000 C 15.916600,8.7675000 16.228600,8.5444000 16.547900,8.3310000 L 17.275000,8.5781000 C 17.759400,8.9072000 18.243800,9.2363000 18.728600,9.5649000 L 19.456100,8.9072000 L 18.655800,8.5781000 L 18.292000,7.8374000 L 16.911100,7.6728000 L 16.838300,7.2612000 L 16.184000,7.4262000 L 15.893600,8.0020000 L 15.529800,7.2613000 L 15.384800,7.5904000 L 15.457600,8.4132000 L 14.876000,8.9072000"
+ id="path3280"
+ style="color:#000000;fill:url(#radialGradient1524);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3282"
+ style="color:#000000;fill:url(#radialGradient1526);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ style="opacity:0.75000000;color:#000000;fill:url(#radialGradient1528);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ id="g3284">
+ <path
+ d=""
+ style="color:#000000;fill:url(#radialGradient1530);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ id="path3286" />
+ </g>
+ <g
+ id="g3288"
+ style="color:#000000;fill:url(#radialGradient1532);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d=""
+ id="path3290"
+ style="color:#000000;fill:url(#radialGradient1534);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3292"
+ style="color:#000000;fill:url(#radialGradient1536);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ style="opacity:0.75000000;color:#000000;fill:url(#radialGradient1538);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ id="g3294">
+ <path
+ d=""
+ style="color:#000000;fill:url(#radialGradient1540);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible"
+ id="path3296" />
+ </g>
+ <g
+ id="g3298"
+ style="color:#000000;fill:url(#radialGradient1542);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d=""
+ id="path3300"
+ style="color:#000000;fill:url(#radialGradient1544);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3302"
+ style="color:#000000;fill:url(#radialGradient1546);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3306"
+ style="color:#000000;fill:url(#radialGradient1550);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 17.492200,6.8496000 L 17.856000,6.5210000 L 18.583100,6.3564000 C 19.081100,6.1142000 19.581100,5.9511000 20.109500,5.7802000 L 19.819500,5.2865000 L 18.881000,5.4213000 L 18.437600,5.8632000 L 17.706600,5.9692000 L 17.056700,6.2744000 L 16.740800,6.4272000 L 16.547900,6.6855000 L 17.492200,6.8496000"
+ id="path3308"
+ style="color:#000000;fill:url(#radialGradient1552);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ <g
+ id="g3310"
+ style="color:#000000;fill:url(#radialGradient1554);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <g
+ id="g3314"
+ style="color:#000000;fill:url(#radialGradient1558);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible">
+ <path
+ d="M 18.728500,14.666500 L 19.165000,14.008300 L 18.510200,13.515100 L 18.728500,14.666500"
+ id="path3316"
+ style="color:#000000;fill:url(#radialGradient4756);stroke-dashoffset:0.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ </g>
+ </g>
+ <path
+ style="fill:none;fill-opacity:1.0000000;fill-rule:nonzero;stroke:url(#radialGradient4132);stroke-miterlimit:4.0000000;stroke-opacity:1.0000000"
+ d="M 42.975093,23.485534 C 42.975093,33.651354 34.733915,41.892440 24.569493,41.892440 C 14.404139,41.892440 6.1634261,33.651261 6.1634261,23.485534 C 6.1634261,13.320180 14.404139,5.0799340 24.569493,5.0799340 C 34.733915,5.0799340 42.975093,13.320180 42.975093,23.485534 L 42.975093,23.485534 z "
+ id="path4122" />
+ </g>
+</svg>
diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py
new file mode 100644
index 00000000..6125e49c
--- /dev/null
+++ b/mediagoblin/media_types/video/processing.py
@@ -0,0 +1,118 @@
+# 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 tempfile
+import logging
+import os
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import mark_entry_failed, \
+ THUMB_SIZE, MEDIUM_SIZE, create_pub_filepath
+from . import transcoders
+
+logging.basicConfig()
+
+_log = logging.getLogger(__name__)
+_log.setLevel(logging.DEBUG)
+
+
+def process_video(entry):
+ """
+ Code to process a video
+
+ Much of this code is derived from the arista-transcoder script in
+ the arista PyPI package and changed to match the needs of
+ MediaGoblin
+
+ This function sets up the arista video encoder in some kind of new thread
+ and attaches callbacks to that child process, hopefully, the
+ entry-complete callback will be called when the video is done.
+ """
+ workbench = mgg.workbench_manager.create_workbench()
+
+ queued_filepath = entry['queued_media_file']
+ queued_filename = workbench.localized_file(
+ mgg.queue_store, queued_filepath,
+ 'source')
+
+ medium_filepath = create_pub_filepath(
+ entry,
+ '{original}-640p.webm'.format(
+ original=os.path.splitext(
+ queued_filepath[-1])[0] # Select the
+ ))
+
+ thumbnail_filepath = create_pub_filepath(
+ entry, 'thumbnail.jpg')
+
+
+ # Create a temporary file for the video destination
+ tmp_dst = tempfile.NamedTemporaryFile()
+
+ with tmp_dst:
+ # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
+ transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name)
+
+ # Push transcoded video to public storage
+ _log.debug('Saving medium...')
+ mgg.public_store.get_file(medium_filepath, 'wb').write(
+ tmp_dst.read())
+ _log.debug('Saved medium')
+
+ entry['media_files']['webm_640'] = medium_filepath
+
+ # Save the width and height of the transcoded video
+ entry['media_data']['video'] = {
+ u'width': transcoder.dst_data.videowidth,
+ u'height': transcoder.dst_data.videoheight}
+
+ # Create a temporary file for the video thumbnail
+ tmp_thumb = tempfile.NamedTemporaryFile()
+
+ with tmp_thumb:
+ # Create a thumbnail.jpg that fits in a 180x180 square
+ transcoders.VideoThumbnailer(queued_filename, tmp_thumb.name)
+
+ # Push the thumbnail to public storage
+ _log.debug('Saving thumbnail...')
+ mgg.public_store.get_file(thumbnail_filepath, 'wb').write(
+ tmp_thumb.read())
+ _log.debug('Saved thumbnail')
+
+ entry['media_files']['thumb'] = thumbnail_filepath
+
+
+ # Push original file to public storage
+ 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:
+ _log.debug('Saving original...')
+ original_file.write(queued_file.read())
+ _log.debug('Saved original')
+
+ entry['media_files']['original'] = original_filepath
+
+ mgg.queue_store.delete_file(queued_filepath)
+
+
+ # Save the MediaEntry
+ entry.save()
diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py
new file mode 100644
index 00000000..d7ed14ca
--- /dev/null
+++ b/mediagoblin/media_types/video/transcoders.py
@@ -0,0 +1,658 @@
+# 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 __future__ import division
+
+import os
+os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
+
+import sys
+import logging
+import pdb
+import urllib
+
+_log = logging.getLogger(__name__)
+logging.basicConfig()
+_log.setLevel(logging.DEBUG)
+
+CPU_COUNT = 2
+try:
+ import multiprocessing
+ try:
+ CPU_COUNT = multiprocessing.cpu_count()
+ except NotImplementedError:
+ _log.warning('multiprocessing.cpu_count not implemented')
+ pass
+except ImportError:
+ _log.warning('Could not import multiprocessing, defaulting to 2 CPU cores')
+ pass
+
+try:
+ import gtk
+except:
+ raise Exception('Could not find pygtk')
+
+try:
+ import gobject
+ gobject.threads_init()
+except:
+ raise Exception('gobject could not be found')
+
+try:
+ import pygst
+ pygst.require('0.10')
+ import gst
+ from gst.extend import discoverer
+except:
+ raise Exception('gst/pygst 0.10 could not be found')
+
+
+class VideoThumbnailer:
+ # Declaration of thumbnailer states
+ STATE_NULL = 0
+ STATE_HALTING = 1
+ STATE_PROCESSING = 2
+
+ # The current thumbnailer state
+ state = STATE_NULL
+
+ # This will contain the thumbnailing pipeline
+ thumbnail_pipeline = None
+
+ buffer_probes = {}
+
+ errors = []
+
+ def __init__(self, source_path, dest_path):
+ '''
+ Set up playbin pipeline in order to get video properties.
+
+ Initializes and runs the gobject.MainLoop()
+ '''
+ self.source_path = source_path
+ self.dest_path = dest_path
+
+ self.loop = gobject.MainLoop()
+
+ # Set up the playbin. It will be used to discover certain
+ # properties of the input file
+ self.playbin = gst.element_factory_make('playbin')
+
+ self.videosink = gst.element_factory_make('fakesink', 'videosink')
+ self.playbin.set_property('video-sink', self.videosink)
+
+ self.audiosink = gst.element_factory_make('fakesink', 'audiosink')
+ self.playbin.set_property('audio-sink', self.audiosink)
+
+ self.bus = self.playbin.get_bus()
+ self.bus.add_signal_watch()
+ self.watch_id = self.bus.connect('message', self._on_bus_message)
+
+ self.playbin.set_property('uri', 'file:{0}'.format(
+ urllib.pathname2url(self.source_path)))
+
+ self.playbin.set_state(gst.STATE_PAUSED)
+
+ self.run()
+
+ def run(self):
+ self.loop.run()
+
+ def _on_bus_message(self, bus, message):
+ _log.debug(' BUS MESSAGE: {0}'.format(message))
+
+ if message.type == gst.MESSAGE_ERROR:
+ gobject.idle_add(self._on_bus_error)
+
+ elif message.type == gst.MESSAGE_STATE_CHANGED:
+ # The pipeline state has changed
+ # Parse state changing data
+ _prev, state, _pending = message.parse_state_changed()
+
+ _log.debug('State changed: {0}'.format(state))
+
+ if state == gst.STATE_PAUSED:
+ if message.src == self.playbin:
+ gobject.idle_add(self._on_bus_paused)
+
+ def _on_bus_paused(self):
+ '''
+ Set up thumbnailing pipeline
+ '''
+ current_video = self.playbin.get_property('current-video')
+
+ if current_video == 0:
+ _log.debug('Found current video from playbin')
+ else:
+ _log.error('Could not get any current video from playbin!')
+
+ self.duration = self._get_duration(self.playbin)
+ _log.info('Video length: {0}'.format(self.duration / gst.SECOND))
+
+ _log.info('Setting up thumbnailing pipeline')
+ self.thumbnail_pipeline = gst.parse_launch(
+ 'filesrc location="{0}" ! decodebin ! '
+ 'ffmpegcolorspace ! videoscale ! '
+ 'video/x-raw-rgb,depth=24,bpp=24,pixel-aspect-ratio=1/1,width=180 ! '
+ 'fakesink signal-handoffs=True'.format(self.source_path))
+
+ self.thumbnail_bus = self.thumbnail_pipeline.get_bus()
+ self.thumbnail_bus.add_signal_watch()
+ self.thumbnail_watch_id = self.thumbnail_bus.connect(
+ 'message', self._on_thumbnail_bus_message)
+
+ self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
+
+ #gobject.timeout_add(3000, self._on_timeout)
+
+ return False
+
+ def _on_thumbnail_bus_message(self, bus, message):
+ _log.debug('Thumbnail bus called, message: {0}'.format(message))
+
+ if message.type == gst.MESSAGE_ERROR:
+ _log.error(message)
+ gobject.idle_add(self._on_bus_error)
+
+ if message.type == gst.MESSAGE_STATE_CHANGED:
+ _prev, state, _pending = message.parse_state_changed()
+
+ if (state == gst.STATE_PAUSED and
+ not self.state == self.STATE_PROCESSING and
+ message.src == self.thumbnail_pipeline):
+ _log.info('Pipeline paused, processing')
+ self.state = self.STATE_PROCESSING
+
+ for sink in self.thumbnail_pipeline.sinks():
+ name = sink.get_name()
+ factoryname = sink.get_factory().get_name()
+
+ if factoryname == 'fakesink':
+ sinkpad = sink.get_pad('sink')
+
+ self.buffer_probes[name] = sinkpad.add_buffer_probe(
+ self.buffer_probe_handler, name)
+
+ _log.info('Added buffer probe')
+
+ break
+
+ # Apply the wadsworth constant, fallback to 1 second
+ seek_amount = max(self.duration / 100 * 30, 1 * gst.SECOND)
+
+ _log.debug('seek amount: {0}'.format(seek_amount))
+
+ seek_result = self.thumbnail_pipeline.seek(
+ 1.0,
+ gst.FORMAT_TIME,
+ gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
+ gst.SEEK_TYPE_SET,
+ seek_amount,
+ gst.SEEK_TYPE_NONE,
+ 0)
+
+ if not seek_result:
+ self.errors.append('COULD_NOT_SEEK')
+ _log.error('Couldn\'t seek! result: {0}'.format(
+ seek_result))
+ _log.info(message)
+ self.shutdown()
+ else:
+ pass
+ #self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
+ #pdb.set_trace()
+
+ def buffer_probe_handler_real(self, pad, buff, name):
+ '''
+ Capture buffers as gdk_pixbufs when told to.
+ '''
+ try:
+ caps = buff.caps
+ if caps is None:
+ _log.error('No caps passed to buffer probe handler!')
+ self.shutdown()
+ return False
+
+ _log.debug('caps: {0}'.format(caps))
+
+ filters = caps[0]
+ width = filters["width"]
+ height = filters["height"]
+
+ pixbuf = gtk.gdk.pixbuf_new_from_data(
+ buff.data, gtk.gdk.COLORSPACE_RGB, False, 8,
+ width, height, width * 3)
+
+ # NOTE: 200x136 is sort of arbitrary. it's larger than what
+ # the ui uses at the time of this writing.
+ # new_width, new_height = scaled_size((width, height), (200, 136))
+
+ #pixbuf = pixbuf.scale_simple(
+ #new_width, new_height, gtk.gdk.INTERP_BILINEAR)
+
+ pixbuf.save(self.dest_path, 'jpeg')
+ _log.info('Saved thumbnail')
+ del pixbuf
+ self.shutdown()
+ except gst.QueryError:
+ pass
+ return False
+
+ def buffer_probe_handler(self, pad, buff, name):
+ '''
+ Proxy function for buffer_probe_handler_real
+ '''
+ gobject.idle_add(
+ lambda: self.buffer_probe_handler_real(pad, buff, name))
+
+ return True
+
+ def _get_duration(self, pipeline, retries=0):
+ '''
+ Get the duration of a pipeline.
+
+ Retries 5 times.
+ '''
+ if retries == 5:
+ return 0
+
+ try:
+ return pipeline.query_duration(gst.FORMAT_TIME)[0]
+ except gst.QueryError:
+ return self._get_duration(pipeline, retries + 1)
+
+ def _on_timeout(self):
+ _log.error('TIMEOUT! DROP EVERYTHING!')
+ self.shutdown()
+
+ def _on_bus_error(self, *args):
+ _log.error('AHAHAHA! Error! args: {0}'.format(args))
+
+ def shutdown(self):
+ '''
+ Tell gobject to call __halt when the mainloop is idle.
+ '''
+ _log.info('Shutting down')
+ self.__halt()
+
+ def __halt(self):
+ '''
+ Halt all pipelines and shut down the main loop
+ '''
+ _log.info('Halting...')
+ self.state = self.STATE_HALTING
+
+ self.__disconnect()
+
+ gobject.idle_add(self.__halt_final)
+
+ def __disconnect(self):
+ _log.debug('Disconnecting...')
+ if not self.playbin is None:
+ self.playbin.set_state(gst.STATE_NULL)
+ for sink in self.playbin.sinks():
+ name = sink.get_name()
+ factoryname = sink.get_factory().get_name()
+
+ _log.debug('Disconnecting {0}'.format(name))
+
+ if factoryname == "fakesink":
+ pad = sink.get_pad("sink")
+ pad.remove_buffer_probe(self.buffer_probes[name])
+ del self.buffer_probes[name]
+
+ self.playbin = None
+
+ if self.bus is not None:
+ self.bus.disconnect(self.watch_id)
+ self.bus = None
+
+
+ def __halt_final(self):
+ _log.info('Done')
+ if self.errors:
+ _log.error(','.join(self.errors))
+
+ self.loop.quit()
+
+
+class VideoTranscoder:
+ '''
+ Video transcoder
+
+ Transcodes the SRC video file to a VP8 WebM video file at DST
+
+ - Does the same thing as VideoThumbnailer, but produces a WebM vp8
+ and vorbis video file.
+ - The VideoTranscoder exceeds the VideoThumbnailer in the way
+ that it was refined afterwards and therefore is done more
+ correctly.
+ '''
+ def __init__(self, src, dst, **kwargs):
+ _log.info('Initializing VideoTranscoder...')
+
+ self.loop = gobject.MainLoop()
+ self.source_path = src
+ self.destination_path = dst
+
+ # Options
+ self.destination_dimensions = kwargs.get('dimensions') or (640, 640)
+ self._progress_callback = kwargs.get('progress_callback') or None
+
+ if not type(self.destination_dimensions) == tuple:
+ raise Exception('dimensions must be tuple: (width, height)')
+
+ self._setup()
+ self._run()
+
+ def _setup(self):
+ self._setup_discover()
+ self._setup_pipeline()
+
+ def _run(self):
+ _log.info('Discovering...')
+ self.discoverer.discover()
+ _log.info('Done')
+
+ _log.debug('Initializing MainLoop()')
+ self.loop.run()
+
+ def _setup_discover(self):
+ _log.debug('Setting up discoverer')
+ self.discoverer = discoverer.Discoverer(self.source_path)
+
+ # Connect self.__discovered to the 'discovered' event
+ self.discoverer.connect('discovered', self.__discovered)
+
+ def __discovered(self, data, is_media):
+ '''
+ Callback for media discoverer.
+ '''
+ if not is_media:
+ self.__stop()
+ raise Exception('Could not discover {0}'.format(self.source_path))
+
+ _log.debug('__discovered, data: {0}'.format(data.__dict__))
+
+ self.data = data
+
+ # Launch things that should be done after discovery
+ self._link_elements()
+ self.__setup_videoscale_capsfilter()
+
+ # Tell the transcoding pipeline to start running
+ self.pipeline.set_state(gst.STATE_PLAYING)
+ _log.info('Transcoding...')
+
+ def _setup_pipeline(self):
+ _log.debug('Setting up transcoding pipeline')
+ # Create the pipeline bin.
+ self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
+
+ # Create all GStreamer elements, starting with
+ # filesrc & decoder
+ self.filesrc = gst.element_factory_make('filesrc', 'filesrc')
+ self.filesrc.set_property('location', self.source_path)
+ self.pipeline.add(self.filesrc)
+
+ self.decoder = gst.element_factory_make('decodebin2', 'decoder')
+ self.decoder.connect('new-decoded-pad', self._on_dynamic_pad)
+ self.pipeline.add(self.decoder)
+
+ # Video elements
+ self.videoqueue = gst.element_factory_make('queue', 'videoqueue')
+ self.pipeline.add(self.videoqueue)
+
+ self.videorate = gst.element_factory_make('videorate', 'videorate')
+ self.pipeline.add(self.videorate)
+
+ self.ffmpegcolorspace = gst.element_factory_make(
+ 'ffmpegcolorspace', 'ffmpegcolorspace')
+ self.pipeline.add(self.ffmpegcolorspace)
+
+ self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale')
+ #self.videoscale.set_property('method', 2) # I'm not sure this works
+ #self.videoscale.set_property('add-borders', 0)
+ self.pipeline.add(self.videoscale)
+
+ self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
+ self.pipeline.add(self.capsfilter)
+
+ self.vp8enc = gst.element_factory_make('vp8enc', 'vp8enc')
+ self.vp8enc.set_property('quality', 6)
+ self.vp8enc.set_property('threads', 2)
+ self.pipeline.add(self.vp8enc)
+
+ # Audio elements
+ self.audioqueue = gst.element_factory_make('queue', 'audioqueue')
+ self.pipeline.add(self.audioqueue)
+
+ self.audiorate = gst.element_factory_make('audiorate', 'audiorate')
+ self.audiorate.set_property('tolerance', 80000000)
+ self.pipeline.add(self.audiorate)
+
+ self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
+ self.pipeline.add(self.audioconvert)
+
+ self.audiocapsfilter = gst.element_factory_make('capsfilter', 'audiocapsfilter')
+ audiocaps = ['audio/x-raw-float']
+ self.audiocapsfilter.set_property(
+ 'caps',
+ gst.caps_from_string(
+ ','.join(audiocaps)))
+ self.pipeline.add(self.audiocapsfilter)
+
+ self.vorbisenc = gst.element_factory_make('vorbisenc', 'vorbisenc')
+ self.vorbisenc.set_property('quality', 1)
+ self.pipeline.add(self.vorbisenc)
+
+ # WebMmux & filesink
+ self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
+ self.pipeline.add(self.webmmux)
+
+ self.filesink = gst.element_factory_make('filesink', 'filesink')
+ self.filesink.set_property('location', self.destination_path)
+ self.pipeline.add(self.filesink)
+
+ # Progressreport
+ self.progressreport = gst.element_factory_make(
+ 'progressreport', 'progressreport')
+ # Update every second
+ self.progressreport.set_property('update-freq', 1)
+ self.progressreport.set_property('silent', True)
+ self.pipeline.add(self.progressreport)
+
+ def _link_elements(self):
+ '''
+ Link all the elements
+
+ This code depends on data from the discoverer and is called
+ from __discovered
+ '''
+ _log.debug('linking elements')
+ # Link the filesrc element to the decoder. The decoder then emits
+ # 'new-decoded-pad' which links decoded src pads to either a video
+ # or audio sink
+ self.filesrc.link(self.decoder)
+
+ # Link all the video elements in a row to webmmux
+ gst.element_link_many(
+ self.videoqueue,
+ self.videorate,
+ self.ffmpegcolorspace,
+ self.videoscale,
+ self.capsfilter,
+ self.vp8enc,
+ self.webmmux)
+
+ if self.data.is_audio:
+ # Link all the audio elements in a row to webmux
+ gst.element_link_many(
+ self.audioqueue,
+ self.audiorate,
+ self.audioconvert,
+ self.audiocapsfilter,
+ self.vorbisenc,
+ self.webmmux)
+
+ gst.element_link_many(
+ self.webmmux,
+ self.progressreport,
+ self.filesink)
+
+ # Setup the message bus and connect _on_message to the pipeline
+ self._setup_bus()
+
+
+ def _on_dynamic_pad(self, dbin, pad, islast):
+ '''
+ Callback called when ``decodebin2`` has a pad that we can connect to
+ '''
+ # Intersect the capabilities of the video sink and the pad src
+ # Then check if they have no common capabilities.
+ if self.ffmpegcolorspace.get_pad_template('sink')\
+ .get_caps().intersect(pad.get_caps()).is_empty():
+ # It is NOT a video src pad.
+ pad.link(self.audioqueue.get_pad('sink'))
+ else:
+ # It IS a video src pad.
+ pad.link(self.videoqueue.get_pad('sink'))
+
+ def _setup_bus(self):
+ self.bus = self.pipeline.get_bus()
+ self.bus.add_signal_watch()
+ self.bus.connect('message', self._on_message)
+
+ def __setup_videoscale_capsfilter(self):
+ '''
+ Sets up the output format (width, height) for the video
+ '''
+ caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1', 'framerate=30/1']
+
+ if self.data.videoheight > self.data.videowidth:
+ # Whoa! We have ourselves a portrait video!
+ caps.append('height={0}'.format(
+ self.destination_dimensions[1]))
+ else:
+ # It's a landscape, phew, how normal.
+ caps.append('width={0}'.format(
+ self.destination_dimensions[0]))
+
+ self.capsfilter.set_property(
+ 'caps',
+ gst.caps_from_string(
+ ','.join(caps)))
+
+ def _on_message(self, bus, message):
+ _log.debug((bus, message, message.type))
+
+ t = message.type
+
+ if t == gst.MESSAGE_EOS:
+ self._discover_dst_and_stop()
+ _log.info('Done')
+
+ elif t == gst.MESSAGE_ELEMENT:
+ if message.structure.get_name() == 'progress':
+ data = dict(message.structure)
+
+ if self._progress_callback:
+ self._progress_callback(data)
+
+ _log.info('{percent}% done...'.format(
+ percent=data.get('percent')))
+ _log.debug(data)
+
+ elif t == gst.MESSAGE_ERROR:
+ _log.error((bus, message))
+ self.__stop()
+
+ def _discover_dst_and_stop(self):
+ self.dst_discoverer = discoverer.Discoverer(self.destination_path)
+
+ self.dst_discoverer.connect('discovered', self.__dst_discovered)
+
+ self.dst_discoverer.discover()
+
+
+ def __dst_discovered(self, data, is_media):
+ self.dst_data = data
+
+ self.__stop()
+
+ def __stop(self):
+ _log.debug(self.loop)
+
+ # Stop executing the pipeline
+ self.pipeline.set_state(gst.STATE_NULL)
+
+ # This kills the loop, mercifully
+ gobject.idle_add(self.__stop_mainloop)
+
+ def __stop_mainloop(self):
+ '''
+ Wrapper for gobject.MainLoop.quit()
+
+ This wrapper makes us able to see if self.loop.quit has been called
+ '''
+ _log.info('Terminating MainLoop')
+
+ self.loop.quit()
+
+
+if __name__ == '__main__':
+ os.nice(19)
+ from optparse import OptionParser
+
+ parser = OptionParser(
+ usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
+
+ parser.add_option('-a', '--action',
+ dest='action',
+ help='One of "video" or "thumbnail"')
+
+ parser.add_option('-v',
+ dest='verbose',
+ action='store_true',
+ help='Output debug information')
+
+ parser.add_option('-q',
+ dest='quiet',
+ action='store_true',
+ help='Dear program, please be quiet unless *error*')
+
+ (options, args) = parser.parse_args()
+
+ if options.verbose:
+ _log.setLevel(logging.DEBUG)
+ else:
+ _log.setLevel(logging.INFO)
+
+ if options.quiet:
+ _log.setLevel(logging.ERROR)
+
+ _log.debug(args)
+
+ if not len(args) == 2:
+ parser.print_help()
+ sys.exit()
+
+ if options.action == 'thumbnail':
+ VideoThumbnailer(*args)
+ elif options.action == 'video':
+ def cb(data):
+ print('I\'m a callback!')
+ transcoder = VideoTranscoder(*args, progress_callback=cb)
diff --git a/mediagoblin/process_media/errors.py b/mediagoblin/process_media/errors.py
deleted file mode 100644
index 4224a3e1..00000000
--- a/mediagoblin/process_media/errors.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.tools.translate import lazy_pass_to_ugettext as _
-
-
-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.')
diff --git a/mediagoblin/processing.py b/mediagoblin/processing.py
new file mode 100644
index 00000000..89c4ac89
--- /dev/null
+++ b/mediagoblin/processing.py
@@ -0,0 +1,143 @@
+# 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 celery.task import Task
+
+from mediagoblin.db.util import ObjectId
+from mediagoblin import mg_globals as mgg
+
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+
+from mediagoblin.media_types import get_media_manager
+
+
+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])
+
+
+################################
+# Media processing initial steps
+################################
+
+class ProcessMedia(Task):
+ """
+ DEPRECATED -- This now resides in the individual media plugins
+
+ 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:
+ #__import__(entry['media_type'])
+ manager = get_media_manager(entry['media_type'])
+ manager['processor'](entry)
+ except BaseProcessingFail, exc:
+ mark_entry_failed(entry._id, exc)
+ return
+ except ImportError, exc:
+ mark_entry_failed(entry[u'_id'], exc)
+
+ 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)
+
+
+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': {}}})
+
+
+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.')
diff --git a/mediagoblin/static/images/media_thumbs/video.jpg b/mediagoblin/static/images/media_thumbs/video.jpg
new file mode 100644
index 00000000..841dc796
--- /dev/null
+++ b/mediagoblin/static/images/media_thumbs/video.jpg
Binary files differ
diff --git a/mediagoblin/storage/cloudfiles.py b/mediagoblin/storage/cloudfiles.py
index 0d3cc3df..51b73579 100644
--- a/mediagoblin/storage/cloudfiles.py
+++ b/mediagoblin/storage/cloudfiles.py
@@ -98,8 +98,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 139b1d1d..3def44ce 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -19,6 +19,8 @@ import uuid
from os.path import splitext
from cgi import FieldStorage
+from celery import registry
+
from werkzeug.utils import secure_filename
from mediagoblin.db.util import ObjectId
@@ -27,8 +29,9 @@ from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.response import render_to_response, redirect
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.processing import mark_entry_failed, ProcessMedia
from mediagoblin.messages import add_message, SUCCESS
+from mediagoblin.media_types import get_media_type_and_manager, InvalidFileType
@require_active_login
@@ -44,86 +47,90 @@ 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
+ try:
+ 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['title'] = (
- unicode(request.POST['title'])
- or unicode(splitext(filename)[0]))
+ # 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]))
- entry['description'] = unicode(request.POST.get('description'))
- entry['description_html'] = cleaned_markdown_conversion(
- entry['description'])
+ entry['description'] = unicode(request.POST.get('description'))
+ entry['description_html'] = cleaned_markdown_conversion(
+ entry['description'])
- entry['media_type'] = u'image' # heh
- entry['uploader'] = request.user._id
+ entry['uploader'] = request.user['_id']
- # Process the user's folksonomy "tags"
- entry['tags'] = convert_to_tag_list_of_dicts(
- request.POST.get('tags'))
+ # Process the user's folksonomy "tags"
+ entry['tags'] = convert_to_tag_list_of_dicts(
+ request.POST.get('tags'))
- # Generate a slug from the title
- entry.generate_slug()
+ # 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',
- unicode(entry._id),
- secure_filename(filename)])
- # queue appropriately
- queue_file = request.app.queue_store.get_file(
- queue_filepath, 'wb')
+ # Now store generate the queueing related filename
+ queue_filepath = request.app.queue_store.get_unique_filepath(
+ ['media_entries',
+ unicode(entry._id),
+ secure_filename(filename)])
- with queue_file:
- queue_file.write(request.POST['file'].file.read())
+ # queue appropriately
+ queue_file = request.app.queue_store.get_file(
+ queue_filepath, 'wb')
- # Add queued filename to the entry
- entry['queued_media_file'] = queue_filepath
+ with queue_file:
+ queue_file.write(request.POST['file'].file.read())
- # We generate this ourselves so we know what the taks id is for
- # retrieval later.
+ # Add queued filename to the entry
+ entry['queued_media_file'] = queue_filepath
- # (If we got it off the task's auto-generation, there'd be
- # a risk of a race condition when we'd save after sending
- # off the task)
- task_id = unicode(uuid.uuid4())
- entry['queued_task_id'] = task_id
+ # We generate this ourselves so we know what the taks id is for
+ # retrieval later.
- # Save now so we have this data before kicking off processing
- entry.save(validate=True)
+ # (If we got it off the task's auto-generation, there'd be
+ # a risk of a race condition when we'd save after sending
+ # off the task)
+ task_id = unicode(uuid.uuid4())
+ entry['queued_task_id'] = task_id
- # Pass off to processing
- #
- # (... don't change entry after this point to avoid race
- # conditions with changes to the document via processing code)
- try:
- process_media.apply_async(
- [unicode(entry._id)], {},
- task_id=task_id)
- except BaseException as exc:
- # The purpose of this section is because when running in "lazy"
- # or always-eager-with-exceptions-propagated celery mode that
- # the failure handling won't happen on Celery end. Since we
- # expect a lot of users to run things in this way we have to
- # capture stuff here.
- #
- # ... not completely the diaper pattern because the
- # exception is re-raised :)
- mark_entry_failed(entry._id, exc)
- # re-raise the exception
- raise
-
- add_message(request, SUCCESS, _('Woohoo! Submitted!'))
+ # Save now so we have this data before kicking off processing
+ entry.save(validate=True)
- return redirect(request, "mediagoblin.user_pages.user_home",
- user=request.user['username'])
+ # Pass off to processing
+ #
+ # (... don't change entry after this point to avoid race
+ # conditions with changes to the document via processing code)
+ process_media = registry.tasks[ProcessMedia.name]
+ try:
+ process_media.apply_async(
+ [unicode(entry._id)], {},
+ task_id=task_id)
+ except BaseException as exc:
+ # The purpose of this section is because when running in "lazy"
+ # or always-eager-with-exceptions-propagated celery mode that
+ # the failure handling won't happen on Celery end. Since we
+ # expect a lot of users to run things in this way we have to
+ # capture stuff here.
+ #
+ # ... not completely the diaper pattern because the
+ # exception is re-raised :)
+ mark_entry_failed(entry._id, exc)
+ # re-raise the exception
+ raise
+
+ add_message(request, SUCCESS, _('Woohoo! Submitted!'))
+
+ return redirect(request, "mediagoblin.user_pages.user_home",
+ user=request.user['username'])
+ except InvalidFileType, exc:
+ submit_form.file.errors.append(
+ _(u'Invalid file type.'))
return render_to_response(
request,
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index ede5f5c6..3dd9c8ff 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -28,8 +28,15 @@
href="{{ request.staticdirect('/css/extlib/960_16_col.css') }}"/>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/base.css') }}"/>
+ <link rel="stylesheet" type="text/css"
+ href="{{ request.staticdirect('/css/video-js.css') }}"/>
<link rel="shortcut icon"
href="{{ request.staticdirect('/images/goblin.ico') }}" />
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/lib/video.js') }}"></script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/video.js') }}"></script>
+ <script type="text/javascript" src="http://html5.kaltura.org/js" > </script>
{% block mediagoblin_head %}
{% endblock mediagoblin_head %}
</head>
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..5b8ec789
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/media_displays/video.html
@@ -0,0 +1,25 @@
+{% extends 'mediagoblin/user_pages/media.html' %}
+{% block mediagoblin_media %}
+ <div class="video-player" style="position: relative;">
+ <video class="video-js vjs-default-skin"
+ width="{{ media.media_data.video.width }}"
+ height="{{ media.media_data.video.height }}"
+ controls="controls"
+ preload="auto"
+ data-setup="">
+ <source src="{{ request.app.public_store.file_url(
+ media['media_files']['webm_640']) }}"
+ type="video/webm; codecs=&quot;vp8, vorbis&quot;" />
+ </video>
+ </div>
+ {% if 'original' in media.media_files %}
+ <p>
+ <a href="{{ request.app.public_store.file_url(
+ media['media_files']['original']) }}">
+ {%- trans -%}
+ Original
+ {%- endtrans -%}
+ </a>
+ </p>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index adbb66db..7434664c 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -26,24 +26,26 @@
{% if media %}
<div class="grid_11 alpha">
<div class="media_image_container">
- {% set display_media = request.app.public_store.file_url(
- media.get_display_media(media.media_files)) %}
+ {% 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') %}
- <a href="{{ request.app.public_store.file_url(
- media['media_files']['original']) }}">
+ {# 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') %}
+ <a href="{{ request.app.public_store.file_url(
+ media['media_files']['original']) }}">
+ <img class="media_image"
+ src="{{ display_media }}"
+ alt="Image for {{ media.title }}" />
+ </a>
+ {% else %}
<img class="media_image"
src="{{ display_media }}"
alt="Image for {{ media.title }}" />
- </a>
- {% else %}
- <img class="media_image"
- src="{{ display_media }}"
- alt="Image for {{ media.title }}" />
- {% endif %}
+ {% endif %}
+ {% endblock %}
</div>
<h2 class="media_title">
diff --git a/mediagoblin/tests/test_celery_setup.py b/mediagoblin/tests/test_celery_setup.py
index 348a4357..19a9b899 100644
--- a/mediagoblin/tests/test_celery_setup.py
+++ b/mediagoblin/tests/test_celery_setup.py
@@ -50,7 +50,7 @@ def test_setup_celery_from_config():
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
assert fake_celery_module.CELERY_RESULT_PERSISTENT is True
assert fake_celery_module.CELERY_IMPORTS == [
- 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.process_media']
+ 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing']
assert fake_celery_module.CELERY_MONGODB_BACKEND_SETTINGS == {
'database': 'mediagoblin'}
assert fake_celery_module.CELERY_RESULT_BACKEND == 'mongodb'
@@ -74,7 +74,7 @@ def test_setup_celery_from_config():
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
assert fake_celery_module.CELERY_RESULT_PERSISTENT is False
assert fake_celery_module.CELERY_IMPORTS == [
- 'baz.bar.foo', 'import.is.a.this', 'mediagoblin.process_media']
+ 'baz.bar.foo', 'import.is.a.this', 'mediagoblin.processing']
assert fake_celery_module.CELERY_MONGODB_BACKEND_SETTINGS == {
'database': 'captain_lollerskates',
'host': 'mongodb.example.org',
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index dec7118b..eea5747f 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -222,7 +222,7 @@ class TestSubmission:
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
form = context['submit_form']
- assert form.file.errors == ['The file doesn\'t seem to be an image!']
+ assert form.file.errors == [u'Invalid file type.']
# NOTE: The following 2 tests will ultimately fail, but they
# *will* pass the initial form submission step. Instead,
@@ -246,7 +246,7 @@ class TestSubmission:
assert_equal(entry['state'], 'failed')
assert_equal(
entry['fail_error'],
- u'mediagoblin.process_media.errors:BadMediaFail')
+ u'mediagoblin.processing:BadMediaFail')
# Test non-supported file with .png extension
# -------------------------------------------
@@ -266,4 +266,4 @@ class TestSubmission:
assert_equal(entry['state'], 'failed')
assert_equal(
entry['fail_error'],
- u'mediagoblin.process_media.errors:BadMediaFail')
+ u'mediagoblin.processing:BadMediaFail')
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 61cae775..3d9735f7 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -30,6 +30,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):
@@ -120,9 +122,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,
diff --git a/mediagoblin/views.py b/mediagoblin/views.py
index ecf5b723..cd6aba9b 100644
--- a/mediagoblin/views.py
+++ b/mediagoblin/views.py
@@ -14,11 +14,15 @@
# 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 sys
+
from mediagoblin import mg_globals
from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools.response import render_to_response
from mediagoblin.db.util import DESCENDING
from mediagoblin.decorators import uses_pagination
+from mediagoblin import media_types
+
@uses_pagination
@@ -28,12 +32,12 @@ def root_view(request, page):
pagination = Pagination(page, cursor)
media_entries = pagination()
-
return render_to_response(
request, 'mediagoblin/root.html',
{'media_entries': media_entries,
'allow_registration': mg_globals.app_config["allow_registration"],
- 'pagination': pagination})
+ 'pagination': pagination,
+ 'sys': sys})
def simple_template_render(request):
diff --git a/mediagoblin/workbench.py b/mediagoblin/workbench.py
index 60a79f47..9578494c 100644
--- a/mediagoblin/workbench.py
+++ b/mediagoblin/workbench.py
@@ -47,7 +47,10 @@ class Workbench(object):
return str(self.dir)
def __repr__(self):
- return repr(self.dir)
+ try:
+ return str(self)
+ except AttributeError:
+ return 'None'
def joinpath(self, *args):
return os.path.join(self.dir, *args)
diff --git a/setup.py b/setup.py
index 7333f974..c3c2f86f 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,7 @@ setup(
## their package managers.
# 'lxml',
],
+ requires=['gst'],
test_suite='nose.collector',
entry_points="""\
[console_scripts]