soup.io: EmbedFix

Repairs embeds in your own and your friend soup. Fixes embeds everywhere if there is a link in the description

// ==UserScript==
// @name soup.io: EmbedFix
// @namespace    http://xcvbnm.org/
// @author Nordern
// @description Repairs embeds in your own and your friend soup. Fixes embeds everywhere if there is a link in the description
// @version 0.2.1
// @match http://*.soup.io/*
// @match https://*.soup.io/*
// @exclude     http://www.soup.io/frames/*
// @exclude     http://www.soup.io/remote/*
// @license public domain, MediaEmbed has MIT Licence
// @run-at document-end
// ==/UserScript==
// Available on github under: https://github.com/edave64/souplements/blob/master/youtube-fix/
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
 * This code snippet allows you to intercept the loading of new elements and modify the loaded posts.
 * The basic idea is to allow filtering out posts before they are inserted into the dom, and thus before their assets
 * are loaded. This reduces stress on both the client and the asset servers.
 *
 * To use the filter, register the event "processBatch" in SOUP.Endless.
 * Example:
 *
 *     SOUP.Endless.on("processBatch", function (doc) {
 *         // your code here
 *     });
 *
 * The doc argument represents a temporary HTMLDocument node, storing the new loaded posts. You can work with it
 * like you can work with ''document''.
 *
 * Be careful to never remove all posts, otherwise Soup will assume you reached the end.
 *
 * Please keep in mind that soup already has some filters build in: http://faq.soup.io/post/4328678
 * These are probably easier on the servers.
 *
 * Licence: Public domain
 *
 * UPDATE 1.1:
 * The soup event-api was used! I just used it wrong. It is supposed to be a template. The event is now fired on
 * SOUP.Endless instead of SOUP.Events
 *
 * UPDATE 1.2:
 * Also trigger for loaded in reactions
 */
