soup.io: EmbedFix

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

目前为 2017-10-23 提交的版本,查看 最新版本

// ==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
// @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) {
                    const textarea = video_post.querySelector("[name='post[embedcode_or_url]']");
                    // Turns out: Edge doesn't support innerText on DomParser elements. or something
                    let 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]);