aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extlib/freesound/audioprocessing.py616
-rw-r--r--mediagoblin/config_spec.ini19
-rw-r--r--mediagoblin/media_types/__init__.py60
-rw-r--r--mediagoblin/media_types/ascii/__init__.py4
-rw-r--r--mediagoblin/media_types/ascii/asciitoimage.py1
-rw-r--r--mediagoblin/media_types/ascii/processing.py22
-rw-r--r--mediagoblin/media_types/audio/__init__.py25
l---------mediagoblin/media_types/audio/audioprocessing.py1
-rw-r--r--mediagoblin/media_types/audio/processing.py131
-rw-r--r--mediagoblin/media_types/audio/transcoders.py237
-rw-r--r--mediagoblin/media_types/image/__init__.py4
-rw-r--r--mediagoblin/media_types/image/processing.py55
-rw-r--r--mediagoblin/media_types/video/__init__.py12
-rw-r--r--mediagoblin/media_types/video/processing.py33
-rw-r--r--mediagoblin/media_types/video/transcoders.py82
-rw-r--r--mediagoblin/processing/__init__.py3
-rw-r--r--mediagoblin/static/css/audio.css53
-rw-r--r--mediagoblin/static/js/audio.js146
-rw-r--r--mediagoblin/static/js/geolocation-map.js5
-rw-r--r--mediagoblin/submit/views.py19
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/audio.html61
-rw-r--r--mediagoblin/tests/test_submission.py37
22 files changed, 1536 insertions, 90 deletions
diff --git a/extlib/freesound/audioprocessing.py b/extlib/freesound/audioprocessing.py
new file mode 100644
index 00000000..2c2b35b5
--- /dev/null
+++ b/extlib/freesound/audioprocessing.py
@@ -0,0 +1,616 @@
+#!/usr/bin/env python
+# processing.py -- various audio processing functions
+# Copyright (C) 2008 MUSIC TECHNOLOGY GROUP (MTG)
+# UNIVERSITAT POMPEU FABRA
+#
+# 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/>.
+#
+# Authors:
+# Bram de Jong <bram.dejong at domain.com where domain in gmail>
+# 2012, Joar Wandborg <first name at last name dot se>
+
+from PIL import Image, ImageDraw, ImageColor #@UnresolvedImport
+from functools import partial
+import math
+import numpy
+import os
+import re
+import signal
+
+
+def get_sound_type(input_filename):
+ sound_type = os.path.splitext(input_filename.lower())[1].strip(".")
+
+ if sound_type == "fla":
+ sound_type = "flac"
+ elif sound_type == "aif":
+ sound_type = "aiff"
+
+ return sound_type
+
+
+try:
+ import scikits.audiolab as audiolab
+except ImportError:
+ print "WARNING: audiolab is not installed so wav2png will not work"
+import subprocess
+
+class AudioProcessingException(Exception):
+ pass
+
+class TestAudioFile(object):
+ """A class that mimics audiolab.sndfile but generates noise instead of reading
+ a wave file. Additionally it can be told to have a "broken" header and thus crashing
+ in the middle of the file. Also useful for testing ultra-short files of 20 samples."""
+ def __init__(self, num_frames, has_broken_header=False):
+ self.seekpoint = 0
+ self.nframes = num_frames
+ self.samplerate = 44100
+ self.channels = 1
+ self.has_broken_header = has_broken_header
+
+ def seek(self, seekpoint):
+ self.seekpoint = seekpoint
+
+ def read_frames(self, frames_to_read):
+ if self.has_broken_header and self.seekpoint + frames_to_read > self.num_frames / 2:
+ raise RuntimeError()
+
+ num_frames_left = self.num_frames - self.seekpoint
+ will_read = num_frames_left if num_frames_left < frames_to_read else frames_to_read
+ self.seekpoint += will_read
+ return numpy.random.random(will_read)*2 - 1
+
+
+def get_max_level(filename):
+ max_value = 0
+ buffer_size = 4096
+ audio_file = audiolab.Sndfile(filename, 'r')
+ n_samples_left = audio_file.nframes
+
+ while n_samples_left:
+ to_read = min(buffer_size, n_samples_left)
+
+ try:
+ samples = audio_file.read_frames(to_read)
+ except RuntimeError:
+ # this can happen with a broken header
+ break
+
+ # convert to mono by selecting left channel only
+ if audio_file.channels > 1:
+ samples = samples[:,0]
+
+ max_value = max(max_value, numpy.abs(samples).max())
+
+ n_samples_left -= to_read
+
+ audio_file.close()
+
+ return max_value
+
+class AudioProcessor(object):
+ """
+ The audio processor processes chunks of audio an calculates the spectrac centroid and the peak
+ samples in that chunk of audio.
+ """
+ def __init__(self, input_filename, fft_size, window_function=numpy.hanning):
+ max_level = get_max_level(input_filename)
+
+ self.audio_file = audiolab.Sndfile(input_filename, 'r')
+ self.fft_size = fft_size
+ self.window = window_function(self.fft_size)
+ self.spectrum_range = None
+ self.lower = 100
+ self.higher = 22050
+ self.lower_log = math.log10(self.lower)
+ self.higher_log = math.log10(self.higher)
+ self.clip = lambda val, low, high: min(high, max(low, val))
+
+ # figure out what the maximum value is for an FFT doing the FFT of a DC signal
+ fft = numpy.fft.rfft(numpy.ones(fft_size) * self.window)
+ max_fft = (numpy.abs(fft)).max()
+ # set the scale to normalized audio and normalized FFT
+ self.scale = 1.0/max_level/max_fft if max_level > 0 else 1
+
+ def read(self, start, size, resize_if_less=False):
+ """ read size samples starting at start, if resize_if_less is True and less than size
+ samples are read, resize the array to size and fill with zeros """
+
+ # number of zeros to add to start and end of the buffer
+ add_to_start = 0
+ add_to_end = 0
+
+ if start < 0:
+ # the first FFT window starts centered around zero
+ if size + start <= 0:
+ return numpy.zeros(size) if resize_if_less else numpy.array([])
+ else:
+ self.audio_file.seek(0)
+
+ add_to_start = -start # remember: start is negative!
+ to_read = size + start
+
+ if to_read > self.audio_file.nframes:
+ add_to_end = to_read - self.audio_file.nframes
+ to_read = self.audio_file.nframes
+ else:
+ self.audio_file.seek(start)
+
+ to_read = size
+ if start + to_read >= self.audio_file.nframes:
+ to_read = self.audio_file.nframes - start
+ add_to_end = size - to_read
+
+ try:
+ samples = self.audio_file.read_frames(to_read)
+ except RuntimeError:
+ # this can happen for wave files with broken headers...
+ return numpy.zeros(size) if resize_if_less else numpy.zeros(2)
+
+ # convert to mono by selecting left channel only
+ if self.audio_file.channels > 1:
+ samples = samples[:,0]
+
+ if resize_if_less and (add_to_start > 0 or add_to_end > 0):
+ if add_to_start > 0:
+ samples = numpy.concatenate((numpy.zeros(add_to_start), samples), axis=1)
+
+ if add_to_end > 0:
+ samples = numpy.resize(samples, size)
+ samples[size - add_to_end:] = 0
+
+ return samples
+
+
+ def spectral_centroid(self, seek_point, spec_range=110.0):
+ """ starting at seek_point read fft_size samples, and calculate the spectral centroid """
+
+ samples = self.read(seek_point - self.fft_size/2, self.fft_size, True)
+
+ samples *= self.window
+ fft = numpy.fft.rfft(samples)
+ spectrum = self.scale * numpy.abs(fft) # normalized abs(FFT) between 0 and 1
+ length = numpy.float64(spectrum.shape[0])
+
+ # scale the db spectrum from [- spec_range db ... 0 db] > [0..1]
+ db_spectrum = ((20*(numpy.log10(spectrum + 1e-60))).clip(-spec_range, 0.0) + spec_range)/spec_range
+
+ energy = spectrum.sum()
+ spectral_centroid = 0
+
+ if energy > 1e-60:
+ # calculate the spectral centroid
+
+ if self.spectrum_range == None:
+ self.spectrum_range = numpy.arange(length)
+
+ spectral_centroid = (spectrum * self.spectrum_range).sum() / (energy * (length - 1)) * self.audio_file.samplerate * 0.5
+
+ # clip > log10 > scale between 0 and 1
+ spectral_centroid = (math.log10(self.clip(spectral_centroid, self.lower, self.higher)) - self.lower_log) / (self.higher_log - self.lower_log)
+
+ return (spectral_centroid, db_spectrum)
+
+
+ def peaks(self, start_seek, end_seek):
+ """ read all samples between start_seek and end_seek, then find the minimum and maximum peak
+ in that range. Returns that pair in the order they were found. So if min was found first,
+ it returns (min, max) else the other way around. """
+
+ # larger blocksizes are faster but take more mem...
+ # Aha, Watson, a clue, a tradeof!
+ block_size = 4096
+
+ max_index = -1
+ max_value = -1
+ min_index = -1
+ min_value = 1
+
+ if start_seek < 0:
+ start_seek = 0
+
+ if end_seek > self.audio_file.nframes:
+ end_seek = self.audio_file.nframes
+
+ if end_seek <= start_seek:
+ samples = self.read(start_seek, 1)
+ return (samples[0], samples[0])
+
+ if block_size > end_seek - start_seek:
+ block_size = end_seek - start_seek
+
+ for i in range(start_seek, end_seek, block_size):
+ samples = self.read(i, block_size)
+
+ local_max_index = numpy.argmax(samples)
+ local_max_value = samples[local_max_index]
+
+ if local_max_value > max_value:
+ max_value = local_max_value
+ max_index = local_max_index
+
+ local_min_index = numpy.argmin(samples)
+ local_min_value = samples[local_min_index]
+
+ if local_min_value < min_value:
+ min_value = local_min_value
+ min_index = local_min_index
+
+ return (min_value, max_value) if min_index < max_index else (max_value, min_value)
+
+
+def interpolate_colors(colors, flat=False, num_colors=256):
+ """ given a list of colors, create a larger list of colors interpolating
+ the first one. If flatten is True a list of numers will be returned. If
+ False, a list of (r,g,b) tuples. num_colors is the number of colors wanted
+ in the final list """
+
+ palette = []
+
+ for i in range(num_colors):
+ index = (i * (len(colors) - 1))/(num_colors - 1.0)
+ index_int = int(index)
+ alpha = index - float(index_int)
+
+ if alpha > 0:
+ r = (1.0 - alpha) * colors[index_int][0] + alpha * colors[index_int + 1][0]
+ g = (1.0 - alpha) * colors[index_int][1] + alpha * colors[index_int + 1][1]
+ b = (1.0 - alpha) * colors[index_int][2] + alpha * colors[index_int + 1][2]
+ else:
+ r = (1.0 - alpha) * colors[index_int][0]
+ g = (1.0 - alpha) * colors[index_int][1]
+ b = (1.0 - alpha) * colors[index_int][2]
+
+ if flat:
+ palette.extend((int(r), int(g), int(b)))
+ else:
+ palette.append((int(r), int(g), int(b)))
+
+ return palette
+
+
+def desaturate(rgb, amount):
+ """
+ desaturate colors by amount
+ amount == 0, no change
+ amount == 1, grey
+ """
+ luminosity = sum(rgb) / 3.0
+ desat = lambda color: color - amount * (color - luminosity)
+
+ return tuple(map(int, map(desat, rgb)))
+
+
+class WaveformImage(object):
+ """
+ Given peaks and spectral centroids from the AudioProcessor, this class will construct
+ a wavefile image which can be saved as PNG.
+ """
+ def __init__(self, image_width, image_height, palette=1):
+ if image_height % 2 == 0:
+ raise AudioProcessingException, "Height should be uneven: images look much better at uneven height"
+
+ if palette == 1:
+ background_color = (0,0,0)
+ colors = [
+ (50,0,200),
+ (0,220,80),
+ (255,224,0),
+ (255,70,0),
+ ]
+ elif palette == 2:
+ background_color = (0,0,0)
+ colors = [self.color_from_value(value/29.0) for value in range(0,30)]
+ elif palette == 3:
+ background_color = (213, 217, 221)
+ colors = map( partial(desaturate, amount=0.7), [
+ (50,0,200),
+ (0,220,80),
+ (255,224,0),
+ ])
+ elif palette == 4:
+ background_color = (213, 217, 221)
+ colors = map( partial(desaturate, amount=0.8), [self.color_from_value(value/29.0) for value in range(0,30)])
+
+ self.image = Image.new("RGB", (image_width, image_height), background_color)
+
+ self.image_width = image_width
+ self.image_height = image_height
+
+ self.draw = ImageDraw.Draw(self.image)
+ self.previous_x, self.previous_y = None, None
+
+ self.color_lookup = interpolate_colors(colors)
+ self.pix = self.image.load()
+
+ def color_from_value(self, value):
+ """ given a value between 0 and 1, return an (r,g,b) tuple """
+
+ return ImageColor.getrgb("hsl(%d,%d%%,%d%%)" % (int( (1.0 - value) * 360 ), 80, 50))
+
+ def draw_peaks(self, x, peaks, spectral_centroid):
+ """ draw 2 peaks at x using the spectral_centroid for color """
+
+ y1 = self.image_height * 0.5 - peaks[0] * (self.image_height - 4) * 0.5
+ y2 = self.image_height * 0.5 - peaks[1] * (self.image_height - 4) * 0.5
+
+ line_color = self.color_lookup[int(spectral_centroid*255.0)]
+
+ if self.previous_y != None:
+ self.draw.line([self.previous_x, self.previous_y, x, y1, x, y2], line_color)
+ else:
+ self.draw.line([x, y1, x, y2], line_color)
+
+ self.previous_x, self.previous_y = x, y2
+
+ self.draw_anti_aliased_pixels(x, y1, y2, line_color)
+
+ def draw_anti_aliased_pixels(self, x, y1, y2, color):
+ """ vertical anti-aliasing at y1 and y2 """
+
+ y_max = max(y1, y2)
+ y_max_int = int(y_max)
+ alpha = y_max - y_max_int
+
+ if alpha > 0.0 and alpha < 1.0 and y_max_int + 1 < self.image_height:
+ current_pix = self.pix[x, y_max_int + 1]
+
+ r = int((1-alpha)*current_pix[0] + alpha*color[0])
+ g = int((1-alpha)*current_pix[1] + alpha*color[1])
+ b = int((1-alpha)*current_pix[2] + alpha*color[2])
+
+ self.pix[x, y_max_int + 1] = (r,g,b)
+
+ y_min = min(y1, y2)
+ y_min_int = int(y_min)
+ alpha = 1.0 - (y_min - y_min_int)
+
+ if alpha > 0.0 and alpha < 1.0 and y_min_int - 1 >= 0:
+ current_pix = self.pix[x, y_min_int - 1]
+
+ r = int((1-alpha)*current_pix[0] + alpha*color[0])
+ g = int((1-alpha)*current_pix[1] + alpha*color[1])
+ b = int((1-alpha)*current_pix[2] + alpha*color[2])
+
+ self.pix[x, y_min_int - 1] = (r,g,b)
+
+ def save(self, filename):
+ # draw a zero "zero" line
+ a = 25
+ for x in range(self.image_width):
+ self.pix[x, self.image_height/2] = tuple(map(lambda p: p+a, self.pix[x, self.image_height/2]))
+
+ self.image.save(filename)
+
+
+class SpectrogramImage(object):
+ """
+ Given spectra from the AudioProcessor, this class will construct a wavefile image which
+ can be saved as PNG.
+ """
+ def __init__(self, image_width, image_height, fft_size):
+ self.image_width = image_width
+ self.image_height = image_height
+ self.fft_size = fft_size
+
+ self.image = Image.new("RGBA", (image_height, image_width))
+
+ colors = [
+ (0, 0, 0, 0),
+ (58/4, 68/4, 65/4, 255),
+ (80/2, 100/2, 153/2, 255),
+ (90, 180, 100, 255),
+ (224, 224, 44, 255),
+ (255, 60, 30, 255),
+ (255, 255, 255, 255)
+ ]
+ self.palette = interpolate_colors(colors)
+
+ # generate the lookup which translates y-coordinate to fft-bin
+ self.y_to_bin = []
+ f_min = 100.0
+ f_max = 22050.0
+ y_min = math.log10(f_min)
+ y_max = math.log10(f_max)
+ for y in range(self.image_height):
+ freq = math.pow(10.0, y_min + y / (image_height - 1.0) *(y_max - y_min))
+ bin = freq / 22050.0 * (self.fft_size/2 + 1)
+
+ if bin < self.fft_size/2:
+ alpha = bin - int(bin)
+
+ self.y_to_bin.append((int(bin), alpha * 255))
+
+ # this is a bit strange, but using image.load()[x,y] = ... is
+ # a lot slower than using image.putadata and then rotating the image
+ # so we store all the pixels in an array and then create the image when saving
+ self.pixels = []
+
+ def draw_spectrum(self, x, spectrum):
+ # for all frequencies, draw the pixels
+ for (index, alpha) in self.y_to_bin:
+ self.pixels.append( self.palette[int((255.0-alpha) * spectrum[index] + alpha * spectrum[index + 1])] )
+
+ # if the FFT is too small to fill up the image, fill with black to the top
+ for y in range(len(self.y_to_bin), self.image_height): #@UnusedVariable
+ self.pixels.append(self.palette[0])
+
+ def save(self, filename, quality=80):
+ assert filename.lower().endswith(".jpg")
+ self.image.putdata(self.pixels)
+ self.image.transpose(Image.ROTATE_90).save(filename, quality=quality)
+
+
+def create_wave_images(input_filename, output_filename_w, output_filename_s, image_width, image_height, fft_size, progress_callback=None):
+ """
+ Utility function for creating both wavefile and spectrum images from an audio input file.
+ """
+ processor = AudioProcessor(input_filename, fft_size, numpy.hanning)
+ samples_per_pixel = processor.audio_file.nframes / float(image_width)
+
+ waveform = WaveformImage(image_width, image_height)
+ spectrogram = SpectrogramImage(image_width, image_height, fft_size)
+
+ for x in range(image_width):
+
+ if progress_callback and x % (image_width/10) == 0:
+ progress_callback((x*100)/image_width)
+
+ seek_point = int(x * samples_per_pixel)
+ next_seek_point = int((x + 1) * samples_per_pixel)
+
+ (spectral_centroid, db_spectrum) = processor.spectral_centroid(seek_point)
+ peaks = processor.peaks(seek_point, next_seek_point)
+
+ waveform.draw_peaks(x, peaks, spectral_centroid)
+ spectrogram.draw_spectrum(x, db_spectrum)
+
+ if progress_callback:
+ progress_callback(100)
+
+ waveform.save(output_filename_w)
+ spectrogram.save(output_filename_s)
+
+
+class NoSpaceLeftException(Exception):
+ pass
+
+def convert_to_pcm(input_filename, output_filename):
+ """
+ converts any audio file type to pcm audio
+ """
+
+ if not os.path.exists(input_filename):
+ raise AudioProcessingException, "file %s does not exist" % input_filename
+
+ sound_type = get_sound_type(input_filename)
+
+ if sound_type == "mp3":
+ cmd = ["lame", "--decode", input_filename, output_filename]
+ elif sound_type == "ogg":
+ cmd = ["oggdec", input_filename, "-o", output_filename]
+ elif sound_type == "flac":
+ cmd = ["flac", "-f", "-d", "-s", "-o", output_filename, input_filename]
+ else:
+ return False
+
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout, stderr) = process.communicate()
+
+ if process.returncode != 0 or not os.path.exists(output_filename):
+ if "No space left on device" in stderr + " " + stdout:
+ raise NoSpaceLeftException
+ raise AudioProcessingException, "failed converting to pcm data:\n" + " ".join(cmd) + "\n" + stderr + "\n" + stdout
+
+ return True
+
+
+def stereofy_and_find_info(stereofy_executble_path, input_filename, output_filename):
+ """
+ converts a pcm wave file to two channel, 16 bit integer
+ """
+
+ if not os.path.exists(input_filename):
+ raise AudioProcessingException, "file %s does not exist" % input_filename
+
+ cmd = [stereofy_executble_path, "--input", input_filename, "--output", output_filename]
+
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout, stderr) = process.communicate()
+
+ if process.returncode != 0 or not os.path.exists(output_filename):
+ if "No space left on device" in stderr + " " + stdout:
+ raise NoSpaceLeftException
+ raise AudioProcessingException, "failed calling stereofy data:\n" + " ".join(cmd) + "\n" + stderr + "\n" + stdout
+
+ stdout = (stdout + " " + stderr).replace("\n", " ")
+
+ duration = 0
+ m = re.match(r".*#duration (?P<duration>[\d\.]+).*", stdout)
+ if m != None:
+ duration = float(m.group("duration"))
+
+ channels = 0
+ m = re.match(r".*#channels (?P<channels>\d+).*", stdout)
+ if m != None:
+ channels = float(m.group("channels"))
+
+ samplerate = 0
+ m = re.match(r".*#samplerate (?P<samplerate>\d+).*", stdout)
+ if m != None:
+ samplerate = float(m.group("samplerate"))
+
+ bitdepth = None
+ m = re.match(r".*#bitdepth (?P<bitdepth>\d+).*", stdout)
+ if m != None:
+ bitdepth = float(m.group("bitdepth"))
+
+ bitrate = (os.path.getsize(input_filename) * 8.0) / 1024.0 / duration if duration > 0 else 0
+
+ return dict(duration=duration, channels=channels, samplerate=samplerate, bitrate=bitrate, bitdepth=bitdepth)
+
+
+def convert_to_mp3(input_filename, output_filename, quality=70):
+ """
+ converts the incoming wave file to a mp3 file
+ """
+
+ if not os.path.exists(input_filename):
+ raise AudioProcessingException, "file %s does not exist" % input_filename
+
+ command = ["lame", "--silent", "--abr", str(quality), input_filename, output_filename]
+
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout, stderr) = process.communicate()
+
+ if process.returncode != 0 or not os.path.exists(output_filename):
+ raise AudioProcessingException, stdout
+
+def convert_to_ogg(input_filename, output_filename, quality=1):
+ """
+ converts the incoming wave file to n ogg file
+ """
+
+ if not os.path.exists(input_filename):
+ raise AudioProcessingException, "file %s does not exist" % input_filename
+
+ command = ["oggenc", "-q", str(quality), input_filename, "-o", output_filename]
+
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout, stderr) = process.communicate()
+
+ if process.returncode != 0 or not os.path.exists(output_filename):
+ raise AudioProcessingException, stdout
+
+def convert_using_ffmpeg(input_filename, output_filename):
+ """
+ converts the incoming wave file to stereo pcm using fffmpeg
+ """
+ TIMEOUT = 3 * 60
+ def alarm_handler(signum, frame):
+ raise AudioProcessingException, "timeout while waiting for ffmpeg"
+
+ if not os.path.exists(input_filename):
+ raise AudioProcessingException, "file %s does not exist" % input_filename
+
+ command = ["ffmpeg", "-y", "-i", input_filename, "-ac","1","-acodec", "pcm_s16le", "-ar", "44100", output_filename]
+
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ signal.signal(signal.SIGALRM,alarm_handler)
+ signal.alarm(TIMEOUT)
+ (stdout, stderr) = process.communicate()
+ signal.alarm(0)
+ if process.returncode != 0 or not os.path.exists(output_filename):
+ raise AudioProcessingException, stdout
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 866d799b..e30825de 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -69,11 +69,28 @@ base_url = string(default="/mgoblin_media/")
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
base_dir = string(default="%(here)s/user_dev/media/queue")
+[media:medium]
+# Dimensions used when creating media display images.
+max_width = integer(default=640)
+max_height = integer(default=640)
+
+[media:thumb]
+# Dimensions used when creating media thumbnails
+# This is unfortunately not implemented in the media
+# types yet. You can help!
+# TODO: Make plugins follow the media size settings
+max_width = integer(default=180)
+max_height = integer(default=180)
-# Should we keep the original file?
[media_type:mediagoblin.media_types.video]
+# Should we keep the original file?
keep_original = boolean(default=False)
+[media_type:mediagoblin.media_types.audio]
+# vorbisenc qualiy
+quality = float(default=0.3)
+create_spectrogram = boolean(default=True)
+
[beaker.cache]
type = string(default="file")
diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py
index 5128826b..93d2319f 100644
--- a/mediagoblin/media_types/__init__.py
+++ b/mediagoblin/media_types/__init__.py
@@ -16,10 +16,13 @@
import os
import sys
+import logging
+import tempfile
from mediagoblin import mg_globals
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+_log = logging.getLogger(__name__)
class FileTypeNotSupported(Exception):
pass
@@ -28,6 +31,35 @@ class InvalidFileType(Exception):
pass
+def sniff_media(media):
+ '''
+ Iterate through the enabled media types and find those suited
+ for a certain file.
+ '''
+
+ try:
+ return get_media_type_and_manager(media.filename)
+ except FileTypeNotSupported:
+ _log.info('No media handler found by file extension. Doing it the expensive way...')
+ # Create a temporary file for sniffers suchs as GStreamer-based
+ # Audio video
+ media_file = tempfile.NamedTemporaryFile()
+ media_file.write(media.file.read())
+ media.file.seek(0)
+
+ for media_type, manager in get_media_managers():
+ _log.info('Sniffing {0}'.format(media_type))
+ if manager['sniff_handler'](media_file, media=media):
+ _log.info('{0} accepts the file'.format(media_type))
+ return media_type, manager
+ else:
+ _log.debug('{0} did not accept the file'.format(media_type))
+
+ raise FileTypeNotSupported(
+ # TODO: Provide information on which file types are supported
+ _(u'Sorry, I don\'t support that file type :('))
+
+
def get_media_types():
"""
Generator, yields the available media types
@@ -42,7 +74,7 @@ def get_media_managers():
'''
for media_type in get_media_types():
__import__(media_type)
-
+
yield media_type, sys.modules[media_type].MEDIA_MANAGER
@@ -67,22 +99,22 @@ def get_media_manager(_media_type):
def get_media_type_and_manager(filename):
'''
- Get the media type and manager based on a filename
+ Try to find the media type based on the file name, extension
+ specifically. This is used as a speedup, the sniffing functionality
+ then falls back on more in-depth bitsniffing of the source file.
'''
if filename.find('.') > 0:
# Get the file extension
ext = os.path.splitext(filename)[1].lower()
- else:
- raise InvalidFileType(
- _(u'Could not extract any file extension from "{filename}"').format(
- filename=filename))
- for media_type, manager in get_media_managers():
- # Omit the dot from the extension and match it against
- # the media manager
- if ext[1:] in manager['accepted_extensions']:
- return media_type, manager
+ for media_type, manager in get_media_managers():
+ # Omit the dot from the extension and match it against
+ # the media manager
+ if ext[1:] in manager['accepted_extensions']:
+ return media_type, manager
else:
- raise FileTypeNotSupported(
- # TODO: Provide information on which file types are supported
- _(u'Sorry, I don\'t support that file type :('))
+ _log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
+ filename))
+
+ raise FileTypeNotSupported(
+ _(u'Sorry, I don\'t support that file type :('))
diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py
index 1c8ca562..856d1d7b 100644
--- a/mediagoblin/media_types/ascii/__init__.py
+++ b/mediagoblin/media_types/ascii/__init__.py
@@ -14,13 +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/>.
-from mediagoblin.media_types.ascii.processing import process_ascii
+from mediagoblin.media_types.ascii.processing import process_ascii, \
+ sniff_handler
MEDIA_MANAGER = {
"human_readable": "ASCII",
"processor": process_ascii, # alternately a string,
# 'mediagoblin.media_types.image.processing'?
+ "sniff_handler": sniff_handler,
"display_template": "mediagoblin/media_displays/ascii.html",
"default_thumb": "images/media_thumbs/ascii.jpg",
"accepted_extensions": [
diff --git a/mediagoblin/media_types/ascii/asciitoimage.py b/mediagoblin/media_types/ascii/asciitoimage.py
index e1c4fb44..3017d2ad 100644
--- a/mediagoblin/media_types/ascii/asciitoimage.py
+++ b/mediagoblin/media_types/ascii/asciitoimage.py
@@ -23,6 +23,7 @@ import os
_log = logging.getLogger(__name__)
+
class AsciiToImage(object):
'''
Converter of ASCII art into image files, preserving whitespace
diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py
index 83b5ea33..a2a52e9d 100644
--- a/mediagoblin/media_types/ascii/processing.py
+++ b/mediagoblin/media_types/ascii/processing.py
@@ -19,11 +19,25 @@ import Image
import logging
from mediagoblin import mg_globals as mgg
-from mediagoblin.processing import create_pub_filepath, THUMB_SIZE
+from mediagoblin.processing import create_pub_filepath
from mediagoblin.media_types.ascii import asciitoimage
_log = logging.getLogger(__name__)
+SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
+
+
+def sniff_handler(media_file, **kw):
+ if kw.get('media') is not None:
+ name, ext = os.path.splitext(kw['media'].filename)
+ clean_ext = ext[1:].lower()
+
+ if clean_ext in SUPPORTED_EXTENSIONS:
+ return True
+
+ return False
+
+
def process_ascii(entry):
'''
Code to process a txt file
@@ -69,7 +83,10 @@ def process_ascii(entry):
queued_file.read())
with file(tmp_thumb_filename, 'w') as thumb_file:
- thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
+ thumb.thumbnail(
+ (mgg.global_config['media:thumb']['max_width'],
+ mgg.global_config['media:thumb']['max_height']),
+ Image.ANTIALIAS)
thumb.save(thumb_file)
_log.debug('Copying local file to public storage')
@@ -84,7 +101,6 @@ def process_ascii(entry):
as original_file:
original_file.write(queued_file.read())
-
queued_file.seek(0) # Rewind *again*
unicode_filepath = create_pub_filepath(entry, 'ascii-portable.txt')
diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py
new file mode 100644
index 00000000..9b33f9e3
--- /dev/null
+++ b/mediagoblin/media_types/audio/__init__.py
@@ -0,0 +1,25 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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.audio.processing import process_audio, \
+ sniff_handler
+
+MEDIA_MANAGER = {
+ 'human_readable': 'Audio',
+ 'processor': process_audio,
+ 'sniff_handler': sniff_handler,
+ 'display_template': 'mediagoblin/media_displays/audio.html',
+ 'accepted_extensions': ['mp3', 'flac', 'ogg', 'wav', 'm4a']}
diff --git a/mediagoblin/media_types/audio/audioprocessing.py b/mediagoblin/media_types/audio/audioprocessing.py
new file mode 120000
index 00000000..c5e3c52c
--- /dev/null
+++ b/mediagoblin/media_types/audio/audioprocessing.py
@@ -0,0 +1 @@
+../../../extlib/freesound/audioprocessing.py \ No newline at end of file
diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py
new file mode 100644
index 00000000..62daf412
--- /dev/null
+++ b/mediagoblin/media_types/audio/processing.py
@@ -0,0 +1,131 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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 logging
+import tempfile
+import os
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import create_pub_filepath, BadMediaFail
+
+from mediagoblin.media_types.audio.transcoders import AudioTranscoder, \
+ AudioThumbnailer
+
+_log = logging.getLogger(__name__)
+
+def sniff_handler(media_file, **kw):
+ try:
+ transcoder = AudioTranscoder()
+ data = transcoder.discover(media_file.name)
+ except BadMediaFail:
+ _log.debug('Audio discovery raised BadMediaFail')
+ return False
+
+ if data.is_audio == True and data.is_video == False:
+ return True
+
+ return False
+
+def process_audio(entry):
+ audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
+
+ workbench = mgg.workbench_manager.create_workbench()
+
+ queued_filepath = entry.queued_media_file
+ queued_filename = workbench.localized_file(
+ mgg.queue_store, queued_filepath,
+ 'source')
+
+ ogg_filepath = create_pub_filepath(
+ entry,
+ '{original}.webm'.format(
+ original=os.path.splitext(
+ queued_filepath[-1])[0]))
+
+ transcoder = AudioTranscoder()
+
+ with tempfile.NamedTemporaryFile() as ogg_tmp:
+
+ transcoder.transcode(
+ queued_filename,
+ ogg_tmp.name,
+ quality=audio_config['quality'])
+
+ data = transcoder.discover(ogg_tmp.name)
+
+ _log.debug('Saving medium...')
+ mgg.public_store.get_file(ogg_filepath, 'wb').write(
+ ogg_tmp.read())
+
+ entry.media_files['ogg'] = ogg_filepath
+
+ entry.media_data['audio'] = {
+ u'length': int(data.audiolength)}
+
+ if audio_config['create_spectrogram']:
+ spectrogram_filepath = create_pub_filepath(
+ entry,
+ '{original}-spectrogram.jpg'.format(
+ original=os.path.splitext(
+ queued_filepath[-1])[0]))
+
+ with tempfile.NamedTemporaryFile(suffix='.wav') as wav_tmp:
+ _log.info('Creating WAV source for spectrogram')
+ transcoder.transcode(
+ queued_filename,
+ wav_tmp.name,
+ mux_string='wavenc')
+
+ thumbnailer = AudioThumbnailer()
+
+ with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp:
+ thumbnailer.spectrogram(
+ wav_tmp.name,
+ spectrogram_tmp.name,
+ width=mgg.global_config['media:medium']['max_width'])
+
+ _log.debug('Saving spectrogram...')
+ mgg.public_store.get_file(spectrogram_filepath, 'wb').write(
+ spectrogram_tmp.read())
+
+ entry.media_files['spectrogram'] = spectrogram_filepath
+
+ with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp:
+ thumbnailer.thumbnail_spectrogram(
+ spectrogram_tmp.name,
+ thumb_tmp.name,
+ (mgg.global_config['media:thumb']['max_width'],
+ mgg.global_config['media:thumb']['max_height']))
+
+ thumb_filepath = create_pub_filepath(
+ entry,
+ '{original}-thumbnail.jpg'.format(
+ original=os.path.splitext(
+ queued_filepath[-1])[0]))
+
+ mgg.public_store.get_file(thumb_filepath, 'wb').write(
+ thumb_tmp.read())
+
+ entry.media_files['thumb'] = thumb_filepath
+ else:
+ entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
+
+ mgg.queue_store.delete_file(queued_filepath)
+
+ entry.save()
+
+ # clean up workbench
+ workbench.destroy_self()
diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py
new file mode 100644
index 00000000..f84ab7f2
--- /dev/null
+++ b/mediagoblin/media_types/audio/transcoders.py
@@ -0,0 +1,237 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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 pdb
+import logging
+from PIL import Image
+
+from mediagoblin.processing import BadMediaFail
+from mediagoblin.media_types.audio import audioprocessing
+
+
+_log = logging.getLogger(__name__)
+
+CPU_COUNT = 2 # Just assuming for now
+
+# IMPORT MULTIPROCESSING
+try:
+ import multiprocessing
+ try:
+ CPU_COUNT = multiprocessing.cpu_count()
+ except NotImplementedError:
+ _log.warning('multiprocessing.cpu_count not implemented!\n'
+ 'Assuming 2 CPU cores')
+except ImportError:
+ _log.warning('Could not import multiprocessing, assuming 2 CPU cores')
+
+# IMPORT GOBJECT
+try:
+ import gobject
+ gobject.threads_init()
+except ImportError:
+ raise Exception('gobject could not be found')
+
+# IMPORT PYGST
+try:
+ import pygst
+
+ # We won't settle for less. For now, this is an arbitrary limit
+ # as we have not tested with > 0.10
+ pygst.require('0.10')
+
+ import gst
+
+ import gst.extend.discoverer
+except ImportError:
+ raise Exception('gst/pygst >= 0.10 could not be imported')
+
+import numpy
+
+
+class AudioThumbnailer(object):
+ def __init__(self):
+ _log.info('Initializing {0}'.format(self.__class__.__name__))
+
+ def spectrogram(self, src, dst, **kw):
+ width = kw['width']
+ height = int(kw.get('height', float(width) * 0.3))
+ fft_size = kw.get('fft_size', 2048)
+ callback = kw.get('progress_callback')
+
+ processor = audioprocessing.AudioProcessor(
+ src,
+ fft_size,
+ numpy.hanning)
+
+ samples_per_pixel = processor.audio_file.nframes / float(width)
+
+ spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
+
+ for x in range(width):
+ if callback and x % (width / 10) == 0:
+ callback((x * 100) / width)
+
+ seek_point = int(x * samples_per_pixel)
+
+ (spectral_centroid, db_spectrum) = processor.spectral_centroid(
+ seek_point)
+
+ spectrogram.draw_spectrum(x, db_spectrum)
+
+ if callback:
+ callback(100)
+
+ spectrogram.save(dst)
+
+ def thumbnail_spectrogram(self, src, dst, thumb_size):
+ '''
+ Takes a spectrogram and creates a thumbnail from it
+ '''
+ if not (type(thumb_size) == tuple and len(thumb_size) == 2):
+ raise Exception('thumb_size argument should be a tuple(width, height)')
+
+ im = Image.open(src)
+
+ im_w, im_h = [float(i) for i in im.size]
+ th_w, th_h = [float(i) for i in thumb_size]
+
+ wadsworth_position = im_w * 0.3
+
+ start_x = max((
+ wadsworth_position - ((im_h * (th_w / th_h)) / 2.0),
+ 0.0))
+
+ stop_x = start_x + (im_h * (th_w / th_h))
+
+ th = im.crop((
+ int(start_x), 0,
+ int(stop_x), int(im_h)))
+
+ if th.size[0] > th_w or th.size[1] > th_h:
+ th.thumbnail(thumb_size, Image.ANTIALIAS)
+
+ th.save(dst)
+
+
+class AudioTranscoder(object):
+ def __init__(self):
+ _log.info('Initializing {0}'.format(self.__class__.__name__))
+
+ # Instantiate MainLoop
+ self._loop = gobject.MainLoop()
+ self._failed = None
+
+ def discover(self, src):
+ self._src_path = src
+ _log.info('Discovering {0}'.format(src))
+ self._discovery_path = src
+
+ self._discoverer = gst.extend.discoverer.Discoverer(
+ self._discovery_path)
+ self._discoverer.connect('discovered', self.__on_discovered)
+ self._discoverer.discover()
+
+ self._loop.run() # Run MainLoop
+
+ if self._failed:
+ raise self._failed
+
+ # Once MainLoop has returned, return discovery data
+ return getattr(self, '_discovery_data', False)
+
+ def __on_discovered(self, data, is_media):
+ if not is_media:
+ self._failed = BadMediaFail()
+ _log.error('Could not discover {0}'.format(self._src_path))
+ self.halt()
+
+ _log.debug('Discovered: {0}'.format(data.__dict__))
+
+ self._discovery_data = data
+
+ # Gracefully shut down MainLoop
+ self.halt()
+
+ def transcode(self, src, dst, **kw):
+ _log.info('Transcoding {0} into {1}'.format(src, dst))
+ self._discovery_data = kw.get('data', self.discover(src))
+
+ self.__on_progress = kw.get('progress_callback')
+
+ quality = kw.get('quality', 0.3)
+
+ mux_string = kw.get(
+ 'mux_string',
+ 'vorbisenc quality={0} ! webmmux'.format(quality))
+
+ # Set up pipeline
+ self.pipeline = gst.parse_launch(
+ 'filesrc location="{src}" ! '
+ 'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
+ 'audioconvert ! audio/x-raw-float,channels=2 ! '
+ '{mux_string} ! '
+ 'progressreport silent=true ! '
+ 'filesink location="{dst}"'.format(
+ src=src,
+ tolerance=80000000,
+ mux_string=mux_string,
+ dst=dst))
+
+ self.bus = self.pipeline.get_bus()
+ self.bus.add_signal_watch()
+ self.bus.connect('message', self.__on_bus_message)
+
+ self.pipeline.set_state(gst.STATE_PLAYING)
+
+ self._loop.run()
+
+ def __on_bus_message(self, bus, message):
+ _log.debug(message)
+
+ if (message.type == gst.MESSAGE_ELEMENT
+ and message.structure.get_name() == 'progress'):
+ data = dict(message.structure)
+
+ if self.__on_progress:
+ self.__on_progress(data)
+
+ _log.info('{0}% done...'.format(
+ data.get('percent')))
+ elif message.type == gst.MESSAGE_EOS:
+ _log.info('Done')
+ self.halt()
+
+ def halt(self):
+ if getattr(self, 'pipeline', False):
+ self.pipeline.set_state(gst.STATE_NULL)
+ del self.pipeline
+ _log.info('Quitting MainLoop gracefully...')
+ gobject.idle_add(self._loop.quit)
+
+if __name__ == '__main__':
+ import sys
+ logging.basicConfig()
+ _log.setLevel(logging.INFO)
+
+ #transcoder = AudioTranscoder()
+ #data = transcoder.discover(sys.argv[1])
+ #res = transcoder.transcode(*sys.argv[1:3])
+
+ thumbnailer = AudioThumbnailer()
+
+ thumbnailer.spectrogram(*sys.argv[1:], width=640)
+
+ pdb.set_trace()
diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py
index 98e0c32a..d4720fab 100644
--- a/mediagoblin/media_types/image/__init__.py
+++ b/mediagoblin/media_types/image/__init__.py
@@ -14,13 +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/>.
-from mediagoblin.media_types.image.processing import process_image
+from mediagoblin.media_types.image.processing import process_image, \
+ sniff_handler
MEDIA_MANAGER = {
"human_readable": "Image",
"processor": process_image, # alternately a string,
# 'mediagoblin.media_types.image.processing'?
+ "sniff_handler": sniff_handler,
"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/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index 43a4a484..bbfcd32d 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -16,14 +16,18 @@
import Image
import os
+import logging
from mediagoblin import mg_globals as mgg
from mediagoblin.processing import BadMediaFail, \
- create_pub_filepath, THUMB_SIZE, MEDIUM_SIZE, FilenameBuilder
+ create_pub_filepath, FilenameBuilder
from mediagoblin.tools.exif import exif_fix_image_orientation, \
extract_exif, clean_exif, get_gps_data, get_useful, \
exif_image_needs_rotation
+_log = logging.getLogger(__name__)
+
+
def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
size_limits=(0, 0)):
"""Store a resized version of an image and return its pathname.
@@ -35,18 +39,13 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
exif_tags -- EXIF data for the original image
workdir -- directory path for storing converted image files
new_size -- 2-tuple size for the resized image
- size_limits (optional) -- image is only resized if it exceeds this size
-
"""
try:
resized = Image.open(filename)
except IOError:
raise BadMediaFail()
resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
-
- if ((resized.size[0] > size_limits[0]) or
- (resized.size[1] > size_limits[1])):
- resized.thumbnail(new_size, Image.ANTIALIAS)
+ resized.thumbnail(new_size, Image.ANTIALIAS)
# Copy the new file to the conversion subdir, then remotely.
tmp_resized_filename = os.path.join(workdir, new_path[-1])
@@ -54,6 +53,33 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
resized.save(resized_file)
mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path)
+
+SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
+
+
+def sniff_handler(media_file, **kw):
+ if kw.get('media') is not None: # That's a double negative!
+ name, ext = os.path.splitext(kw['media'].filename)
+ clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase
+
+ _log.debug('name: {0}\next: {1}\nlower_ext: {2}'.format(
+ name,
+ ext,
+ clean_ext))
+
+ if clean_ext in SUPPORTED_FILETYPES:
+ _log.info('Found file extension in supported filetypes')
+ return True
+ else:
+ _log.debug('Media present, extension not found in {0}'.format(
+ SUPPORTED_FILETYPES))
+ else:
+ _log.warning('Need additional information (keyword argument \'media\')'
+ ' to be able to handle sniffing')
+
+ return False
+
+
def process_image(entry):
"""
Code to process an image
@@ -77,19 +103,24 @@ def process_image(entry):
thumb_filepath = create_pub_filepath(
entry, name_builder.fill('{basename}.thumbnail{ext}'))
resize_image(entry, queued_filename, thumb_filepath,
- exif_tags, conversions_subdir, THUMB_SIZE)
+ exif_tags, conversions_subdir,
+ (mgg.global_config['media:thumb']['max_width'],
+ mgg.global_config['media:thumb']['max_height']))
# If the size of the original file exceeds the specified size of a `medium`
# file, a `.medium.jpg` files is created and later associated with the media
# entry.
medium = Image.open(queued_filename)
- if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1] \
- or exif_image_needs_rotation(exif_tags):
+ if medium.size[0] > mgg.global_config['media:medium']['max_width'] \
+ or medium.size[1] > mgg.global_config['media:medium']['max_height'] \
+ or exif_image_needs_rotation(exif_tags):
medium_filepath = create_pub_filepath(
entry, name_builder.fill('{basename}.medium{ext}'))
resize_image(
entry, queued_filename, medium_filepath,
- exif_tags, conversions_subdir, MEDIUM_SIZE, MEDIUM_SIZE)
+ exif_tags, conversions_subdir,
+ (mgg.global_config['media:medium']['max_width'],
+ mgg.global_config['media:medium']['max_height']))
else:
medium_filepath = None
@@ -99,7 +130,7 @@ def process_image(entry):
with queued_file:
original_filepath = create_pub_filepath(
- entry, name_builder.fill('{basename}{ext}') )
+ entry, name_builder.fill('{basename}{ext}'))
with mgg.public_store.get_file(original_filepath, 'wb') \
as original_file:
diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py
index 579fdc6a..3faa5b9f 100644
--- a/mediagoblin/media_types/video/__init__.py
+++ b/mediagoblin/media_types/video/__init__.py
@@ -14,16 +14,16 @@
# 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
+from mediagoblin.media_types.video.processing import process_video, \
+ sniff_handler
MEDIA_MANAGER = {
"human_readable": "Video",
- "processor": process_video, # alternately a string,
- # 'mediagoblin.media_types.image.processing'?
+ "processor": process_video, # alternately a string,
+ # 'mediagoblin.media_types.image.processing'?
+ "sniff_handler": sniff_handler,
"display_template": "mediagoblin/media_displays/video.html",
"default_thumb": "images/media_thumbs/video.jpg",
- # TODO: This list should be autogenerated based on gst plugins
"accepted_extensions": [
- "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "ogg",
- "m4v"]}
+ "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]}
diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py
index 24c03648..6a5ce364 100644
--- a/mediagoblin/media_types/video/processing.py
+++ b/mediagoblin/media_types/video/processing.py
@@ -20,7 +20,7 @@ import os
from mediagoblin import mg_globals as mgg
from mediagoblin.processing import mark_entry_failed, \
- THUMB_SIZE, MEDIUM_SIZE, create_pub_filepath, FilenameBuilder
+ create_pub_filepath, FilenameBuilder
from . import transcoders
logging.basicConfig()
@@ -29,17 +29,27 @@ _log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG)
-def process_video(entry):
- """
- Code to process a video
+def sniff_handler(media_file, **kw):
+ transcoder = transcoders.VideoTranscoder()
+ data = transcoder.discover(media_file.name)
+
+ _log.debug('Discovered: {0}'.format(data))
+
+ if not data:
+ _log.error('Could not discover {0}'.format(
+ kw.get('media')))
+ return False
- Much of this code is derived from the arista-transcoder script in
- the arista PyPI package and changed to match the needs of
- MediaGoblin
+ if data['is_video'] == True:
+ return True
- 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.
+ return False
+
+
+def process_video(entry):
+ """
+ Process a video entry, transcode the queued media files (originals) and
+ create a thumbnail for the entry.
"""
video_config = mgg.global_config['media_type:mediagoblin.media_types.video']
@@ -62,7 +72,8 @@ def process_video(entry):
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)
+ transcoder = transcoders.VideoTranscoder()
+ transcoder.transcode(queued_filename, tmp_dst.name)
# Push transcoded video to public storage
_log.debug('Saving medium...')
diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py
index 6137c3bf..e0bd0d3d 100644
--- a/mediagoblin/media_types/video/transcoders.py
+++ b/mediagoblin/media_types/video/transcoders.py
@@ -21,12 +21,9 @@ 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:
@@ -38,17 +35,16 @@ try:
pass
except ImportError:
_log.warning('Could not import multiprocessing, defaulting to 2 CPU cores')
- pass
try:
import gtk
-except:
+except ImportError:
raise Exception('Could not find pygtk')
try:
import gobject
gobject.threads_init()
-except:
+except ImportError:
raise Exception('gobject could not be found')
try:
@@ -56,7 +52,7 @@ try:
pygst.require('0.10')
import gst
from gst.extend import discoverer
-except:
+except ImportError:
raise Exception('gst/pygst 0.10 could not be found')
@@ -270,7 +266,7 @@ class VideoThumbnailer:
return 0
try:
- return pipeline.query_duration(gst.FORMAT_TIME)[0]
+ return pipeline.query_duration(gst.FORMAT_TIME)[0]
except gst.QueryError:
return self._get_duration(pipeline, retries + 1)
@@ -320,12 +316,11 @@ class VideoThumbnailer:
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()
@@ -341,10 +336,15 @@ class VideoTranscoder:
that it was refined afterwards and therefore is done more
correctly.
'''
- def __init__(self, src, dst, **kwargs):
+ def __init__(self):
_log.info('Initializing VideoTranscoder...')
self.loop = gobject.MainLoop()
+
+ def transcode(self, src, dst, **kwargs):
+ '''
+ Transcode a video file into a 'medium'-sized version.
+ '''
self.source_path = src
self.destination_path = dst
@@ -358,6 +358,34 @@ class VideoTranscoder:
self._setup()
self._run()
+ def discover(self, src):
+ '''
+ Discover properties about a media file
+ '''
+ _log.info('Discovering {0}'.format(src))
+
+ self.source_path = src
+ self._setup_discover(discovered_callback=self.__on_discovered)
+
+ self.discoverer.discover()
+
+ self.loop.run()
+
+ if hasattr(self, '_discovered_data'):
+ return self._discovered_data.__dict__
+ else:
+ return None
+
+ def __on_discovered(self, data, is_media):
+ _log.debug('Discovered: {0}'.format(data))
+ if not is_media:
+ self.__stop()
+ raise Exception('Could not discover {0}'.format(self.source_path))
+
+ self._discovered_data = data
+
+ self.__stop_mainloop()
+
def _setup(self):
self._setup_discover()
self._setup_pipeline()
@@ -370,12 +398,14 @@ class VideoTranscoder:
_log.debug('Initializing MainLoop()')
self.loop.run()
- def _setup_discover(self):
+ def _setup_discover(self, **kw):
_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)
+ self.discoverer.connect(
+ 'discovered',
+ kw.get('discovered_callback', self.__discovered))
def __discovered(self, data, is_media):
'''
@@ -422,7 +452,7 @@ class VideoTranscoder:
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)
@@ -516,7 +546,6 @@ class VideoTranscoder:
# 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
@@ -561,11 +590,11 @@ class VideoTranscoder:
t = message.type
- if t == gst.MESSAGE_EOS:
+ if message.type == gst.MESSAGE_EOS:
self._discover_dst_and_stop()
_log.info('Done')
- elif t == gst.MESSAGE_ELEMENT:
+ elif message.type == gst.MESSAGE_ELEMENT:
if message.structure.get_name() == 'progress':
data = dict(message.structure)
@@ -587,7 +616,6 @@ class VideoTranscoder:
self.dst_discoverer.discover()
-
def __dst_discovered(self, data, is_media):
self.dst_data = data
@@ -596,8 +624,9 @@ class VideoTranscoder:
def __stop(self):
_log.debug(self.loop)
- # Stop executing the pipeline
- self.pipeline.set_state(gst.STATE_NULL)
+ if hasattr(self, 'pipeline'):
+ # Stop executing the pipeline
+ self.pipeline.set_state(gst.STATE_NULL)
# This kills the loop, mercifully
gobject.idle_add(self.__stop_mainloop)
@@ -615,14 +644,15 @@ class VideoTranscoder:
if __name__ == '__main__':
os.nice(19)
+ logging.basicConfig()
from optparse import OptionParser
parser = OptionParser(
- usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
+ usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]')
parser.add_option('-a', '--action',
dest='action',
- help='One of "video" or "thumbnail"')
+ help='One of "video", "discover" or "thumbnail"')
parser.add_option('-v',
dest='verbose',
@@ -646,13 +676,17 @@ if __name__ == '__main__':
_log.debug(args)
- if not len(args) == 2:
+ if not len(args) == 2 and not options.action == 'discover':
parser.print_help()
sys.exit()
+ transcoder = VideoTranscoder()
+
if options.action == 'thumbnail':
VideoThumbnailer(*args)
elif options.action == 'video':
def cb(data):
print('I\'m a callback!')
- transcoder = VideoTranscoder(*args, progress_callback=cb)
+ transcoder.transcode(*args, progress_callback=cb)
+ elif options.action == 'discover':
+ print transcoder.discover(*args).__dict__
diff --git a/mediagoblin/processing/__init__.py b/mediagoblin/processing/__init__.py
index 9dee3baa..4a827af4 100644
--- a/mediagoblin/processing/__init__.py
+++ b/mediagoblin/processing/__init__.py
@@ -24,9 +24,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
_log = logging.getLogger(__name__)
-THUMB_SIZE = 180, 180
-MEDIUM_SIZE = 640, 640
-
def create_pub_filepath(entry, filename):
return mgg.public_store.get_unique_filepath(
diff --git a/mediagoblin/static/css/audio.css b/mediagoblin/static/css/audio.css
new file mode 100644
index 00000000..5f7a888a
--- /dev/null
+++ b/mediagoblin/static/css/audio.css
@@ -0,0 +1,53 @@
+.audio-spectrogram {
+ position: relative;
+}
+.playhead {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: rgba(134, 212, 177, 0.3);
+ border-right: thin solid #ffaa00;
+ height: 100%;
+ -webkit-transition: width .1s ease-out;
+ -moz-transition: width .1s ease-out;
+}
+.audio-control-play-pause {
+ position: absolute;
+ bottom: 0;
+ left: 5px;
+ cursor: pointer;
+ /* background: rgba(0, 0, 0, 0.7); */
+ font-size: 40px;
+ width: 50px;
+ text-shadow: 0 0 10px black;
+}
+ .audio-control-play-pause.playing {
+ color: #b71500;
+ }
+ .audio-control-play-pause.paused {
+ color: rgb(134, 212, 177);
+ }
+.buffered {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ height: 2px;
+ width: 0;
+ -webkit-transition: width 1s ease-out;
+ -moz-transition: width 1s ease-out;
+ background: rgba(134, 177, 212, 1);
+ cursor: pointer;
+}
+.seekbar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+.audio-currentTime {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.7);
+}
diff --git a/mediagoblin/static/js/audio.js b/mediagoblin/static/js/audio.js
new file mode 100644
index 00000000..91d52f96
--- /dev/null
+++ b/mediagoblin/static/js/audio.js
@@ -0,0 +1,146 @@
+/**
+ * GNU MediaGoblin -- federated, autonomous media hosting
+ * Copyright (C) 2011, 2012 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/>.
+ */
+
+var audioPlayer = new Object();
+
+(function (audioPlayer) {
+ audioPlayer.init = function (audioElement) {
+ audioPlayer.audioElement = audioElement;
+ console.log(audioElement);
+ attachEvents();
+ $(audioElement).hide();
+ };
+
+ function attachEvents () {
+ audioPlayer.audioElement.addEventListener('durationchange', audioPlayer.durationChange, true);
+ audioPlayer.audioElement.addEventListener('timeupdate', audioPlayer.timeUpdate, true);
+ audioPlayer.audioElement.addEventListener('progress', audioPlayer.onProgress, true);
+ $(document).ready( function () {
+ $('.audio-spectrogram').delegate('.seekbar', 'click', audioPlayer.onSeek);
+ $('.audio-spectrogram').delegate('.audio-control-play-pause', 'click', audioPlayer.playPause);
+ });
+ }
+
+ audioPlayer.onProgress = function(a, b, c) {
+ console.log(a, b, c);
+ buffered = audioPlayer.audioElement.buffered;
+
+ ranges = new Array();
+
+ for (i = 0; i < buffered.length; i++) {
+ ranges[i] = new Array();
+ ranges[i][0] = buffered.start(i);
+ ranges[i][1] = buffered.end(i);
+ }
+ console.log('ranges', ranges);
+ $('.audio-spectrogram .buffered').width(
+ (ranges[0][1] / audioPlayer.audioElement.duration) * audioPlayer.imageElement.width());
+ };
+
+ audioPlayer.onSeek = function (e) {
+ console.log('onSeek', e);
+ im = audioPlayer.imageElement;
+ pos = e.offsetX / im.width();
+ audioPlayer.audioElement.currentTime = pos * audioPlayer.audioElement.duration;
+ audioPlayer.audioElement.play();
+ audioPlayer.setState(audioPlayer.PLAYING);
+ };
+
+ audioPlayer.playPause = function (e) {
+ console.log('playPause', e);
+ if (audioPlayer.audioElement.paused) {
+ audioPlayer.audioElement.play();
+ audioPlayer.setState(audioPlayer.PLAYING);
+ } else {
+ audioPlayer.audioElement.pause();
+ audioPlayer.setState(audioPlayer.PAUSED);
+ }
+ };
+
+ audioPlayer.NULL = null;
+ audioPlayer.PLAYING = 2;
+ audioPlayer.PAUSED = 4;
+
+ audioPlayer.state = audioPlayer.NULL;
+
+ audioPlayer.setState = function (state) {
+ if (state == audioPlayer.state) {
+ return;
+ }
+
+ switch (state) {
+ case audioPlayer.PLAYING:
+ $('.audio-spectrogram .audio-control-play-pause')
+ .removeClass('paused').addClass('playing')
+ .text('■');
+ break;
+ case audioPlayer.PAUSED:
+ $('.audio-spectrogram .audio-control-play-pause')
+ .removeClass('playing').addClass('paused')
+ .text('▶');
+ break;
+ }
+ };
+
+ audioPlayer.durationChange = function () {
+ duration = audioPlayer.audioElement.duration;
+ };
+
+ audioPlayer.timeUpdate = function () {
+ currentTime = audioPlayer.audioElement.currentTime;
+ playhead = audioPlayer.imageElement.parent().find('.playhead');
+ playhead.css('width', (currentTime / audioPlayer.audioElement.duration) * audioPlayer.imageElement.width());
+ time = formatTime(currentTime);
+ duration = formatTime(audioPlayer.audioElement.duration);
+ audioPlayer.imageElement.parent().find('.audio-currentTime').text(time + '/' + duration);
+ };
+
+ function formatTime(seconds) {
+ var h = Math.floor(seconds / (60 * 60));
+ var m = Math.floor((seconds - h * 60 * 60) / 60);
+ var s = Math.round(seconds - h * 60 * 60 - m * 60);
+ return '' + (h ? (h < 10 ? '0' + h : h) + ':' : '') + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
+ }
+
+ audioPlayer.attachToImage = function (imageElement) {
+ /**
+ * Attach the player to an image element
+ */
+ console.log(imageElement);
+ im = $(imageElement);
+ audioPlayer.imageElement = im;
+ $('<div class="playhead"></div>').appendTo(im.parent());
+ $('<div class="buffered"></div>').appendTo(im.parent());
+ $('<div class="seekbar"></div>').appendTo(im.parent());
+ $('<div class="audio-control-play-pause paused">▶</div>').appendTo(im.parent());
+ $('<div class="audio-currentTime">00:00</div>').appendTo(im.parent());
+ };
+})(audioPlayer);
+
+$(document).ready(function () {
+ if (!$('.audio-media').length) {
+ return;
+ }
+
+ console.log('Initializing audio player');
+
+ audioElements = $('.audio-media .audio-player');
+ audioPlayer.init(audioElements[0]);
+ audioPlayer.attachToImage($('.audio-spectrogram img')[0]);
+});
+
diff --git a/mediagoblin/static/js/geolocation-map.js b/mediagoblin/static/js/geolocation-map.js
index a2c62045..de49a37d 100644
--- a/mediagoblin/static/js/geolocation-map.js
+++ b/mediagoblin/static/js/geolocation-map.js
@@ -17,6 +17,11 @@
*/
$(document).ready(function () {
+ if (!$('#tile-map').length) {
+ return;
+ }
+ console.log('Initializing map');
+
var longitude = Number(
$('#tile-map #gps-longitude').val());
var latitude = Number(
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index 1df676ab..517fb646 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -20,7 +20,8 @@ from os.path import splitext
from cgi import FieldStorage
from celery import registry
-import urllib,urllib2
+import urllib
+import urllib2
import logging
_log = logging.getLogger(__name__)
@@ -36,7 +37,7 @@ from mediagoblin.submit import forms as submit_forms
from mediagoblin.processing import mark_entry_failed
from mediagoblin.processing.task import ProcessMedia
from mediagoblin.messages import add_message, SUCCESS
-from mediagoblin.media_types import get_media_type_and_manager, \
+from mediagoblin.media_types import sniff_media, \
InvalidFileType, FileTypeNotSupported
@@ -56,7 +57,11 @@ def submit_start(request):
else:
try:
filename = request.POST['file'].filename
- media_type, media_manager = get_media_type_and_manager(filename)
+
+ # Sniff the submitted media to determine which
+ # media plugin should handle processing
+ media_type, media_manager = sniff_media(
+ request.POST['file'])
# create entry and save in database
entry = request.db.MediaEntry()
@@ -131,9 +136,10 @@ def submit_start(request):
raise
if mg_globals.app_config["push_urls"]:
- feed_url=request.urlgen(
+ feed_url = request.urlgen(
'mediagoblin.user_pages.atom_feed',
- qualified=True,user=request.user.username)
+ qualified=True,
+ user=request.user.username)
hubparameters = {
'hub.mode': 'publish',
'hub.url': feed_url}
@@ -160,10 +166,9 @@ def submit_start(request):
user=request.user.username)
except Exception as e:
'''
- This section is intended to catch exceptions raised in
+ This section is intended to catch exceptions raised in
mediagobling.media_types
'''
-
if isinstance(e, InvalidFileType) or \
isinstance(e, FileTypeNotSupported):
submit_form.file.errors.append(
diff --git a/mediagoblin/templates/mediagoblin/media_displays/audio.html b/mediagoblin/templates/mediagoblin/media_displays/audio.html
new file mode 100644
index 00000000..36bd9d1d
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/media_displays/audio.html
@@ -0,0 +1,61 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+#}
+
+{% extends 'mediagoblin/user_pages/media.html' %}
+
+{% block mediagoblin_head %}
+ {{ super() }}
+ <link rel="stylesheet" type="text/css" href="{{ request.staticdirect('/css/audio.css') }}" />
+ <script type="text/javascript" src="{{ request.staticdirect(
+ '/js/audio.js') }}"></script>
+{% endblock %}
+
+{% block mediagoblin_media %}
+ <div class="audio-media">
+ {% if 'spectrogram' in media.media_files %}
+ <div class="audio-spectrogram">
+ <img src="{{ request.app.public_store.file_url(
+ media.media_files.spectrogram) }}"
+ alt="Spectrogram" />
+ </div>
+ {% endif %}
+ <audio class="audio-player" controls="controls"
+ preload="metadata">
+ <source src="{{ request.app.public_store.file_url(
+ media.media_files.ogg) }}" type="video/webm; encoding=&quot;vorbis&quot;" />
+ <div class="no_html5">
+ {%- trans -%}Sorry, this audio will not work because
+ your web browser does not support HTML5
+ audio.{%- endtrans -%}<br/>
+ {%- trans -%}You can get a modern web browser that
+ can play the audio at <a href="http://getfirefox.com">
+ http://getfirefox.com</a>!{%- endtrans -%}
+ </div>
+ </audio>
+ </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/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index ba80ba20..8bf7d13c 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -17,20 +17,20 @@
import urlparse
import os
import re
-import time
from nose.tools import assert_equal, assert_true, assert_false
from pkg_resources import resource_filename
-from mediagoblin.tests.tools import setup_fresh_app, get_test_app, \
+from mediagoblin.tests.tools import get_test_app, \
fixture_add_user
from mediagoblin import mg_globals
-from mediagoblin.processing import create_pub_filepath
-from mediagoblin.tools import template, common
+from mediagoblin.tools import template
+
def resource(filename):
return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
+
GOOD_JPG = resource('good.jpg')
GOOD_PNG = resource('good.png')
EVIL_FILE = resource('evil')
@@ -44,6 +44,7 @@ BAD_TAG_STRING = 'rage,' + 'f' * 26 + 'u' * 26
FORM_CONTEXT = ['mediagoblin/submit/start.html', 'submit_form']
REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request']
+
class TestSubmission:
def setUp(self):
self.test_app = get_test_app()
@@ -76,7 +77,7 @@ class TestSubmission:
for key in context_keys:
context_data = context_data[key]
return response, context_data
-
+
def upload_data(self, filename):
return {'upload_files': [('file', filename)]}
@@ -102,7 +103,7 @@ class TestSubmission:
response, context = self.do_post({'title': title}, do_follow=True,
**self.upload_data(filename))
self.check_url(response, '/u/{0}/'.format(self.test_user.username))
- assert_true(context.has_key('mediagoblin/user_pages/user.html'))
+ assert_true('mediagoblin/user_pages/user.html' in context)
# Make sure the media view is at least reachable, logged in...
url = '/u/{0}/m/{1}/'.format(self.test_user.username,
title.lower().replace(' ', '-'))
@@ -190,8 +191,30 @@ class TestSubmission:
r'^Could not extract any file extension from ".*?"$',
str(form.file.errors[0])))
+ def test_sniffing(self):
+ '''
+ Test sniffing mechanism to assert that regular uploads work as intended
+ '''
+ template.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'
+ }, upload_files=[(
+ 'file', GOOD_JPG)])
+
+ response.follow()
+
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/user_pages/user.html']
+
+ request = context['request']
+
+ media = request.db.MediaEntry.find_one({
+ u'title': u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'})
+
+ assert media.media_type == 'mediagoblin.media_types.image'
+
def check_false_image(self, title, filename):
- # NOTE: These images should ultimately fail, but they
+ # NOTE: The following 2 tests will ultimately fail, but they
# *will* pass the initial form submission step. Instead,
# they'll be caught as failures during the processing step.
response, context = self.do_post({'title': title}, do_follow=True,