(function () {
    // Add events to SOUP.Endless
    if (!SOUP.Endless.trigger) {
        SOUP.tools.extend(SOUP.Endless, SOUP.Events);
    }

    if (Ajax.Request._EndlessFilter) return;

    var oldRequest = Ajax.Request;

    function getLoadAboveURL() {
        var url = $("endless_top_post").href;
        return url.match(/[&?]newer=1/) ? url : url + (url.indexOf("?") >= 0 ? "&" : "?") + "newer=1";
    }

    function getLoadBelowURL() {
        return SOUP.Endless.next_url.replace(/&?newer=1&?/g, "");
    }

    function catchBatchLoad (path, options) {
        var oldSuccess = options.onSuccess;
        options.onSuccess = function (response) {
            var text = response.responseText,
                pipePosition = text.indexOf("|"),
                nextPath = text.substring(0, pipePosition),
                content = text.substring(pipePosition + 1),
                parser = new DOMParser(),
                xmlDoc = parser.parseFromString(content, "text/html"),
                root = xmlDoc.body;

            root.setAttribute("id", "posts");
            SOUP.Endless.trigger("processBatch", xmlDoc);

            response.responseText = nextPath + "|" + root.innerHTML;

            return oldSuccess.apply(this, arguments);
        };

        return oldRequest.apply(this, arguments);
    }

    function catchPreviewLoad (path, options) {
        var oldSuccess = options.onSuccess;
        options.onSuccess = function (response) {
            var content = response.responseText,
                parser = new DOMParser(),
                xmlDoc = parser.parseFromString(content, "text/html"),
                root = xmlDoc.body;

            root.setAttribute("id", "posts");
            SOUP.Endless.trigger("processBatch", xmlDoc);

            response.responseText = root.innerHTML;

            return oldSuccess.apply(this, arguments);
        };

        return oldRequest.apply(this, arguments);
    }

    Ajax.Request = function (path, options) {
        var aboveURL = getLoadAboveURL();
        var belowURL = getLoadBelowURL();

        if (path === aboveURL || path === belowURL) {
            return catchBatchLoad.apply(this, arguments);
        }
        if (path.startsWith("http://" + document.location.host + "/preview/")) {
            return catchPreviewLoad.apply(this, arguments);
        }
        return oldRequest.apply(this, arguments);
    };
    Ajax.Request._EndlessFilter = true;
    Ajax.Request.Events = oldRequest.Events;
    Ajax.Request.prototype = oldRequest.prototype;
}());

},{}],2:[function(require,module,exports){
require("../endlessFilter");
const MediaEmbedder = require("media-embedder");

if (!SOUP.EmbedFix) {
    SOUP.EmbedFix = true;

    function fixAll (doc) {
        var video_posts = [].slice.call(doc.getElementsByClassName("post_video"));
        
        const firstPost = document.querySelector(".post .content");
        let width = 500;

        if (firstPost) {
            width = parseInt(window.getComputedStyle(firstPost).width);
        }

        const height = (width / 16 * 9)|0;
        
        for (const video_post of video_posts) {
            const embed = video_post.getElementsByClassName("embed")[0];
            if (embed.children.length === 0) {
                let mediaData;

                const textarea = video_post.querySelector("[name='post[embedcode_or_url]']");
                if (textarea) {
                    mediaData = MediaEmbedder.detect(textarea.childNodes[0] ? textarea.childNodes[0].nodeValue : "");
                }

                if (!mediaData) {
                    const description = video_post.getElementsByClassName("body")[0];
                    if (description) {
                        mediaData = MediaEmbedder.detect(description.innerHTML);
                    }
                }

                if (mediaData) {
                    mediaData.width = width;
                    mediaData.height = height;
                    embed.innerHTML = MediaEmbedder.buildIframe(mediaData);
                }
            }
        }
    }

    SOUP.Endless.on("processBatch", function (doc) {
        fixAll(doc);
    });

    fixAll(document);
}

},{"../endlessFilter":1,"media-embedder":4}],3:[function(require,module,exports){
module.exports = {
    parse: function (text) {
        const a = document.createElement("a");
        a.href = text;
        a.query = a.search.substring(1);
        return a;
    }
};

},{}],4:[function(require,module,exports){
(function (global){
"use strict";

/**
 * @typedef MediaInfo
 * @name MediaInfo
 * @type {object}
 * @property {string} platform - The name of the media platfrom.
 * @property {string} mediaid - A string uniquely identifying on piece of media on the platform
 * @property {number} [height] - The height of the embeded player
 * @property {number} [width] - The width of the embeded player
 * @property {boolean} [allowFullscreen] - True if the player can enter fullscreen
 * @property {boolean} [loop] - True if the player will start over at the end
 * @property {number} [timestamp] - The number of seconds at the begining that will be skiped
 */

/**
 * @typedef MediaPlatform
 * @name MediaPlatform
 */

/**
 * Parses a text can either be a url to a video on a platform, or an html
 * snipplet containing an embedding code for one of these platforms.
 * 
 * It attemps to extract as much information as possible from this text.
 * 
 * @method MediaPlatform~detect
 * @param {string} test - A URL or an html snipplet containing an embed code
 * @returns {MediaInfo|undefined} Information found in the string, undefined if none where found.
 */

/**
 * Generates a iframe embed html snipplet from a descriptor
 * 
 * @method MediaPlatform~buildIframe
 * @param {MediaInfo} descriptor
 * @returns {string} An html snipplet
 */

/**
 * Generates a link url from a descriptor
 * 
 * @method MediaPlatform~buildLink
 * @param {MediaInfo} descriptor
 * @returns {string} A url
 */

/** @type {Object.<string, MediaPlatform>} */
const platforms = {
    youtube: require("./platforms/youtube"),
    dailymotion: require("./platforms/dailymotion"),
    vimeo: require("./platforms/vimeo")
};

/** @type {MediaPlatform} */
global.test = module.exports = {
    detect: function(text) {
        for (const platform in platforms) {
            const ret = platforms[platform].detect(text);
            if (ret) {
                ret.platform = platform;
                return ret;
            }
        }
    },
    buildIframe: function (descriptor) {
        const platform = platforms[descriptor.platform];
        return platform.buildIframe(descriptor);
    },
    buildLink: function (descriptor) {
        const platform = platforms[descriptor.platform];
        return platform.buildLink(descriptor);
    }
};

}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./platforms/dailymotion":6,"./platforms/vimeo":7,"./platforms/youtube":8}],5:[function(require,module,exports){
"use strict";

const querystring = require("querystring");
const UrlHelper = require("./utils/url_helper");
const DocHelper = require("./utils/doc_helper");

/**
 * @callback processor
 * @param {UrlObject} url
 * @param {Object} queryObject
 * @return {MediaInfo}
 */

/**
 * @callback generator
 * @param {MediaInfo} mediaInfo
 * @param {boolean} embedded
 * @param {string[]} queryParts
 * @returns {string} Url
 */

/**
 * @param {processor} urlProcessor
 * @param {generator} urlGenerator
 * @returns {MediaPlatform}
 */
module.exports = function (urlProcessor, urlGenerator) {
    function wrappedProcessor (text) {
        try {
            const urlData = UrlHelper.parse(text);
            const qs = querystring.parse(urlData.query);

            return urlProcessor(urlData, qs);
        } catch (e) {
            console.error(e);
        }
    }

    function wrappedGenerator (data, embed) {
        const queryData = [];
        queryData.when = function (condition, val) { if (condition) this.push(val); };
        let url = urlGenerator(data, embed, queryData);
        
        if (queryData.length > 0) {
            url += "?" + queryData.join("&");
        }
        return url;
    }
    
    return {
        detect: (text) => {
            // is entire text a url?
            let entire = wrappedProcessor(text),
                ret;
            if (entire) {
                return entire;
            }
            
            for (const tag of DocHelper.get_nodes(text, "iframe", "embed", "a")) {
                switch (tag.nodeName.toLowerCase()) {
                    case "iframe":
                    case "embed":
                        ret = DocHelper.processIframe(tag, wrappedProcessor);
                        if (ret) {
                            return ret;
                        }
                        break;
                    case "a":
                        ret = wrappedProcessor(tag.getAttribute("href"));
                        if (ret) {
                            return ret;
                        }
                        break;
                }
            }
        },
        buildIframe: (data) => {
            return DocHelper.buildIframe(wrappedGenerator(data, true), data.height, data.width, data.allowFullscreen);
        },
        buildLink: (data) => {
            return wrappedGenerator(data, false);
        }
    };
};

},{"./utils/doc_helper":9,"./utils/url_helper":10,"querystring":13}],6:[function(require,module,exports){
"use strict";
const urlParse = /^\/(embed\/video|video|swf)\/([0-9a-zA-Z]*)/

module.exports = require("../platform_base")(
    function (urlData, qs) {
        if (urlData.normalizedHost === "dailymotion.com") {
            const urlMatch = urlData.pathname.match(urlParse);
            if (urlMatch) {
                return {
                    mediaid: urlMatch[2],
                    height: null,
                    width: null,
                    allowFullscreen: null,
                    loop: null,
                    timestamp: qs.start || null,
                    autoplay: qs.autoplay === "1" || qs.autoPlay === "1"
                }
            }
        }
    },
    function (data, embed, query) {
        let url = embed ? "https://www.dailymotion.com/embed/video/" : "https://www.dailymotion.com/video/";
        url += data.mediaid.replace(/[^0-9a-zA-Z]/g, ""); // sanitize mediaid
    
        query.when(data.allowFullscreen === false, "fullscreen=1");
        query.when(data.autoplay                 , "autoplay=1");
        query.when(data.timestamp                , "start=" + parseInt(data.timestamp));
        
        return url;
    }
);

},{"../platform_base":5}],7:[function(require,module,exports){
"use strict";
const urlParse = /^\/(video\/)?([0-9]*)$/;

/**
 * @param {string} videoId 
 * @param {object} param
 * @returns {MediaInfo}
 */
function generate (videoId, param) {
    return {
        mediaid: videoId,
        height: null, width: null, timestamp: null,
        allowFullscreen: null,
        loop: param.loop === "1",
        autoplay: param.autoplay === "1"
    }
}

module.exports = require("../platform_base")(
    function (urlData, qs) {
        if (urlData.normalizedHost === "vimeo.com" || urlData.normalizedHost === "player.vimeo.com") {
            if (urlData.pathname === "/moogaloop.swf") {
                return generate(qs.clip_id, qs);
            }
            const urlMatch = urlData.pathname.match(urlParse);
            if (urlMatch) {
                return generate(urlMatch[2], qs);
            }
        }
    },
    function (data, embed, query) {
        let url = embed ? "https://player.vimeo.com/video/" : "https://vimeo.com/";
        url += data.mediaid.replace(/[^0-9]/g, ""); // sanitize mediaid
    
        query.when(data.loop,     "loop=1");
        query.when(data.autoplay, "autoplay=1");

        return url;
    }
);

},{"../platform_base":5}],8:[function(require,module,exports){
"use strict";
const querystring = require("querystring");
const embedUrlParse = /^\/(embed|v)\/([0-9a-zA-Z\-_]*)(.*)/;

/**
 * @param {string} videoId
 * @param {object} param
 * @returns {MediaInfo}
 */
function generate (videoId, param) {
    return {
        mediaid: videoId,
        height: null, width: null,
        allowFullscreen: param.fs !== "0",
        timestamp: param.t || param.time || param.start || null,
        loop: param.loop === "1",
        autoplay: param.autoplay === "1"
    }
}

module.exports = require("../platform_base")(
    function (urlData, qs) {
        if (urlData.normalizedHost === "youtube.com") {
            if (qs.v) {
                return generate(qs.v, qs);
            }
            const embedUrlMatch = urlData.pathname.match(embedUrlParse);
            if (embedUrlMatch) {
                // youtube /v/ urls can be kind of odd and and append the query with & to the path
                const vUrlQs = querystring.parse(embedUrlMatch[3]);
                return generate(embedUrlMatch[2], Object.assign({}, qs, vUrlQs));
            }
        } else if (urlData.normalizedHost === "youtu.be") {
            const qs = querystring.parse(urlData.query);
            return generate(urlData.pathname.slice(1), qs);
        }
    },
    function (data, embed, query) {
        let url = embed ? "https://www.youtube.com/embed/" : "https://www.youtube.com/watch";
        const mediaid = data.mediaid.replace(/[^0-9a-zA-Z\-_]/g, ""); // sanitize mediaid
    
        query.when(data.allowFullscreen === false, "fs=1");
        query.when(data.loop,                      "loop=1");
        query.when(data.autoplay,                  "autoplay=1");
        if (data.timestamp) {
            query.push("start=" + data.timestamp.replace(/[^0-9hms]/g, ""));
        }

        if (embed) {
            url += mediaid;
        } else {
            query.push("v=" + mediaid)
        }

        return url;
    }
);

},{"../platform_base":5,"querystring":13}],9:[function(require,module,exports){
const DOMParser = (window.window).DOMParser;

module.exports = {
    get_nodes: function (text, ...node_types) {
        let parser = (new DOMParser ()).parseFromString("<html><body>" + text + "</body></html>", "text/html");
        var tags = [];
        for (const node_type of node_types) {
            tags.push.apply(tags, parser.getElementsByTagName(node_type));
        }
        return tags;
    },

    processIframe: function (iframe, urlProcessor) {
        const ret = urlProcessor(iframe.getAttribute("src"));
        if (ret) {
            ret.allowFullscreen = iframe.getAttribute("allowfullscreen") != null;
            ret.height = iframe.getAttribute("height");
            ret.width = iframe.getAttribute("width");
            return ret;
        }
    },

    buildIframe: function (src, height, width, allowFullscreen) {
        let ret = "<iframe "
        if (height != null) {
            ret += 'height="' + parseInt(height) + '" '
        }
        if (width != null) {
            ret += 'width="' + parseInt(width) + '" '
        }
        if (allowFullscreen === true) {
            ret += 'allowfullscreen webkitallowfullscreen mozallowfullscreen '
        }
        ret += 'frameborder="0" src="' + src + '"></iframe>'
        return ret;
    }
};

},{}],10:[function(require,module,exports){
const url = require("url");
const querystring = require("querystring");

module.exports = {
    parse: function (text) {
        let fullUrl = url.parse(text);
    
        if (fullUrl.protocol === null) {
            fullUrl = url.parse("test:" + (text.startsWith("//") ? "" : "//") + text);
        }

        if (fullUrl.host) {
            fullUrl.normalizedHost = fullUrl.host.startsWith("www.") ? fullUrl.host.substring(4) : fullUrl.host;
        }

        return fullUrl;
    }
};

},{"querystring":13,"url":3}],11:[function(require,module,exports){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

// If obj.hasOwnProperty has been overridden, then calling
// obj.hasOwnProperty(prop) will break.
// See: https://github.com/joyent/node/issues/1707
function hasOwnProperty(obj, prop) {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

module.exports = function(qs, sep, eq, options) {
  sep = sep || '&';
  eq = eq || '=';
  var obj = {};

  if (typeof qs !== 'string' || qs.length === 0) {
    return obj;
  }

  var regexp = /\+/g;
  qs = qs.split(sep);

  var maxKeys = 1000;
  if (options && typeof options.maxKeys === 'number') {
    maxKeys = options.maxKeys;
  }

  var len = qs.length;
  // maxKeys <= 0 means that we should not limit keys count
  if (maxKeys > 0 && len > maxKeys) {
    len = maxKeys;
  }

  for (var i = 0; i < len; ++i) {
    var x = qs[i].replace(regexp, '%20'),
        idx = x.indexOf(eq),
        kstr, vstr, k, v;

    if (idx >= 0) {
      kstr = x.substr(0, idx);
      vstr = x.substr(idx + 1);
    } else {
      kstr = x;
      vstr = '';
    }

    k = decodeURIComponent(kstr);
    v = decodeURIComponent(vstr);

    if (!hasOwnProperty(obj, k)) {
      obj[k] = v;
    } else if (isArray(obj[k])) {
      obj[k].push(v);
    } else {
      obj[k] = [obj[k], v];
    }
  }

  return obj;
};

var isArray = Array.isArray || function (xs) {
  return Object.prototype.toString.call(xs) === '[object Array]';
};

},{}],12:[function(require,module,exports){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

var stringifyPrimitive = function(v) {
  switch (typeof v) {
    case 'string':
      return v;

    case 'boolean':
      return v ? 'true' : 'false';

    case 'number':
      return isFinite(v) ? v : '';

    default:
      return '';
  }
};

module.exports = function(obj, sep, eq, name) {
  sep = sep || '&';
  eq = eq || '=';
  if (obj === null) {
    obj = undefined;
  }

  if (typeof obj === 'object') {
    return map(objectKeys(obj), function(k) {
      var ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
      if (isArray(obj[k])) {
        return map(obj[k], function(v) {
          return ks + encodeURIComponent(stringifyPrimitive(v));
        }).join(sep);
      } else {
        return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
      }
    }).join(sep);

  }

  if (!name) return '';
  return encodeURIComponent(stringifyPrimitive(name)) + eq +
         encodeURIComponent(stringifyPrimitive(obj));
};

var isArray = Array.isArray || function (xs) {
  return Object.prototype.toString.call(xs) === '[object Array]';
};

function map (xs, f) {
  if (xs.map) return xs.map(f);
  var res = [];
  for (var i = 0; i < xs.length; i++) {
    res.push(f(xs[i], i));
  }
  return res;
}

var objectKeys = Object.keys || function (obj) {
  var res = [];
  for (var key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key);
  }
  return res;
};

},{}],13:[function(require,module,exports){
'use strict';

exports.decode = exports.parse = require('./decode');
exports.encode = exports.stringify = require('./encode');

},{"./decode":11,"./encode":12}]},{},[2]);