Cinny GIF Embedder

embeds tenor and other gif url's into media players like discord

// ==UserScript==
// @name         Cinny GIF Embedder
// @namespace    org.jerryjhird.cinnygifs
// @description  embeds tenor and other gif url's into media players like discord
// @version      1.1
// @match        https://app.cinny.in/*
// @match        https://cinny.*/*
// @match        https://*.cinny.*/*
// @match        https://*.matrix.org/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @license      GPL3
// @run-at       document-idle
// ==/UserScript==

// WARNING WILL BE BUGGED WITH "Url Preview" ENABLED IN YOUR ACCOUNT/GENERAL SETTINGS TURN OFF FOR THE BEST EXPERIENCE

(function(){
'use strict';

const log = (...a)=>console.log("CinnyGIF:",...a);

const decodeHref = h=>{
    try {
        if(!/^https?:\/\//i.test(h)) h="https://"+h;
        return decodeURIComponent(h);
    } catch { return h; }
};

const extractMedia = t=>{
    if(!t) return null;
    let d=t.match(/https?:\/\/[^"'<> ]+\.(?:mp4|gif|webm)(?:\?[^\s"'<>]*)?/i);
    if(d) return d[0];
    let m=t.match(/https?:\/\/[^"'<> ]+(?:media|i|thumb|media0)\.[^"'<> ]+\/[^"'<> ]+\.(?:mp4|gif|webm)(?:\?[^\s"'<>]*)?/i);
    return m?m[0]:null;
};

const httpGet = url => new Promise((res, rej)=>GM_xmlhttpRequest({method:"GET",url,responseType:"text",onload:res,onerror:rej,ontimeout:rej}));

const fetchMedia = async u=>{
    if(/\.(gif|mp4|webm)(?:\?|$)/i.test(u)) return u;
    try {
        if(u.includes("tenor.com")||u.includes("giphy.com")){
            const endpoint = u.includes("tenor.com")
                ? `https://tenor.com/oembed?url=${encodeURIComponent(u)}`
                : `https://giphy.com/services/oembed?url=${encodeURIComponent(u)}`;
            const resp = await httpGet(endpoint);
            const data = JSON.parse(resp.responseText||"{}");
            if(data.html){
                const m = data.html.match(/src="([^"]+)"/);
                if(m?.[1] && /\.(mp4|gif|webm)(?:\?|$)/i.test(m[1])) return m[1];
                try{ const r2=await httpGet(m?.[1]||""); const f=extractMedia(r2.responseText); if(f) return f; } catch{}
            }
            if(data.thumbnail_url && /\.(gif|mp4|webm)(?:\?|$)/i.test(data.thumbnail_url)) return data.thumbnail_url;
        }
    } catch{}
    try{ const r=await httpGet(u); return extractMedia(r.responseText); } catch{}
    return null;
};

const createPlayer = url=>{
    if(!url) return null;
    const isVideo = /\.(mp4|webm)(?:\?|$)/i.test(url.toLowerCase());
    if(isVideo){
        const v=document.createElement("video");
        Object.assign(v,{src:url,autoplay:true,loop:true,muted:true,playsInline:true});
        v.className="cinny-gif-player";
        Object.assign(v.style,{maxHeight:"260px",borderRadius:"8px",marginTop:"6px",transition:"opacity 0.25s",opacity:"0"});
        setTimeout(()=>v.style.opacity="1",40);
        return v;
    } else {
        const i=document.createElement("img");
        i.src=url;
        i.className="cinny-gif-player";
        Object.assign(i.style,{maxHeight:"260px",borderRadius:"8px",marginTop:"6px",transition:"opacity 0.25s",opacity:"0"});
        setTimeout(()=>i.style.opacity="1",40);
        return i;
    }
};

const processLink = async a=>{
    if(a.dataset.processed) return;
    a.dataset.processed="1";
    let url = decodeHref(a.href || a.getAttribute("href")||a.textContent||"");
    if(!url) return;

    const media = await fetchMedia(url);
    if(!media) return;
    if(a.parentElement.querySelector(".cinny-gif-player")) return;

    const player = createPlayer(media);
    if(player){
        a.style.display="none"; // hide URL
        a.parentElement.appendChild(player);
        setTimeout(()=>window.scrollTo({top:document.body.scrollHeight,behavior:"smooth"}),100);
    }
};

const observer = new MutationObserver(ms=>{
    for(const m of ms) for(const n of m.addedNodes) if(n instanceof HTMLElement){
        const links = n.querySelectorAll? n.querySelectorAll("a[href],span"):[];
        links.forEach(a=>{
            const h = a.href || a.getAttribute("href") || a.textContent;
            if(h && /tenor\.com|giphy\.com|\.gif$|\.mp4$|\.webm$/i.test(h)) processLink(a);
        });
    }
});

const start = ()=>{
    const container = [
        document.querySelector("[class*='Timeline']"),
        document.querySelector("[role='log']"),
        document.querySelector("main"),
        document.body
    ].find(Boolean)||document.body;

    observer.observe(container,{childList:true,subtree:true});

    container.querySelectorAll && container.querySelectorAll("a[href],span").forEach(a=>{
        const h=a.href||a.getAttribute("href")||a.textContent||"";
        if(/tenor\.com|giphy\.com|\.gif$|\.mp4$|\.webm$/i.test(h)) processLink(a);
    });
};

const ready = setInterval(()=>{
    if(document.readyState==="complete"){clearInterval(ready);start();}
},500);

})();