diff --git a/client/options.js b/client/options.js
index b61c4f3..c53bcb3 100644
--- a/client/options.js
+++ b/client/options.js
@@ -316,7 +316,7 @@ function toggle_expansion(img, event) {
if (event.metaKey)
return;
event.preventDefault();
- var expand = !img.data('thumbSrc');
+ var expand = !img.data('thumbSrc');
img.closest('article').toggleClass('expanded', expand);
var $imgs = img;
if (THREAD && (event.altKey || event.shiftKey)) {
@@ -332,7 +332,7 @@ function toggle_expansion(img, event) {
with_dom(function () {
$imgs.each(function () {
var $img = $(this);
- if (expand)
+ if (expand)
expand_image($img);
else {
contract_image($img, event);
@@ -344,24 +344,30 @@ function toggle_expansion(img, event) {
function contract_image($img, event) {
var thumb = $img.data('thumbSrc');
- if (!thumb)
- return;
- // try to keep the thumbnail in-window for large images
- var h = $img.height();
- var th = parseInt($img.data('thumbHeight'), 10);
- if (event) {
- var y = $img.offset().top, t = $(window).scrollTop();
- if (y < t && th < h)
- window.scrollBy(0, Math.max(th - h,
- y - t - event.clientY + th/2));
+ var audio = $($img).parent().siblings("audio");
+ if (audio.length > 0) {
+ $img.data("thumbSrc", null);
+ audio.remove();
+ }
+ else {
+ if (!thumb)
+ return;
+ // try to keep the thumbnail in-window for large images
+ var h = $img.height();
+ var th = parseInt($img.data('thumbHeight'), 10);
+ if (event) {
+ var y = $img.offset().top, t = $(window).scrollTop();
+ if (y < t && th < h)
+ window.scrollBy(0, Math.max(th - h,
+ y - t - event.clientY + th/2));
+ }
+ if (fullWidthExpansion)
+ contract_full_width(parent_post($img));
+ $img.replaceWith($(' ')
+ .width($img.data('thumbWidth')).height(th)
+ .attr('src', thumb));
}
- if (fullWidthExpansion)
- contract_full_width(parent_post($img));
- $img.replaceWith($(' ')
- .width($img.data('thumbWidth')).height(th)
- .attr('src', thumb));
}
-
function expand_image($img) {
if ($img.data('thumbSrc'))
return;
@@ -372,36 +378,49 @@ function expand_image($img) {
var cap = a.siblings('figcaption').text();
var dims = cap.match(/(\d+)x(\d+)/);
var video = /^Video/.test(cap);
- if (!dims)
- return;
- var tw = $img.width(), th = $img.height();
- var w = parseInt(dims[1], 10), h = parseInt(dims[2], 10);
- // if this is a high-density screen, reduce image size appropriately
- var r = window.devicePixelRatio;
- if (!options.get('nohighres') && !video && r && r > 1) {
- var min = 1000;
- if ((w > min || h > min) && w/r > tw && h/r > th) {
- w /= r;
- h /= r;
+ var audio = /^Audio/.test(cap);
+ if (dims) {
+ var tw = $img.width(), th = $img.height();
+ var w = parseInt(dims[1], 10), h = parseInt(dims[2], 10);
+ // if this is a high-density screen, reduce image size appropriately
+ var r = window.devicePixelRatio;
+ if (!options.get('nohighres') && !video && r && r > 1) {
+ var min = 1000;
+ if ((w > min || h > min) && w/r > tw && h/r > th) {
+ w /= r;
+ h /= r;
+ }
}
- }
-
- $img.remove();
- $img = $(video ? '' : ' ', {
- src: href,
- width: w, height: h,
- data: {
- thumbWidth: tw, thumbHeight: th,
- thumbSrc: $img.attr('src'),
- },
- prop: video ? {autoplay: true, loop: true} : {},
- }).appendTo(a);
-
- var fit = options.get('inlinefit');
- if (fit != 'none') {
- var both = fit == 'both';
- fit_to_window($img, w, h, both || fit == 'width',
- both || fit == 'height');
+ $img.remove();
+ $img = $(video ? '' : ' ', {
+ src: href,
+ width: w, height: h,
+ data: {
+ thumbWidth: tw, thumbHeight: th,
+ thumbSrc: $img.attr('src'),
+ },
+ prop: video ? {autoplay: true, loop: true} : {},
+ }).appendTo(a);
+ var fit = options.get('inlinefit');
+ if (fit != 'none') {
+ var both = fit == 'both';
+ fit_to_window($img, w, h, both || fit == 'width',
+ both || fit == 'height');
+ }
+ } else if (audio) {
+ //$fig = this.$el.children('figure');
+ $aud = $(' ', {
+ src: href,
+ width: 300,
+ height: '3em',
+ autoplay: true,
+ loop: false,
+ controls: true
+ }).insertAfter(a);
+ $img.data("thumbSrc", $aud);
+ //if (vol)
+ // $fig.find('audio')['0'].volume = vol;
+ //this.model.set('imageExpanded', true);
}
}
diff --git a/common.js b/common.js
index 575b6ed..f4152e4 100644
--- a/common.js
+++ b/common.js
@@ -797,26 +797,34 @@ OS.gazou = function (info, toppu) {
}
else {
src = encodeURI(this.image_paths().src + info.src);
- video = info.video || (/\.webm$/i.test(src) && 'webm'); // webm check is legacy
- caption = [video ? 'Video ' : 'Image ', new_tab_link(src, info.src)];
+ caption = [
+ (/\.(mp3|ogg|wav|flac)/.test(info.src)?'Audio':
+ (this.sauceToggle?image_sauce_id(info.mid):
+ (/\.(webm|mp4)/.test(info.src)?'Video':'Image'))), ' ',
+ new_tab_link(src, (this.thumbStyle == 'hide') ? '[Show]' : info.src, 'imageSrc')
+ ];
}
var img = this.gazou_img(info, toppu);
var dims = info.dims[0] + 'x' + info.dims[1];
- return [safe(''),
- caption, safe(' ('),
- info.audio ? (audioIndicator + ', ') : '',
- info.duration ? (info.duration + ', ') : '',
- readable_filesize(info.size), ', ',
- dims, (info.apng ? ', APNG' : ''),
- this.full ? [', ', chibi(info.imgnm, img.src)] : '',
- safe(') '),
- this.thumbStyle == 'hide' ? '' : img.html,
- safe(' \n\t')];
+ return [safe(''),
+ caption, safe(' ('),
+ (this.spoilToggle && info.spoiler ? 'Spoiler, ' : ''),
+ info.audio ? (audioIndicator + ', ') : '',
+ (info.songTitle || info.artist) ?
+ [safe(''), soundInformation, safe(' '), ', '] : '',
+ info.duration ? (info.duration + ', ') : '',
+ readable_filesize(info.size),
+ /\.(mp3|ogg|wav|flac)/.test(info.src) ? '' : [', ', dims],
+ (info.apng ? ', APNG' : ''),
+ this.full ? [', ', chibi(info.imgnm, img.src)] : '',
+ safe(') '),
+ this.thumbStyle == 'hide' ? '' : img.html,
+ safe(' \n\t')];
};
exports.thumbStyles = ['small', 'sharp', 'large', 'hide'];
diff --git a/imager/config.js.example b/imager/config.js.example
index 74b52cd..127dbd5 100644
--- a/imager/config.js.example
+++ b/imager/config.js.example
@@ -36,6 +36,7 @@ module.exports = {
VIDEO_EXTS: ['.webm', '.mp4'],
// allow audio streams
AUDIO: false,
+ AUDIO_EXTS: ['.mp3', '.flac', '.ogg', '.wav'],
// uncomment this to have all audio uploads overlaid with
// the corresponding spoiler image
/*
diff --git a/imager/daemon.js b/imager/daemon.js
index e012b6f..008879d 100644
--- a/imager/daemon.js
+++ b/imager/daemon.js
@@ -177,15 +177,18 @@ IU.process = function () {
if (image.ext == '.mov')
image.ext = '.mp4';
if (IMAGE_EXTS.indexOf(image.ext) < 0
- && (!config.VIDEO || config.VIDEO_EXTS.indexOf(image.ext) < 0))
+ && (!config.VIDEO || config.VIDEO_EXTS.indexOf(image.ext) < 0)
+ && (!config.VIDEO || config.AUDIO_EXTS.indexOf(image.ext) < 0))
return this.failure(Muggle('Invalid image format.'));
image.imgnm = filename.substr(0, 256);
this.status('Verifying...');
if (config.VIDEO_EXTS.indexOf(image.ext) >= 0)
- video_still(image.path, image.ext, this.verify_video.bind(this));
+ video_still(image.path, image.ext, this.verify_video.bind(this));
+ else if (config.AUDIO_EXTS.indexOf(image.ext) >= 0)
+ audio_still(image.path, this.verify_audio.bind(this));
else if (image.ext == '.jpg' && jpegtranBin && jheadBin)
- jobs.schedule(new AutoRotateJob(image.path), this.verify_image.bind(this));
+ jobs.schedule(new AutoRotateJob(image.path), this.verify_image.bind(this));
else
this.verify_image();
};
@@ -210,6 +213,166 @@ AutoRotateJob.prototype.perform_job = function () {
});
};
+
+function AudioStillJob(src) {
+ jobs.Job.call(this);
+ this.src = src;
+}
+util.inherits(AudioStillJob, jobs.Job);
+
+AudioStillJob.prototype.describe_job = function () {
+ return "FFmpeg audio still of " + this.src;
+};
+
+
+AudioStillJob.prototype.perform_job = function () {
+ var self = this;
+ self.get_info();
+}
+
+AudioStillJob.prototype.get_info = function () {
+ var self = this;
+ var audioData = {};
+ child_process.execFile(ffprobeBin, [this.src],
+ function(err, stdout, stderr){
+ var type = stderr.match(/Input #0, (.*),/);
+ if (type)
+ audioData.type = type[1];
+ var title = stderr.match(/title\s+: (.*)/i);
+ if (title)
+ audioData.title = title[1];
+ var artist = stderr.match(/artist\s+: (.*)/i);
+ if (artist)
+ audioData.artist = artist[1];
+ var l = stderr.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})/);
+ if (l){
+ var h = (l[1] != '00' ? l[1] + 'h' : '');
+ var m = (l[2] != '00' ? l[2] + 'm' : '');
+ var s = (l[3] != '00' ? l[3] + 's' : '');
+ audioData.total = parseFloat(parseFloat(l[1])*3600 +
+ parseFloat(l[2])*60 + parseFloat(l[3]) + '.' + parseFloat(l[4]));
+ audioData.length = h + m + s;
+ }
+ self.short_test(audioData);
+ });
+}
+
+AudioStillJob.prototype.short_test = function (audioData) {
+ var self = this;
+ var dest = index.media_path('tmp', 'still_'+etc.random_id());
+ if (audioData.total <= 10) {
+ if (!config.AUDIOFILE_IMAGE) {
+ var msg = "Failure serverside.";
+ var prob = "Missing AUDIOFILE_IMAGE.";
+ winston.warn(prob);
+ fs.unlink(dest, function (err) {
+ self.finish_job(Muggle(msg, prob));
+ });
+ return;
+ }
+ fs.readFile(config.AUDIOFILE_IMAGE, function (errRead, img) {
+ if (errRead) {
+ var msg = "Failure serverside.";
+ var prob = "Error reading AUDIOFILE_IMAGE";
+ winston.warn(prob);
+ fs.unlink(dest, function (err) {
+ self.finish_job(Muggle(msg, prob));
+ });
+ return;
+ }
+ fs.writeFile(dest, img, function (errWrite) {
+ if (errWrite) {
+ var msg = "Failure serverside.";
+ var prob = "Error copying AUDIOFILE_IMAGE";
+ winston.warn(prob);
+ fs.unlink(dest, function (err) {
+ self.finish_job(Muggle(msg, prob));
+ });
+ return;
+ }
+ self.finish_job(null, {
+ still_path: dest,
+ duration: audioData.length,
+ audiotype: audioData.type,
+ title: audioData.title,
+ artist: audioData.artist,
+ });
+ });
+ });
+ } else {
+ self.encode_thumb(audioData, dest);
+ }
+}
+
+AudioStillJob.prototype.encode_thumb = function (audioData, dest) {
+ var self = this;
+ var args = ['-hide_banner', '-loglevel', 'info',
+ '-f', 'lavfi', '-ss', (Math.floor(audioData.total/2) <= 10 ?
+ Math.floor(audioData.total - audioData.total/10) : Math.floor(audioData.total/2)),
+ '-i', 'amovie=' + this.src + ', asplit [a][out1];[a] showspectrum=mode=separate:color=intensity:slide=1:scale=cbrt [out0]',
+ '-f', 'image2', '-vframes', '1', '-vcodec', 'png',
+ '-y', dest];
+ var opts = {env: {AV_LOG_FORCE_NOCOLOR: '1'}};
+ child_process.execFile(ffmpegBin, args, opts,
+ function (err, stdout, stderr) {
+ var lines = stderr ? stderr.split('\n') : [];
+ var first = lines[0];
+ if (err) {
+ var msg;
+ if (/no such file or directory/i.test(first))
+ msg = "Audio went missing.";
+ else if (/invalid data found when/i.test(first))
+ msg = "Invalid audio file.";
+ else if (/^ffmpeg version/i.test(first))
+ msg = "Server's ffmpeg is too old.";
+ else {
+ msg = "Unknown audio reading error.";
+ winston.warn("Unknown ffmpeg output: "+first);
+ }
+ fs.unlink(dest, function (err) {
+ self.finish_job(Muggle(msg, stderr));
+ });
+ return;
+ }
+ self.finish_job(null, {
+ still_path: dest,
+ duration: audioData.length,
+ audiotype: audioData.type,
+ title: audioData.title,
+ artist: audioData.artist,
+ });
+ });
+};
+
+function audio_still(src, cb) {
+ jobs.schedule(new AudioStillJob(src), cb);
+}
+
+IU.verify_audio = function (err, info) {
+ if (err)
+ return this.failure(err);
+ var self = this;
+ this.db.track_temporary(info.still_path, function (err) {
+ if (err)
+ winston.warn("Tracking error: " + err);
+ // pretend it's a PNG for the next steps
+ var image = self.image;
+ image.video_path = image.path;
+ image.path = info.still_path;
+ image.ext = '.png';
+ if (info.duration)
+ image.duration = info.duration;
+ if (info.audiotype)
+ image.audiofile = info.audiotype;
+ if (info.title)
+ image.title = info.title;
+ if (info.artist)
+ image.artist = info.artist;
+ self.verify_image();
+ });
+};
+
+
function StillJob(src, ext) {
jobs.Job.call(this);
this.src = src;
@@ -305,6 +468,10 @@ function video_still(src, ext, cb) {
jobs.schedule(new StillJob(src, ext), cb);
}
+function audio_thumb(src, ext, cb) {
+ jobs.schedule(new AudioJob(src, ext), cb);
+}
+
IU.verify_video = function (err, info) {
if (err)
return this.failure(err);
@@ -474,8 +641,8 @@ IU.got_nails = function () {
var image = this.image;
if (image.video_path) {
// stop pretending this is just a still image
- image.path = image.video_path;
- image.ext = '.' + image.video;
+ image.path = image.video_path;
+ image.ext = image.audiofile ? '.'+image.audiofile : '.'+image.video;
delete image.video_path;
}
@@ -532,9 +699,10 @@ var identifyBin, convertBin;
which('identify', function (err, bin) { if (err) throw err; identifyBin = bin; });
which('convert', function (err, bin) { if (err) throw err; convertBin = bin; });
-var ffmpegBin;
+var ffmpegBin, ffprobeBin;
if (config.VIDEO) {
which('ffmpeg', function (err, bin) { if (err) throw err; ffmpegBin = bin; });
+ which('ffprobe', function (err, bin) { if (err) throw err; ffprobeBin = bin; });
}
/* optional JPEG auto-rotation */
diff --git a/imager/index.js b/imager/index.js
index 4e63921..3831796 100644
--- a/imager/index.js
+++ b/imager/index.js
@@ -12,7 +12,7 @@ exports.Onegai = db.Onegai;
exports.config = config;
var image_attrs = ('src thumb dims size MD5 hash imgnm spoiler realthumb vint'
- + ' apng mid audio video duration').split(' ');
+ + ' apng mid audio video duration audiofile').split(' ');
exports.image_attrs = image_attrs;
exports.send_dead_image = function (kind, filename, resp) {